nb-touch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. nb_touch-0.1.0/LICENSE +21 -0
  2. nb_touch-0.1.0/PKG-INFO +208 -0
  3. nb_touch-0.1.0/README.md +177 -0
  4. nb_touch-0.1.0/pyproject.toml +61 -0
  5. nb_touch-0.1.0/pyproject.toml.orig +58 -0
  6. nb_touch-0.1.0/src/nb_touch/__init__.py +5 -0
  7. nb_touch-0.1.0/src/nb_touch/__tests__/test_init.py +6 -0
  8. nb_touch-0.1.0/src/nb_touch/calibration/__init__.py +9 -0
  9. nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_orientation.py +170 -0
  10. nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_samples.py +105 -0
  11. nb_touch-0.1.0/src/nb_touch/calibration/__tests__/test_synthetic.py +107 -0
  12. nb_touch-0.1.0/src/nb_touch/calibration/orientation.py +173 -0
  13. nb_touch-0.1.0/src/nb_touch/calibration/samples.py +138 -0
  14. nb_touch-0.1.0/src/nb_touch/calibration/synthetic.py +158 -0
  15. nb_touch-0.1.0/src/nb_touch/contact/__init__.py +27 -0
  16. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_active.py +16 -0
  17. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_contacts.py +189 -0
  18. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_cop_model.py +162 -0
  19. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_detector.py +72 -0
  20. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_filters.py +61 -0
  21. nb_touch-0.1.0/src/nb_touch/contact/__tests__/test_zones.py +36 -0
  22. nb_touch-0.1.0/src/nb_touch/contact/active.py +25 -0
  23. nb_touch-0.1.0/src/nb_touch/contact/contacts.py +208 -0
  24. nb_touch-0.1.0/src/nb_touch/contact/cop_model.py +205 -0
  25. nb_touch-0.1.0/src/nb_touch/contact/detector.py +68 -0
  26. nb_touch-0.1.0/src/nb_touch/contact/filters.py +113 -0
  27. nb_touch-0.1.0/src/nb_touch/contact/zones.py +55 -0
  28. nb_touch-0.1.0/src/nb_touch/geometry/__init__.py +24 -0
  29. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_frames.py +66 -0
  30. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_neighbours.py +34 -0
  31. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_normals.py +50 -0
  32. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_so3.py +87 -0
  33. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_synthetic.py +81 -0
  34. nb_touch-0.1.0/src/nb_touch/geometry/__tests__/test_transforms.py +77 -0
  35. nb_touch-0.1.0/src/nb_touch/geometry/frames.py +73 -0
  36. nb_touch-0.1.0/src/nb_touch/geometry/neighbours.py +48 -0
  37. nb_touch-0.1.0/src/nb_touch/geometry/normals.py +127 -0
  38. nb_touch-0.1.0/src/nb_touch/geometry/so3.py +75 -0
  39. nb_touch-0.1.0/src/nb_touch/geometry/synthetic.py +130 -0
  40. nb_touch-0.1.0/src/nb_touch/geometry/transforms.py +73 -0
  41. nb_touch-0.1.0/src/nb_touch/io/__init__.py +17 -0
  42. nb_touch-0.1.0/src/nb_touch/io/__tests__/test_recordings.py +145 -0
  43. nb_touch-0.1.0/src/nb_touch/io/__tests__/test_sensor_geometry.py +166 -0
  44. nb_touch-0.1.0/src/nb_touch/io/recordings.py +149 -0
  45. nb_touch-0.1.0/src/nb_touch/io/sensor_geometry.py +209 -0
  46. nb_touch-0.1.0/src/nb_touch/py.typed +0 -0
  47. nb_touch-0.1.0/src/nb_touch/utils/__init__.py +1 -0
  48. nb_touch-0.1.0/src/nb_touch/utils/__tests__/test_units.py +14 -0
  49. nb_touch-0.1.0/src/nb_touch/utils/__tests__/test_yaml_io.py +39 -0
  50. nb_touch-0.1.0/src/nb_touch/utils/units.py +19 -0
  51. nb_touch-0.1.0/src/nb_touch/utils/yaml_io.py +46 -0
  52. nb_touch-0.1.0/src/nb_touch/viz/__init__.py +2 -0
  53. nb_touch-0.1.0/src/nb_touch/viz/__tests__/conftest.py +15 -0
  54. nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_arrows.py +134 -0
  55. nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_markers.py +31 -0
  56. nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_sensor.py +116 -0
  57. nb_touch-0.1.0/src/nb_touch/viz/__tests__/test_sliders.py +47 -0
  58. nb_touch-0.1.0/src/nb_touch/viz/arrows.py +139 -0
  59. nb_touch-0.1.0/src/nb_touch/viz/markers.py +41 -0
  60. nb_touch-0.1.0/src/nb_touch/viz/sensor.py +152 -0
  61. nb_touch-0.1.0/src/nb_touch/viz/sliders.py +56 -0
