Skip to content

Commit e3f1b32

Browse files
authored
Add in a compatibility layer for older versions of numpy. (#185)
Specifically, the version of numpy in RHEL 8 is 1.14, which lacks the structured_to_unstructured and unstructured_to_structured methods of numpy 1.16 and later. Add in compatibility versions here. Signed-off-by: Chris Lalancette <clalancette@openrobotics.org>
1 parent 467f448 commit e3f1b32

File tree

3 files changed

+169
-5
lines changed

3 files changed

+169
-5
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2005-2019 NumPy Developers.
2+
# Copyright 2022 Open Source Robotics Foundation, Inc.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
#
14+
# * Neither the name of the Willow Garage, Inc. nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
30+
# This is compatibility code for older versions of numpy that lack these functions.
31+
# The original code was copied from:
32+
# https://github.com/numpy/numpy/blob/3dec7099ce38cb189880f6f69df318f35ff9a5ea/numpy/lib/recfunctions.py
33+
# and then lightly edited for style.
34+
35+
import numpy as np
36+
37+
38+
def _get_fields_and_offsets(dt, offset=0):
39+
# counts up elements in subarrays, including nested subarrays, and returns
40+
# base dtype and count
41+
def count_elem(dt):
42+
count = 1
43+
while dt.shape != ():
44+
for size in dt.shape:
45+
count *= size
46+
dt = dt.base
47+
return dt, count
48+
49+
fields = []
50+
for name in dt.names:
51+
field = dt.fields[name]
52+
f_dt, f_offset = field[0], field[1]
53+
f_dt, n = count_elem(f_dt)
54+
55+
if f_dt.names is None:
56+
fields.append((np.dtype((f_dt, (n,))), n, f_offset + offset))
57+
else:
58+
subfields = _get_fields_and_offsets(f_dt, f_offset + offset)
59+
size = f_dt.itemsize
60+
61+
for i in range(n):
62+
if i == 0:
63+
# optimization: avoid list comprehension if no subarray
64+
fields.extend(subfields)
65+
else:
66+
fields.extend([(d, c, o + i*size) for d, c, o in subfields])
67+
return fields
68+
69+
70+
def structured_to_unstructured(arr, dtype=None, copy=False, casting='unsafe'):
71+
if arr.dtype.names is None:
72+
raise ValueError('arr must be a structured array')
73+
74+
fields = _get_fields_and_offsets(arr.dtype)
75+
n_fields = len(fields)
76+
if n_fields == 0 and dtype is None:
77+
raise ValueError('arr has no fields. Unable to guess dtype')
78+
elif n_fields == 0:
79+
# too many bugs elsewhere for this to work now
80+
raise NotImplementedError('arr with no fields is not supported')
81+
82+
dts, counts, offsets = zip(*fields)
83+
names = ['f{}'.format(n) for n in range(n_fields)]
84+
85+
if dtype is None:
86+
out_dtype = np.result_type(*[dt.base for dt in dts])
87+
else:
88+
out_dtype = dtype
89+
90+
# Use a series of views and casts to convert to an unstructured array:
91+
92+
# first view using flattened fields (doesn't work for object arrays)
93+
# Note: dts may include a shape for subarrays
94+
flattened_fields = np.dtype({'names': names,
95+
'formats': dts,
96+
'offsets': offsets,
97+
'itemsize': arr.dtype.itemsize})
98+
arr = arr.view(flattened_fields)
99+
100+
# next cast to a packed format with all fields converted to new dtype
101+
packed_fields = np.dtype({'names': names,
102+
'formats': [(out_dtype, dt.shape) for dt in dts]})
103+
arr = arr.astype(packed_fields, copy=copy, casting=casting)
104+
105+
# finally is it safe to view the packed fields as the unstructured type
106+
return arr.view((out_dtype, (sum(counts),)))
107+
108+
109+
def unstructured_to_structured(arr, dtype=None, names=None, align=False,
110+
copy=False, casting='unsafe'):
111+
if arr.shape == ():
112+
raise ValueError('arr must have at least one dimension')
113+
n_elem = arr.shape[-1]
114+
if n_elem == 0:
115+
# too many bugs elsewhere for this to work now
116+
raise NotImplementedError('last axis with size 0 is not supported')
117+
118+
if dtype is None:
119+
if names is None:
120+
names = ['f{}'.format(n) for n in range(n_elem)]
121+
out_dtype = np.dtype([(n, arr.dtype) for n in names], align=align)
122+
fields = _get_fields_and_offsets(out_dtype)
123+
dts, counts, offsets = zip(*fields)
124+
else:
125+
if names is not None:
126+
raise ValueError("don't supply both dtype and names")
127+
# sanity check of the input dtype
128+
fields = _get_fields_and_offsets(dtype)
129+
if len(fields) == 0:
130+
dts, counts, offsets = [], [], []
131+
else:
132+
dts, counts, offsets = zip(*fields)
133+
134+
if n_elem != sum(counts):
135+
raise ValueError('The length of the last dimension of arr must '
136+
'be equal to the number of fields in dtype')
137+
out_dtype = dtype
138+
if align and not out_dtype.isalignedstruct:
139+
raise ValueError('align was True but dtype is not aligned')
140+
141+
names = ['f{}'.format(n) for n in range(len(fields))]
142+
143+
# Use a series of views and casts to convert to a structured array:
144+
145+
# first view as a packed structured array of one dtype
146+
packed_fields = np.dtype({'names': names,
147+
'formats': [(arr.dtype, dt.shape) for dt in dts]})
148+
arr = np.ascontiguousarray(arr).view(packed_fields)
149+
150+
# next cast to an unpacked but flattened format with varied dtypes
151+
flattened_fields = np.dtype({'names': names,
152+
'formats': dts,
153+
'offsets': offsets,
154+
'itemsize': out_dtype.itemsize})
155+
arr = arr.astype(flattened_fields, copy=copy, casting=casting)
156+
157+
# finally view as the final nested dtype and remove the last axis
158+
return arr.view(out_dtype)[..., 0]

‎sensor_msgs_py/sensor_msgs_py/point_cloud2.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@
4343
from typing import Iterable, List, NamedTuple, Optional
4444

4545
import numpy as np
46-
from numpy.lib.recfunctions import (structured_to_unstructured,
47-
unstructured_to_structured)
46+
try:
47+
from numpy.lib.recfunctions import (structured_to_unstructured, unstructured_to_structured)
48+
except ImportError:
49+
from sensor_msgs_py.numpy_compat import (structured_to_unstructured,
50+
unstructured_to_structured)
51+
4852
from sensor_msgs.msg import PointCloud2, PointField
4953
from std_msgs.msg import Header
5054

‎sensor_msgs_py/test/test_point_cloud2.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
import unittest
3232

3333
import numpy as np
34-
from numpy.lib.recfunctions import structured_to_unstructured
34+
try:
35+
from numpy.lib.recfunctions import structured_to_unstructured
36+
except ImportError:
37+
from sensor_msgs_py.numpy_compat import structured_to_unstructured
38+
3539
from sensor_msgs.msg import PointCloud2, PointField
3640
from sensor_msgs_py import point_cloud2
3741
from std_msgs.msg import Header
@@ -257,8 +261,6 @@ def test_create_cloud_xyz32_organized(self):
257261
thispcd = point_cloud2.create_cloud_xyz32(
258262
Header(frame_id='frame'),
259263
points3)
260-
print(thispcd)
261-
print(pcd3)
262264
self.assertEqual(thispcd, pcd3)
263265

264266
def test_create_cloud__non_one_count(self):

0 commit comments

Comments
 (0)