Skip to content

Commit 719959f

Browse files
Merge pull request #1 from NVIDIA-ISAAC-ROS/release-3.2
Isaac ROS 3.2
2 parents 8612e88 + 55a750d commit 719959f

File tree

13 files changed

+489
-2
lines changed

13 files changed

+489
-2
lines changed

.gitattributes

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Ignore Python files in linguist
2+
*.py linguist-detectable=false
3+
4+
# Images
5+
*.gif filter=lfs diff=lfs merge=lfs -text
6+
*.jpg filter=lfs diff=lfs merge=lfs -text
7+
*.png filter=lfs diff=lfs merge=lfs -text
8+
*.psd filter=lfs diff=lfs merge=lfs -text
9+
10+
# Archives
11+
*.gz filter=lfs diff=lfs merge=lfs -text
12+
*.tar filter=lfs diff=lfs merge=lfs -text
13+
*.zip filter=lfs diff=lfs merge=lfs -text
14+
15+
# Documents
16+
*.pdf filter=lfs diff=lfs merge=lfs -text
17+
18+
# Shared libraries
19+
*.so filter=lfs diff=lfs merge=lfs -text
20+
*.so.* filter=lfs diff=lfs merge=lfs -text
21+
22+
# ROS Bags
23+
**/resources/**/*.zstd filter=lfs diff=lfs merge=lfs -text
24+
**/resources/**/*.db3 filter=lfs diff=lfs merge=lfs -text
25+
**/resources/**/*.yaml filter=lfs diff=lfs merge=lfs -text

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Ignore all pycache files
2+
**/__pycache__/**

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
# My Repository
2-
This is my new repository.
1+
# Isaac ROS Data Tools
2+
3+
Tools for inspecting and validating data sent through graphs containing Isaac ROS nodes.
4+
5+
---
6+
7+
## Latest
8+
9+
Update 2024-12-10: Initial release
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
2+
# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# SPDX-License-Identifier: Apache-2.0
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
2+
# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# SPDX-License-Identifier: Apache-2.0
17+
18+
import struct
19+
20+
from isaac_ros_tensor_list_interfaces.msg import Tensor, TensorList
21+
import numpy as np
22+
import rclpy
23+
from rclpy.node import Node
24+
25+
26+
class TensorInspectorNode(Node):
27+
# Conversion map from Tensor.msg's data types to Python types
28+
MSG_TO_PYTHON_MAP = {
29+
# data_type_number : ('struct_specifier', num_bytes)
30+
1: ('b', 1), # int8
31+
2: ('B', 1), # uint8
32+
3: ('h', 2), # int16
33+
4: ('H', 2), # uint16
34+
5: ('i', 4), # int32
35+
6: ('I', 4), # uint32
36+
7: ('q', 8), # int64
37+
8: ('Q', 8), # uint64
38+
9: ('f', 4), # float32
39+
10: ('d', 8) # float64
40+
}
41+
42+
# Conversion map from Numpy types to Tensor.msg's data types
43+
PYTHON_TO_MSG_MAP = {
44+
# dtype : data_type_number
45+
np.dtype('int8'): 1,
46+
np.dtype('uint8'): 2,
47+
np.dtype('int16'): 3,
48+
np.dtype('uint16'): 4,
49+
np.dtype('int32'): 5,
50+
np.dtype('uint32'): 6,
51+
np.dtype('int64'): 7,
52+
np.dtype('uint64'): 8,
53+
np.dtype('float32'): 9,
54+
np.dtype('float64'): 10
55+
}
56+
57+
def __init__(self):
58+
super().__init__('tensor_inspector')
59+
60+
# Input side: whether or not to save the original tensor received
61+
self.subscription = self.create_subscription(
62+
TensorList,
63+
'original_tensor',
64+
self.listener_callback,
65+
10)
66+
67+
self.declare_parameter('original_tensor_npz_path', '')
68+
69+
self.original_tensor_npz_path = self.get_parameter(
70+
'original_tensor_npz_path').get_parameter_value().string_value
71+
72+
self.should_save_original = len(self.original_tensor_npz_path) > 0
73+
74+
if self.should_save_original:
75+
self.get_logger().info(
76+
f'Original tensor received will be saved to {self.original_tensor_npz_path}')
77+
78+
# Output side: whether or not to replace the original tensor with an edited one
79+
self.publisher = self.create_publisher(
80+
TensorList,
81+
'edited_tensor',
82+
10)
83+
84+
self.declare_parameter('edited_tensor_npz_path', '')
85+
86+
edited_tensor_npz_path = self.get_parameter(
87+
'edited_tensor_npz_path').get_parameter_value().string_value
88+
89+
self.should_edit = len(edited_tensor_npz_path) > 0
90+
91+
if self.should_edit:
92+
self.edited_tensor_data = np.load(edited_tensor_npz_path)
93+
self.get_logger().info(
94+
f'Edited tensor will be loaded from {edited_tensor_npz_path}')
95+
96+
def listener_callback(self, msg):
97+
98+
if self.should_save_original:
99+
tensor_dict = {}
100+
for tensor in msg.tensors:
101+
dims = tensor.shape.dims
102+
N = np.prod(dims)
103+
self.get_logger().debug(
104+
f'Tensor {tensor.name} with dims {dims} has N={N} elements')
105+
106+
conversion = self.MSG_TO_PYTHON_MAP.get(tensor.data_type)
107+
if conversion is None:
108+
self.get_logger().error(
109+
f'Original tensor {tensor.name} has unknown data type {tensor.data_type}')
110+
continue
111+
112+
element_format, element_num_bytes = conversion
113+
114+
elements = []
115+
for i in range(N):
116+
# Interpret tensor fields as particular numeric type from bytes
117+
element = struct.unpack(f'<{element_format}', tensor.data[
118+
element_num_bytes * i:
119+
element_num_bytes * (i + 1)
120+
])[0] # struct.unpack returns a tuple with one element
121+
122+
elements.append(element)
123+
124+
# Add element as numpy array to dictionary
125+
tensor_dict[tensor.name] = np.resize(elements, dims)
126+
127+
np.savez(self.original_tensor_npz_path, **tensor_dict)
128+
self.get_logger().debug('Saved original tensor')
129+
130+
if self.should_edit:
131+
tensors = []
132+
for tensor_name, tensor_npz in self.edited_tensor_data.items():
133+
# Create new tensor from data
134+
tensor = Tensor()
135+
136+
tensor.name = tensor_name
137+
138+
tensor.shape.rank = len(tensor_npz.shape)
139+
tensor.shape.dims = tensor_npz.shape
140+
141+
element_data_type = self.PYTHON_TO_MSG_MAP.get(tensor_npz.dtype)
142+
if element_data_type is None:
143+
self.get_logger().error(
144+
f'Edited tensor {tensor.name} has unknown data type {tensor_npz.dtype}')
145+
continue
146+
147+
tensor.data_type = element_data_type
148+
tensor.strides = tensor_npz.strides
149+
150+
tensor.data = tensor_npz.tobytes()
151+
152+
tensors.append(tensor)
153+
154+
# Overwrite original tensors with new tensors
155+
msg.tensors = tensors
156+
self.get_logger().debug('Edited tensor')
157+
158+
self.publisher.publish(msg)
159+
self.get_logger().debug('Published tensor message')
160+
161+
162+
def main(args=None):
163+
rclpy.init(args=args)
164+
node = TensorInspectorNode()
165+
rclpy.spin(node)
166+
rclpy.shutdown()
167+
168+
169+
if __name__ == '__main__':
170+
main()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>isaac_ros_tensor_inspector</name>
5+
<version>3.2.0</version>
6+
<description>Tools for inspecting and verifying TensorList messages</description>
7+
8+
<maintainer email="[email protected]">Isaac ROS Maintainers</maintainer>
9+
<license>Apache-2.0</license>
10+
<url type="website">https://developer.nvidia.com/isaac-ros/</url>
11+
<author>Jaiveer Singh</author>
12+
<build_depend>isaac_ros_common</build_depend>
13+
14+
<depend>isaac_ros_tensor_list_interfaces</depend>
15+
16+
<test_depend>ament_copyright</test_depend>
17+
<test_depend>ament_flake8</test_depend>
18+
<test_depend>ament_pep257</test_depend>
19+
<test_depend>python3-pytest</test_depend>
20+
21+
<export>
22+
<build_type>ament_python</build_type>
23+
</export>
24+
</package>

isaac_ros_tensor_inspector/resource/isaac_ros_tensor_inspector

Whitespace-only changes.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
4+
# Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# SPDX-License-Identifier: Apache-2.0
19+
20+
import argparse as ap
21+
import math
22+
from pathlib import Path
23+
24+
import numpy as np
25+
26+
27+
def compare(
28+
tensor1_path: Path,
29+
tensor2_path: Path,
30+
output_directory: Path,
31+
rel_tol: float,
32+
abs_tol: float
33+
):
34+
tensor_list1 = np.load(tensor1_path, allow_pickle=True)
35+
tensor_list2 = np.load(tensor2_path, allow_pickle=True)
36+
37+
assert len(tensor_list1.keys()) == len(tensor_list2.keys()), \
38+
'Tensor lists are not the same length'
39+
40+
logfile_path = output_directory / \
41+
f'comparator_logfile_{tensor1_path.stem}_{tensor2_path.stem}.txt'
42+
print(f'Logfile path: {logfile_path}')
43+
with open(logfile_path, 'w+') as logfile:
44+
45+
for tensor1_name, tensor2_name in zip(tensor_list1, tensor_list2):
46+
print(f'T1 name: \t{tensor1_name}')
47+
print(f'T2 name: \t{tensor2_name}')
48+
49+
tensor1, tensor2 = tensor_list1[tensor1_name], tensor_list2[tensor2_name]
50+
51+
print(f'T1 shape: \t{tensor1.shape}')
52+
print(f'T2 shape: \t{tensor2.shape}')
53+
54+
count_good, count_bad = 0, 0
55+
56+
logfile.write(
57+
f'Comparing: {tensor1_path.name} vs. {tensor2_path.name}\n')
58+
logfile.write(
59+
f'Relative Tolerance: {rel_tol} \tAbsolute Tolerance: {abs_tol}\n\n')
60+
61+
for (index1, e1), (index2, e2) in zip(
62+
np.ndenumerate(tensor1), np.ndenumerate(tensor2)
63+
):
64+
status_string = ''
65+
if math.isclose(e1, e2, rel_tol=rel_tol, abs_tol=abs_tol):
66+
count_good += 1
67+
status_string = 'GOOD'
68+
else:
69+
count_bad += 1
70+
status_string = 'BAD'
71+
72+
logfile.writelines([
73+
status_string + '\n',
74+
f'T1 \t{index1}: {e1:.10f}\n',
75+
f'T2 \t{index2}: {e2:.10f}\n',
76+
'\n'
77+
])
78+
79+
logfile.write(f'Count GOOD: {count_good} vs. Count BAD: {count_bad}\n')
80+
81+
print(f'Count GOOD: {count_good}')
82+
print(f'Count BAD: {count_bad}')
83+
84+
85+
if __name__ == '__main__':
86+
parser = ap.ArgumentParser()
87+
88+
parser.add_argument('tensor1_filename')
89+
parser.add_argument('tensor2_filename')
90+
parser.add_argument('output_directory')
91+
parser.add_argument('--abs-tol', '-a', default=0.0, type=float,
92+
help='Absolute numerical error acceptable per element')
93+
parser.add_argument('--rel-tol', '-r', default=0.05, type=float,
94+
help='Relative percentage error acceptable per element')
95+
96+
args = parser.parse_args()
97+
98+
compare(Path(args.tensor1_filename), Path(args.tensor2_filename), Path(args.output_directory),
99+
rel_tol=args.rel_tol, abs_tol=args.abs_tol)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[develop]
2+
script_dir=$base/lib/isaac_ros_tensor_inspector
3+
[install]
4+
install_scripts=$base/lib/isaac_ros_tensor_inspector

0 commit comments

Comments
 (0)