nb_touch-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Océan·e Patiny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: nb-touch
3
+ Version: 0.1.0
4
+ Summary: Hardware-agnostic processing and calibration of taxel array touch sensors.
5
+ Keywords: tactile,touch sensor,taxel,robotics,center of pressure
6
+ Author: Oceane Patiny
7
+ Author-email: Oceane Patiny <oceane@patiny.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: numpy>=1.26
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: torch>=2.2 ; extra == 'calibration'
22
+ Requires-Dist: mujoco>=3.16.0 ; extra == 'viz'
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/opatiny/nb-touch
25
+ Project-URL: Repository, https://github.com/opatiny/nb-touch
26
+ Project-URL: Issues, https://github.com/opatiny/nb-touch/issues
27
+ Project-URL: Paper, https://arxiv.org/abs/2605.28812
28
+ Provides-Extra: calibration
29
+ Provides-Extra: viz
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Beyond binary touch sensing
33
+
34
+ This library is useful for: processing data of a robot equipped with taxel array sensors, as well as to detect the taxels orientation based on experimental measurements.
35
+
36
+ Hardware-agnostic python library implementing the main functionalities of the article: Pan, Jiahe, et al. "Beyond Binary: Sim-to-Real Dexterous Manipulation with Physics-Grounded Contact Representation." arXiv preprint arXiv:2605.28812 (2026).
37
+ Url: https://arxiv.org/abs/2605.28812
38
+
39
+ Main features:
40
+
41
+ - Conversion from Center of Pressure (CoP) force to individual taxel forces.
42
+ - Conversion from taxel forces to CoP force.
43
+ - Tools for automatic taxel orientation detection: this requires torque sensors for all the links active when touching the sensor.
44
+
45
+ ## Installation
46
+
47
+ - `pip install nb-touch`: CoP model, contact detection, geometry and I/O.
48
+ - `pip install "nb-touch[calibration]"`: adds the taxel orientation calibration (needs `torch`).
49
+ - `pip install "nb-touch[viz]"`: adds the MuJoCo drawing helpers used by the example (needs `mujoco`).
50
+
51
+ ## Conventions
52
+
53
+ - Normals at the taxels point outward, out of the sensing surface. The sensor frame and every taxel frame are right-handed, with z along the outward normal.
54
+ - Forces are the forces acting on the sensor, as the taxels feel them: a push points against the outward normal.
55
+
56
+ Be careful:
57
+
58
+ - The paper's normals $n_i$ point inward: they are the opposites of ours.
59
+ - Hardware readings must be converted to these conventions before use.
60
+
61
+ Source: [arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Sections 3.2 (inward normals, $f_{cop}$ acting on the robot link) and 3.3 ($\tau = -J^T f$).
62
+
63
+ ## From taxels to resultant force (and conversely)
64
+
65
+ The resultant force is computed from the taxel forces using the CoP model presented in the paper.
66
+
67
+ Both examples use a flat 3×3 grid of taxels, 3 mm apart, numbered row by row: taxel 4 is the centre one, at the origin. Positions are in m, forces in N, both in the sensor frame.
68
+
69
+ ### From taxels to CoP
70
+
71
+ With a 1 N push on the centre taxel only:
72
+
73
+ ```python
74
+ import numpy as np
75
+
76
+ from nb_touch.contact import ContactDetector, ContactSettings
77
+ from nb_touch.geometry.synthetic import plane_sensor
78
+
79
+ np.set_printoptions(precision=3, suppress=True)
80
+ sensor = plane_sensor(rows=3, cols=3, spacing=0.003)
81
+ detector = ContactDetector(sensor, ContactSettings(min_taxels=1))
82
+
83
+ forces = np.zeros((9, 3)) # one force per taxel
84
+ forces[4] = [0.0, 0.0, -1.0]
85
+ contacts = detector.update(forces, in_taxel_frames=False)
86
+ print(contacts[0].cop) # [0. 0. 0.]
87
+ print(contacts[0].cop_force) # [ 0. 0. -1.]
88
+ ```
89
+
90
+ `min_taxels=1` lets a single taxel make a contact (default: 3).
91
+
92
+ ### From CoP to taxels
93
+
94
+ With a 1 N push at the centre:
95
+
96
+ ```python
97
+ from nb_touch.contact import (
98
+ cop_normal,
99
+ cop_to_taxel_forces,
100
+ spacing_sigma,
101
+ taxel_force_matrices,
102
+ )
103
+ from nb_touch.geometry import normals_from_orientations
104
+
105
+ normals = normals_from_orientations(sensor.orientations)
106
+ cop = np.zeros(3)
107
+ normal = cop_normal(sensor.positions, normals, cop)
108
+ sigma = spacing_sigma(sensor.positions) # 1.5 x the median taxel spacing
109
+ matrices = taxel_force_matrices(sensor.positions, normals, cop, normal, sigma)
110
+ print(cop_to_taxel_forces([0.0, 0.0, -1.0], matrices))
111
+ ```
112
+
113
+ Expected print, one row per taxel:
114
+
115
+ ```text
116
+ [[-0.221 -0.221 -0.56 ]
117
+ [ 0. -0.193 -0.777]
118
+ [ 0.221 -0.221 -0.56 ]
119
+ [-0.193 0. -0.777]
120
+ [ 0. 0. -1. ]
121
+ [ 0.193 0. -0.777]
122
+ [-0.221 0.221 -0.56 ]
123
+ [ 0. 0.193 -0.777]
124
+ [ 0.221 0.221 -0.56 ]]
125
+ ```
126
+
127
+ The centre taxel gets the whole push. The others get less the further they are, tilted away from the CoP.
128
+
129
+ For more details, see [docs/theory/cop.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md).
130
+
131
+ ## CoP examples
132
+
133
+ An example script for the taxel to CoP conversion can be found in [`examples/cop_in_mujoco.py`](https://github.com/opatiny/nb-touch/blob/main/examples/cop_in_mujoco.py). It provides the taxel-CoP mapping on a synthetic sensor, shown in MuJoCo. It needs the viz extra. Run it in an activated terminal with `python examples/cop_in_mujoco.py` (on macOS: `mjpython examples/cop_in_mujoco.py`).
134
+
135
+ It shows:
136
+
137
+ - **gray spheres:** all the sensor taxels
138
+ - **yellow spheres**: taxels in a contact zone
139
+ - **blue arrows**: resultant forces for each taxel
140
+ - **green arrow and dot**: true CoP force and position
141
+ - **red arrow and dot**: estimated CoP force and position, recomputed from the taxel forces
142
+
143
+ Why the green and red arrows don't match perfectly? See [Known bias](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md#known-bias).
144
+
145
+ ### Planar sensor
146
+
147
+ https://github.com/user-attachments/assets/189b1fb1-31b1-47bc-b6a7-87e07696a47e
148
+
149
+ ### Cylindrical sensor
150
+
151
+ https://github.com/user-attachments/assets/d66bacdf-331c-4f35-a88c-500e3954136c
152
+
153
+ ### Spherical sensor
154
+
155
+ https://github.com/user-attachments/assets/cd5f517f-bf19-4a19-b95b-86a3a4ac1ba7
156
+
157
+ ## Taxel orientation detection
158
+
159
+ ### Generate a recording file
160
+
161
+ A recording holds, for each sample, the taxel forces (each in its taxel's frame), the joint angles and the joint torques of the finger carrying the sensor, while an object is pressed at random places on the sensor. The paper uses 2400 samples: 2 min at 20 Hz ([arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Appendix A).
162
+
163
+ File format: [Recording format](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#recording-format).
164
+
165
+ Synthetic recording of 2400 samples on the 3×3 grid above, mounted on a 4-joint finger, with true taxel orientations (the orientations on the real hardware) up to 20° away from the geometry's:
166
+
167
+ ```python
168
+ from nb_touch.calibration.synthetic import perturbed_orientations, synthetic_recording
169
+ from nb_touch.io import save_recording
170
+
171
+ rng = np.random.default_rng(0)
172
+ true_orientations = perturbed_orientations(sensor.orientations, np.radians(20), rng)
173
+ recording = synthetic_recording(sensor, true_orientations, num_samples=2400, rng=rng)
174
+ save_recording(recording, "results/recording") # recording.npz and recording.yaml
175
+ ```
176
+
177
+ How the joint torques and taxel forces are simulated: [docs/synthetic-recording.md](https://github.com/opatiny/nb-touch/blob/main/docs/synthetic-recording.md).
178
+
179
+ With real hardware, build a `Recording` from the measured arrays instead (see [Reading and writing a recording](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#reading-and-writing-a-recording)).
180
+
181
+ ### Use recording to compute the taxel orientations
182
+
183
+ The calibration needs the recording, the sensor geometry (orientations are optional, they are only the starting point) and a function `jacobian(q, link, point)` returning the `(3, J)` position Jacobian of a point of the sensor, in the sensor frame. Here, the Jacobian of the synthetic finger:
184
+
185
+ ```python
186
+ from nb_touch.calibration import calibrate_orientations
187
+ from nb_touch.calibration.synthetic import finger_jacobian
188
+ from nb_touch.geometry.transforms import angles_between_rotations
189
+ from nb_touch.io import load_recording
190
+
191
+ recording = load_recording("results/recording")
192
+ result = calibrate_orientations(recording, sensor, finger_jacobian)
193
+ before = angles_between_rotations(sensor.orientations, true_orientations)
194
+ after = angles_between_rotations(result.geometry.orientations, true_orientations)
195
+ print(np.degrees(before).max().round(1), np.degrees(after).max().round(1)) # 19.6 6.4
196
+ ```
197
+
198
+ `result.geometry` is the sensor geometry with the calibrated orientations, `result.losses` the torque MSE at each step. The paper's 100 Adam steps are the default; with `settings=CalibrationSettings(steps=1000)`, the largest is expected to drop to 1.0°.
199
+
200
+ For more details, see [docs/theory/taxel-orientation.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md).
201
+
202
+ ## Recording calibration data on a real hand
203
+
204
+ This library was tested on a real robotic hand: the Orca hand touch. The code and results are accessible here:
205
+
206
+ Coming soon
207
+
208
+ <!--- todo: add link once code is public -->
@@ -0,0 +1,177 @@
1
+ # Beyond binary touch sensing
2
+
3
+ This library is useful for: processing data of a robot equipped with taxel array sensors, as well as to detect the taxels orientation based on experimental measurements.
4
+
5
+ Hardware-agnostic python library implementing the main functionalities of the article: Pan, Jiahe, et al. "Beyond Binary: Sim-to-Real Dexterous Manipulation with Physics-Grounded Contact Representation." arXiv preprint arXiv:2605.28812 (2026).
6
+ Url: https://arxiv.org/abs/2605.28812
7
+
8
+ Main features:
9
+
10
+ - Conversion from Center of Pressure (CoP) force to individual taxel forces.
11
+ - Conversion from taxel forces to CoP force.
12
+ - Tools for automatic taxel orientation detection: this requires torque sensors for all the links active when touching the sensor.
13
+
14
+ ## Installation
15
+
16
+ - `pip install nb-touch`: CoP model, contact detection, geometry and I/O.
17
+ - `pip install "nb-touch[calibration]"`: adds the taxel orientation calibration (needs `torch`).
18
+ - `pip install "nb-touch[viz]"`: adds the MuJoCo drawing helpers used by the example (needs `mujoco`).
19
+
20
+ ## Conventions
21
+
22
+ - Normals at the taxels point outward, out of the sensing surface. The sensor frame and every taxel frame are right-handed, with z along the outward normal.
23
+ - Forces are the forces acting on the sensor, as the taxels feel them: a push points against the outward normal.
24
+
25
+ Be careful:
26
+
27
+ - The paper's normals $n_i$ point inward: they are the opposites of ours.
28
+ - Hardware readings must be converted to these conventions before use.
29
+
30
+ Source: [arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Sections 3.2 (inward normals, $f_{cop}$ acting on the robot link) and 3.3 ($\tau = -J^T f$).
31
+
32
+ ## From taxels to resultant force (and conversely)
33
+
34
+ The resultant force is computed from the taxel forces using the CoP model presented in the paper.
35
+
36
+ Both examples use a flat 3×3 grid of taxels, 3 mm apart, numbered row by row: taxel 4 is the centre one, at the origin. Positions are in m, forces in N, both in the sensor frame.
37
+
38
+ ### From taxels to CoP
39
+
40
+ With a 1 N push on the centre taxel only:
41
+
42
+ ```python
43
+ import numpy as np
44
+
45
+ from nb_touch.contact import ContactDetector, ContactSettings
46
+ from nb_touch.geometry.synthetic import plane_sensor
47
+
48
+ np.set_printoptions(precision=3, suppress=True)
49
+ sensor = plane_sensor(rows=3, cols=3, spacing=0.003)
50
+ detector = ContactDetector(sensor, ContactSettings(min_taxels=1))
51
+
52
+ forces = np.zeros((9, 3)) # one force per taxel
53
+ forces[4] = [0.0, 0.0, -1.0]
54
+ contacts = detector.update(forces, in_taxel_frames=False)
55
+ print(contacts[0].cop) # [0. 0. 0.]
56
+ print(contacts[0].cop_force) # [ 0. 0. -1.]
57
+ ```
58
+
59
+ `min_taxels=1` lets a single taxel make a contact (default: 3).
60
+
61
+ ### From CoP to taxels
62
+
63
+ With a 1 N push at the centre:
64
+
65
+ ```python
66
+ from nb_touch.contact import (
67
+ cop_normal,
68
+ cop_to_taxel_forces,
69
+ spacing_sigma,
70
+ taxel_force_matrices,
71
+ )
72
+ from nb_touch.geometry import normals_from_orientations
73
+
74
+ normals = normals_from_orientations(sensor.orientations)
75
+ cop = np.zeros(3)
76
+ normal = cop_normal(sensor.positions, normals, cop)
77
+ sigma = spacing_sigma(sensor.positions) # 1.5 x the median taxel spacing
78
+ matrices = taxel_force_matrices(sensor.positions, normals, cop, normal, sigma)
79
+ print(cop_to_taxel_forces([0.0, 0.0, -1.0], matrices))
80
+ ```
81
+
82
+ Expected print, one row per taxel:
83
+
84
+ ```text
85
+ [[-0.221 -0.221 -0.56 ]
86
+ [ 0. -0.193 -0.777]
87
+ [ 0.221 -0.221 -0.56 ]
88
+ [-0.193 0. -0.777]
89
+ [ 0. 0. -1. ]
90
+ [ 0.193 0. -0.777]
91
+ [-0.221 0.221 -0.56 ]
92
+ [ 0. 0.193 -0.777]
93
+ [ 0.221 0.221 -0.56 ]]
94
+ ```
95
+
96
+ The centre taxel gets the whole push. The others get less the further they are, tilted away from the CoP.
97
+
98
+ For more details, see [docs/theory/cop.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md).
99
+
100
+ ## CoP examples
101
+
102
+ An example script for the taxel to CoP conversion can be found in [`examples/cop_in_mujoco.py`](https://github.com/opatiny/nb-touch/blob/main/examples/cop_in_mujoco.py). It provides the taxel-CoP mapping on a synthetic sensor, shown in MuJoCo. It needs the viz extra. Run it in an activated terminal with `python examples/cop_in_mujoco.py` (on macOS: `mjpython examples/cop_in_mujoco.py`).
103
+
104
+ It shows:
105
+
106
+ - **gray spheres:** all the sensor taxels
107
+ - **yellow spheres**: taxels in a contact zone
108
+ - **blue arrows**: resultant forces for each taxel
109
+ - **green arrow and dot**: true CoP force and position
110
+ - **red arrow and dot**: estimated CoP force and position, recomputed from the taxel forces
111
+
112
+ Why the green and red arrows don't match perfectly? See [Known bias](https://github.com/opatiny/nb-touch/blob/main/docs/theory/cop.md#known-bias).
113
+
114
+ ### Planar sensor
115
+
116
+ https://github.com/user-attachments/assets/189b1fb1-31b1-47bc-b6a7-87e07696a47e
117
+
118
+ ### Cylindrical sensor
119
+
120
+ https://github.com/user-attachments/assets/d66bacdf-331c-4f35-a88c-500e3954136c
121
+
122
+ ### Spherical sensor
123
+
124
+ https://github.com/user-attachments/assets/cd5f517f-bf19-4a19-b95b-86a3a4ac1ba7
125
+
126
+ ## Taxel orientation detection
127
+
128
+ ### Generate a recording file
129
+
130
+ A recording holds, for each sample, the taxel forces (each in its taxel's frame), the joint angles and the joint torques of the finger carrying the sensor, while an object is pressed at random places on the sensor. The paper uses 2400 samples: 2 min at 20 Hz ([arXiv 2605.28812](https://arxiv.org/html/2605.28812v1), Appendix A).
131
+
132
+ File format: [Recording format](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#recording-format).
133
+
134
+ Synthetic recording of 2400 samples on the 3×3 grid above, mounted on a 4-joint finger, with true taxel orientations (the orientations on the real hardware) up to 20° away from the geometry's:
135
+
136
+ ```python
137
+ from nb_touch.calibration.synthetic import perturbed_orientations, synthetic_recording
138
+ from nb_touch.io import save_recording
139
+
140
+ rng = np.random.default_rng(0)
141
+ true_orientations = perturbed_orientations(sensor.orientations, np.radians(20), rng)
142
+ recording = synthetic_recording(sensor, true_orientations, num_samples=2400, rng=rng)
143
+ save_recording(recording, "results/recording") # recording.npz and recording.yaml
144
+ ```
145
+
146
+ How the joint torques and taxel forces are simulated: [docs/synthetic-recording.md](https://github.com/opatiny/nb-touch/blob/main/docs/synthetic-recording.md).
147
+
148
+ With real hardware, build a `Recording` from the measured arrays instead (see [Reading and writing a recording](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md#reading-and-writing-a-recording)).
149
+
150
+ ### Use recording to compute the taxel orientations
151
+
152
+ The calibration needs the recording, the sensor geometry (orientations are optional, they are only the starting point) and a function `jacobian(q, link, point)` returning the `(3, J)` position Jacobian of a point of the sensor, in the sensor frame. Here, the Jacobian of the synthetic finger:
153
+
154
+ ```python
155
+ from nb_touch.calibration import calibrate_orientations
156
+ from nb_touch.calibration.synthetic import finger_jacobian
157
+ from nb_touch.geometry.transforms import angles_between_rotations
158
+ from nb_touch.io import load_recording
159
+
160
+ recording = load_recording("results/recording")
161
+ result = calibrate_orientations(recording, sensor, finger_jacobian)
162
+ before = angles_between_rotations(sensor.orientations, true_orientations)
163
+ after = angles_between_rotations(result.geometry.orientations, true_orientations)
164
+ print(np.degrees(before).max().round(1), np.degrees(after).max().round(1)) # 19.6 6.4
165
+ ```
166
+
167
+ `result.geometry` is the sensor geometry with the calibrated orientations, `result.losses` the torque MSE at each step. The paper's 100 Adam steps are the default; with `settings=CalibrationSettings(steps=1000)`, the largest is expected to drop to 1.0°.
168
+
169
+ For more details, see [docs/theory/taxel-orientation.md](https://github.com/opatiny/nb-touch/blob/main/docs/theory/taxel-orientation.md).
170
+
171
+ ## Recording calibration data on a real hand
172
+
173
+ This library was tested on a real robotic hand: the Orca hand touch. The code and results are accessible here:
174
+
175
+ Coming soon
176
+
177
+ <!--- todo: add link once code is public -->
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "nb-touch"
3
+ version = "0.1.0"
4
+ description = "Hardware-agnostic processing and calibration of taxel array touch sensors."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ keywords = [
10
+ "tactile",
11
+ "touch sensor",
12
+ "taxel",
13
+ "robotics",
14
+ "center of pressure",
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Scientific/Engineering",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "numpy>=1.26",
29
+ "pyyaml>=6.0",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Oceane Patiny"
34
+ email = "oceane@patiny.com"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/opatiny/nb-touch"
38
+ Repository = "https://github.com/opatiny/nb-touch"
39
+ Issues = "https://github.com/opatiny/nb-touch/issues"
40
+ Paper = "https://arxiv.org/abs/2605.28812"
41
+
42
+ [project.optional-dependencies]
43
+ calibration = ["torch>=2.2"]
44
+ viz = ["mujoco>=3.16.0"]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "nb-touch[calibration]",
49
+ "pytest",
50
+ ]
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.12.15,<0.13.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [tool.uv.build-backend]
57
+ wheel-exclude = ["**/__tests__"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["src"]
61
+ addopts = ["--import-mode=importlib"]
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "nb-touch"
3
+ version = "0.1.0"
4
+ description = "Hardware-agnostic processing and calibration of taxel array touch sensors."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Oceane Patiny", email = "oceane@patiny.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ keywords = ["tactile", "touch sensor", "taxel", "robotics", "center of pressure"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Science/Research",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Scientific/Engineering",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "numpy>=1.26",
26
+ "pyyaml>=6.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/opatiny/nb-touch"
31
+ Repository = "https://github.com/opatiny/nb-touch"
32
+ Issues = "https://github.com/opatiny/nb-touch/issues"
33
+ Paper = "https://arxiv.org/abs/2605.28812"
34
+
35
+ [project.optional-dependencies]
36
+ calibration = [
37
+ "torch>=2.2",
38
+ ]
39
+ viz = [
40
+ "mujoco>=3.16.0",
41
+ ]
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "nb-touch[calibration]",
46
+ "pytest",
47
+ ]
48
+
49
+ [build-system]
50
+ requires = ["uv_build>=0.12.15,<0.13.0"]
51
+ build-backend = "uv_build"
52
+
53
+ [tool.uv.build-backend]
54
+ wheel-exclude = ["**/__tests__"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["src"]
58
+ addopts = ["--import-mode=importlib"]
@@ -0,0 +1,5 @@
1
+ """Hardware-agnostic processing and calibration of taxel array touch sensors."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("nb-touch")
@@ -0,0 +1,6 @@
1
+ import nb_touch
2
+
3
+
4
+ def test_version_is_set():
5
+ assert isinstance(nb_touch.__version__, str)
6
+ assert nb_touch.__version__ != ""
@@ -0,0 +1,9 @@
1
+ """Taxel orientation calibration from joint torques."""
2
+
3
+ from nb_touch.calibration.orientation import (
4
+ CalibrationResult,
5
+ CalibrationSettings,
6
+ calibrate_orientations,
7
+ )
8
+
9
+ __all__ = ["CalibrationResult", "CalibrationSettings", "calibrate_orientations"]