yostlabs 2025.9.17__py3-none-any.whl → 2025.10.6__py3-none-any.whl

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.
@@ -51,6 +51,15 @@ def quat_from_axis_angle(axis: list[float], angle: float):
51
51
  quat.append(math.cos(angle / 2))
52
52
  return quat
53
53
 
54
+ #There are multiple valid quats that can be returned by this. The intention of this function
55
+ #is to be able to rotate an arrow by the quat such that it points the correct direction. The rotation
56
+ #of that arrow along its axis may differ though
57
+ def quat_from_one_vector(vec: list[float]):
58
+ vec = _vec.vec_normalize(vec)
59
+ perpendicular = _vec.vec_normalize(_vec.vec_cross([0, 0, 1], vec))
60
+ angle = math.acos(_vec.vec_dot([0, 0, 1], vec))
61
+ return quat_from_axis_angle(perpendicular, angle)
62
+
54
63
  def quat_from_two_vectors(forward: list[float], down: list[float]):
55
64
  """
56
65
  This function requires two orthogonal vectors to work
@@ -150,4 +150,214 @@ class ThreespaceGradientDescentCalibration:
150
150
  print(f"Final Rating: {best_rating}")
151
151
  print(f"Final Params: {best_params}")
152
152
 
153
- return best_params
153
+ return best_params
154
+
155
+ def fibonacci_sphere(samples=1000):
156
+ points = []
157
+ phi = np.pi * (3. - np.sqrt(5.)) # golden angle
158
+
159
+ for i in range(samples):
160
+ y = 1 - (i / float(samples - 1)) * 2 # y goes from 1 to -1
161
+ radius = np.sqrt(1 - y * y)
162
+ theta = phi * i
163
+ x = np.cos(theta) * radius
164
+ z = np.sin(theta) * radius
165
+ points.append((x, y, z))
166
+
167
+ return np.array(points)
168
+
169
+ class ThreespaceSphereCalibration:
170
+
171
+ def __init__(self, max_comparison_points=500):
172
+ self.buffer = 0.1
173
+ self.test_points = fibonacci_sphere(samples=max_comparison_points)
174
+ self.clear()
175
+
176
+ def process_point(self, raw_mag: list[float]):
177
+ if len(self.samples) == 0:
178
+ self.samples = np.array([raw_mag])
179
+ return True
180
+
181
+ raw_mag = np.array(raw_mag, dtype=np.float64)
182
+ new_len = np.linalg.norm(raw_mag)
183
+
184
+ avg_len = np.linalg.norm(self.samples, axis=1)
185
+ avg_len = (avg_len + new_len) / 2 * self.buffer
186
+
187
+ dist = np.linalg.norm(self.samples - raw_mag, axis=1)
188
+ if np.any(dist < avg_len):
189
+ return False
190
+
191
+ self.samples = np.concatenate((self.samples, [raw_mag]))
192
+ self.__update_density(raw_mag / new_len)
193
+ return True
194
+
195
+ def __update_density(self, normalized_point: np.ndarray):
196
+ #First check to see if the new point is the closest point for any of the previous points
197
+ dots = np.sum(self.test_points * normalized_point, axis=1)
198
+ self.closest_dot = np.maximum(self.closest_dot, dots)
199
+ self.largest_delta_index = np.argmin(self.closest_dot)
200
+ self.largest_delta = np.rad2deg(np.acos(self.closest_dot[self.largest_delta_index]))
201
+
202
+ def clear(self):
203
+ self.samples = np.array([])
204
+ self.closest_dot = np.array([-1] * len(self.test_points))
205
+ self.largest_delta = 180
206
+ self.largest_delta_index = 0
207
+
208
+ @property
209
+ def sparsest_vector(self):
210
+ return self.test_points[self.largest_delta_index]
211
+
212
+ def calculate(self):
213
+ """
214
+ Returns matrix and bias
215
+ """
216
+ comp_A, comp_b, comp_d = ThreespaceSphereCalibration.alternating_least_squares(np.array(self.samples))
217
+ comp_A, comp_b = ThreespaceSphereCalibration.make_calibration_params(comp_A, comp_b, comp_d, 1)
218
+
219
+ comp_scale: float = comp_A[0][0] + comp_A[1][1] + comp_A[2][2]
220
+ comp_scale /= 3
221
+
222
+ comp_A /= comp_scale
223
+
224
+ return comp_A.flatten().tolist(), comp_b.tolist()
225
+
226
+ @staticmethod
227
+ def alternating_least_squares(data: np.ndarray[np.ndarray]) -> tuple[np.ndarray, np.ndarray, float]:
228
+ n = 3
229
+ m = len(data)
230
+
231
+ #Step 1
232
+ noise_variance = 0.1
233
+ sigma2 = noise_variance ** 2
234
+ rotini = data.T
235
+ rotini2 = rotini ** 2
236
+
237
+ T = np.zeros((5,n,m))
238
+ T[0,:,:] = 1
239
+ T[1,:,:] = rotini
240
+ T[2,:,:] = rotini2 - sigma2
241
+ T[3,:,:] = rotini2 * rotini - 3 * rotini * sigma2
242
+ T[4,:,:] = rotini2 * rotini2 - 6 * rotini2 * sigma2 + 3 * sigma2 * sigma2
243
+
244
+ #Step 2
245
+ one = np.ones((n+1,))
246
+ i = np.arange(1, n+1)
247
+ i = np.append(i, 0)
248
+
249
+ m1 = np.outer(i.T, one)
250
+ m2 = np.outer(one.T, i)
251
+
252
+ M = np.array([ThreespaceSphereCalibration.vec_s(m1), ThreespaceSphereCalibration.vec_s(m2)])
253
+
254
+ #Step 3
255
+ nb = int((n + 1) * n / 2 + n + 1)
256
+ R = np.zeros((nb,nb,n), dtype=np.int32)
257
+
258
+ for p in range(nb):
259
+ for q in range(p, nb):
260
+ for i in range(1, n+1):
261
+ R[p,q,i-1] = int(M[0,p] == i) + int(M[1,p] == i) + int(M[0,q] == i) + int(M[1,q] == i)
262
+
263
+ #Step 4
264
+ nals = np.zeros((nb, nb))
265
+ for p in range(nb):
266
+ for q in range(p, nb):
267
+ sum = 0
268
+ for l in range(m):
269
+ prod = 1
270
+ for i in range(n):
271
+ prod *= T[R[p,q,i],i,l]
272
+ sum += prod
273
+ nals[p,q] = sum
274
+
275
+ #Step 5
276
+ D2 = [i * (i + 1) / 2 for i in range(1, n+1)]
277
+ D = [d for d in range(1, (n+1) * n // 2 + 1) if not d in D2]
278
+
279
+ #Step 6
280
+ menorah_als = np.zeros((nb, nb))
281
+
282
+ for p in range(nb):
283
+ for q in range(p, nb):
284
+ coeff = 2
285
+ if p + 1 in D and q + 1 in D:
286
+ coeff = 4
287
+ elif not p + 1 in D and not q + 1 in D:
288
+ coeff = 1
289
+ menorah_als[p,q] = coeff * nals[p,q]
290
+
291
+ # Fill the lower triangle with the upper triangle values
292
+ i_lower, j_lower = np.tril_indices(nb, k=-1)
293
+ menorah_als[i_lower, j_lower] = menorah_als[j_lower, i_lower]
294
+
295
+ #Step 7
296
+ eigenmat = menorah_als
297
+
298
+ #It is unclear if this is correct, there are differences in sign and positions
299
+ eigenvalues, eigenvectors = np.linalg.eig(eigenmat)
300
+ eigenvectors = eigenvectors.T
301
+
302
+ #Looks like this section gets the eigen vector with the largest eigen value?
303
+ combinedmatr = [[abs(eigenvalues[i]), eigenvectors[i]] for i in range(len(eigenvalues))]
304
+ combinedmatr.sort(key=lambda a: a[0], reverse=True)
305
+
306
+ bals = combinedmatr[-1][1]
307
+
308
+ #Step 8 : ensure normalized
309
+ bals = bals / np.linalg.norm(bals)
310
+
311
+ #Step 9:
312
+ triangle = n*(n+1)//2
313
+ A = ThreespaceSphereCalibration.inv_vec_s(bals[:triangle])
314
+ b = bals[triangle:nb-1]
315
+ d = bals[-1]
316
+
317
+ return A, b, d
318
+
319
+ @staticmethod
320
+ def make_calibration_params(Q: np.ndarray, u: np.ndarray, k: float, H_m: float) -> tuple[np.ndarray,np.ndarray]:
321
+ pa = np.linalg.inv(Q)
322
+ pb = u.T
323
+ b = np.dot(pa, pb) * 0.5
324
+
325
+ eigenvalues, V = np.linalg.eig(Q)
326
+ D = np.diag(eigenvalues)
327
+
328
+ vu_prod = np.dot(V.T, u.T)
329
+ p1a = np.dot(vu_prod.T, np.linalg.inv(D))
330
+ p1b = np.dot(p1a, vu_prod)
331
+ p1 = p1b - (4 * k)
332
+
333
+ alpha = 4 * (H_m ** 2) / p1
334
+
335
+ aD = np.diag(abs(alpha * eigenvalues) ** 0.5)
336
+
337
+ A = np.dot(np.dot(V, aD), V.T)
338
+
339
+ return A, b
340
+
341
+ @staticmethod
342
+ def vec_s(matrix: np.ndarray):
343
+ rows, cols = np.tril_indices(matrix.shape[0])
344
+ return matrix[rows, cols]
345
+
346
+ @staticmethod
347
+ def inv_vec_s(vec: np.ndarray):
348
+ #Its unclear if this function works as intended. But this is how the suite does it.
349
+ size = int((-1 + (1 + 8 * len(vec)) ** 0.5) / 2)
350
+ matr = np.zeros((size,size))
351
+ base = 0
352
+ for i in range(size):
353
+ for j in range(i):
354
+ matr[i,j] = vec[base + j]
355
+ matr[j,i] = vec[base + j]
356
+ matr[i,i] = vec[base + i]
357
+ base += i + 1
358
+
359
+ return matr
360
+
361
+ @property
362
+ def num_points(self):
363
+ return len(self.samples)
@@ -27,7 +27,7 @@ class ThreespaceBufferInputStream(ThreespaceInputStream):
27
27
  length = self.buffer.index(expected) + len(expected)
28
28
  result = self.buffer[:length]
29
29
  del self.buffer[:length]
30
- raise result
30
+ return result
31
31
 
32
32
  """Allows reading without removing the data from the buffer"""
33
33
  def peek(self, num_bytes) -> bytes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yostlabs
3
- Version: 2025.9.17
3
+ Version: 2025.10.6
4
4
  Summary: Python resources and API for 3Space sensors from Yost Labs Inc.
5
5
  Project-URL: Homepage, https://yostlabs.com/
6
6
  Project-URL: Repository, https://github.com/YostLabs/3SpacePythonPackage/tree/main
@@ -13,6 +13,7 @@ Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Requires-Python: >=3.10
16
+ Requires-Dist: async-timeout
16
17
  Requires-Dist: bleak
17
18
  Requires-Dist: numpy
18
19
  Requires-Dist: pyserial
@@ -4,18 +4,18 @@ yostlabs/communication/base.py,sha256=ahAIQndfo9ifX6Lf2NeEaHpIhRJ_uBv6jv9P7N3Rbh
4
4
  yostlabs/communication/ble.py,sha256=UwbUDEp0lU6CQv-BWmqB3mzEUJrwwRH78eeJUmFO9c4,15498
5
5
  yostlabs/communication/serial.py,sha256=j7SksPhd2mCvcMIGVvPcAAhYOE29K6uGLwZCwD-b21E,5685
6
6
  yostlabs/math/__init__.py,sha256=JFzsPQ4AbsX1AH1brBpn1c_Pa_ItF43__D3mlPvA2a4,34
7
- yostlabs/math/quaternion.py,sha256=vQQmT5T0FXAOrZ27cj007Gb_sfEhs7h0Sz6nYxNc5hQ,6357
7
+ yostlabs/math/quaternion.py,sha256=CUIh4RHUcoSt2RUyPPh7Vw-xJfcUP2Z_mAkA0EHwyn4,6865
8
8
  yostlabs/math/vector.py,sha256=9vfVFSahHa0ZZRZ_SgAU5ucVplt7J-fHQ0s8ymOanj4,2725
9
9
  yostlabs/tss3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  yostlabs/tss3/api.py,sha256=h8K1VsWZutTE-Vu5k2xJbz-OBwKxR-IKnFOJ5qBW0xs,86165
11
11
  yostlabs/tss3/consts.py,sha256=RwhqmKIXRGpRdusss3q17ukCuRS96ZsvBl6y0mjF0b4,2404
12
12
  yostlabs/tss3/eepts.py,sha256=7A7sCyOfDiJgw5Y9pGneg-5YgNvcfKtqeS9FoVWfJO8,9540
13
13
  yostlabs/tss3/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- yostlabs/tss3/utils/calibration.py,sha256=42jCEzfTXoHuPZ4e-30N1ijOhkz9ld4PQnhX6AhTrZE,7069
15
- yostlabs/tss3/utils/parser.py,sha256=QfjjFeeIcnWjVbEjSx2yqOsNuoTK3DjkfT6BOMcQOsg,11346
14
+ yostlabs/tss3/utils/calibration.py,sha256=J7RnY23MUUo4lmfapHavUqrTArFo4PxGKw68sL4DfOM,14070
15
+ yostlabs/tss3/utils/parser.py,sha256=ij_kQpB3EukOq3O8wQTYhkqBP-OneiHMUcsHLuL6m-8,11347
16
16
  yostlabs/tss3/utils/streaming.py,sha256=G2OjSIL9zub0EbkgDGDWaqSXoRY6MJzMD4mazWOCUOA,22419
17
17
  yostlabs/tss3/utils/version.py,sha256=NT2H9l-oIRCYhV_yjf5UjkadoJQ0IN4eLl8y__pyTPc,3001
18
- yostlabs-2025.9.17.dist-info/METADATA,sha256=h1vomT-_6UquhivvLQHwz9SayNn0idxbHiJskrHe-z0,2722
19
- yostlabs-2025.9.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- yostlabs-2025.9.17.dist-info/licenses/LICENSE,sha256=PtF8EXRlVhm1_ve52_3GHixSPwMn0tGajFxF3xKS-j0,1090
21
- yostlabs-2025.9.17.dist-info/RECORD,,
18
+ yostlabs-2025.10.6.dist-info/METADATA,sha256=vChqVP1GJ13Cje3O40RtkMKppBYWz1olZPaHyYxJMiE,2751
19
+ yostlabs-2025.10.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ yostlabs-2025.10.6.dist-info/licenses/LICENSE,sha256=PtF8EXRlVhm1_ve52_3GHixSPwMn0tGajFxF3xKS-j0,1090
21
+ yostlabs-2025.10.6.dist-info/RECORD,,