ialdev-maths 0.1.0__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.
@@ -0,0 +1,496 @@
1
+ __all__ = ['Pose', 'zero3D', 'Vec2d', 'Range', 'Rect', 'in_region', 'TupleXY']
2
+
3
+ import numpy as np
4
+ from typing import Union, Iterable, Optional
5
+ from numbers import Real, Number
6
+ import operator
7
+ import math
8
+ from collections import namedtuple
9
+
10
+ TupleXY = namedtuple('TupleXY', ['x', 'y'])
11
+ Range = namedtuple('Range', ['low', 'high'])
12
+
13
+
14
+ class Vec2d(object):
15
+ """2d vector class, supports vector and scalar operators,
16
+ and also provides a bunch of high level functions
17
+ """
18
+ __slots__ = ['x', 'y']
19
+
20
+ def __init__(self, x_or_pair, y=None):
21
+ if y is None:
22
+ self.x = x_or_pair[0]
23
+ self.y = x_or_pair[1]
24
+ else:
25
+ self.x = x_or_pair
26
+ self.y = y
27
+
28
+ def __len__(self):
29
+ return 2
30
+
31
+ def __getitem__(self, key):
32
+ if key == 0:
33
+ return self.x
34
+ elif key == 1:
35
+ return self.y
36
+ else:
37
+ raise IndexError("Invalid subscript " + str(key) + " to Vec2d")
38
+
39
+ def __setitem__(self, key, value):
40
+ if key == 0:
41
+ self.x = value
42
+ elif key == 1:
43
+ self.y = value
44
+ else:
45
+ raise IndexError("Invalid subscript " + str(key) + " to Vec2d")
46
+
47
+ # String representation (for debugging)
48
+ def __repr__(self):
49
+ return 'Vec2d(%s, %s)' % (self.x, self.y)
50
+
51
+ # Comparison
52
+ def __eq__(self, other):
53
+ if hasattr(other, "__getitem__") and len(other) == 2:
54
+ return self.x == other[0] and self.y == other[1]
55
+ else:
56
+ return False
57
+
58
+ def __ne__(self, other):
59
+ if hasattr(other, "__getitem__") and len(other) == 2:
60
+ return self.x != other[0] or self.y != other[1]
61
+ else:
62
+ return True
63
+
64
+ def __nonzero__(self):
65
+ return bool(self.x or self.y)
66
+
67
+ # Generic operator handlers
68
+ def _o2(self, other, f):
69
+ """Any two-operator operation where the left operand is a Vec2d"""
70
+ if isinstance(other, Number):
71
+ return Vec2d(f(self.x, other), f(self.y, other))
72
+ if isinstance(other, Vec2d):
73
+ return Vec2d(f(self.x, other.x), f(self.y, other.y))
74
+ if hasattr(other, "__getitem__") and hasattr(other, "__len__"):
75
+ return Vec2d(f(self.x, other[0]), f(self.y, other[1]))
76
+ return Vec2d(f(self.x, other), f(self.y, other))
77
+
78
+ def _r_o2(self, other, f):
79
+ "Any two-operator operation where the right operand is a Vec2d"
80
+ if hasattr(other, "__getitem__") and hasattr(other, "__len__"):
81
+ return Vec2d(f(other[0], self.x),
82
+ f(other[1], self.y))
83
+ else:
84
+ return Vec2d(f(other, self.x),
85
+ f(other, self.y))
86
+
87
+ def _io(self, other, f):
88
+ "inplace operator"
89
+ if hasattr(other, "__getitem__"):
90
+ self.x = f(self.x, other[0])
91
+ self.y = f(self.y, other[1])
92
+ else:
93
+ self.x = f(self.x, other)
94
+ self.y = f(self.y, other)
95
+ return self
96
+
97
+ # Addition
98
+ def __add__(self, other):
99
+ if isinstance(other, Vec2d):
100
+ return Vec2d(self.x + other.x, self.y + other.y)
101
+ elif hasattr(other, "__getitem__"):
102
+ return Vec2d(self.x + other[0], self.y + other[1])
103
+ else:
104
+ return Vec2d(self.x + other, self.y + other)
105
+
106
+ __radd__ = __add__
107
+
108
+ def __iadd__(self, other):
109
+ if isinstance(other, Vec2d):
110
+ self.x += other.x
111
+ self.y += other.y
112
+ elif hasattr(other, "__getitem__"):
113
+ self.x += other[0]
114
+ self.y += other[1]
115
+ else:
116
+ self.x += other
117
+ self.y += other
118
+ return self
119
+
120
+ # Subtraction
121
+ def __sub__(self, other):
122
+ if isinstance(other, Vec2d):
123
+ return Vec2d(self.x - other.x, self.y - other.y)
124
+ elif hasattr(other, "__getitem__"):
125
+ return Vec2d(self.x - other[0], self.y - other[1])
126
+ else:
127
+ return Vec2d(self.x - other, self.y - other)
128
+
129
+ def __rsub__(self, other):
130
+ if isinstance(other, Vec2d):
131
+ return Vec2d(other.x - self.x, other.y - self.y)
132
+ if hasattr(other, "__getitem__"):
133
+ return Vec2d(other[0] - self.x, other[1] - self.y)
134
+ else:
135
+ return Vec2d(other - self.x, other - self.y)
136
+
137
+ def __isub__(self, other):
138
+ if isinstance(other, Vec2d):
139
+ self.x -= other.x
140
+ self.y -= other.y
141
+ elif hasattr(other, "__getitem__"):
142
+ self.x -= other[0]
143
+ self.y -= other[1]
144
+ else:
145
+ self.x -= other
146
+ self.y -= other
147
+ return self
148
+
149
+ # Multiplication
150
+ def __mul__(self, other):
151
+ if isinstance(other, Vec2d):
152
+ return Vec2d(self.x * other.x, self.y * other.y)
153
+ if hasattr(other, "__getitem__"):
154
+ return Vec2d(self.x * other[0], self.y * other[1])
155
+ else:
156
+ return Vec2d(self.x * other, self.y * other)
157
+
158
+ __rmul__ = __mul__
159
+
160
+ def __imul__(self, other):
161
+ if isinstance(other, Vec2d):
162
+ self.x *= other.x
163
+ self.y *= other.y
164
+ elif hasattr(other, "__getitem__"):
165
+ self.x *= other[0]
166
+ self.y *= other[1]
167
+ else:
168
+ self.x *= other
169
+ self.y *= other
170
+ return self
171
+
172
+ # Division
173
+ def __div__(self, other):
174
+ return self._o2(other, operator.div)
175
+
176
+ def __rdiv__(self, other):
177
+ return self._r_o2(other, operator.div)
178
+
179
+ def __idiv__(self, other):
180
+ return self._io(other, operator.div)
181
+
182
+ def __floordiv__(self, other):
183
+ return self._o2(other, operator.floordiv)
184
+
185
+ def __rfloordiv__(self, other):
186
+ return self._r_o2(other, operator.floordiv)
187
+
188
+ def __ifloordiv__(self, other):
189
+ return self._io(other, operator.floordiv)
190
+
191
+ def __truediv__(self, other):
192
+ return self._o2(other, operator.truediv)
193
+
194
+ def __rtruediv__(self, other):
195
+ return self._r_o2(other, operator.truediv)
196
+
197
+ def __itruediv__(self, other):
198
+ return self._io(other, operator.floordiv)
199
+
200
+ # Modulo
201
+ def __mod__(self, other):
202
+ return self._o2(other, operator.mod)
203
+
204
+ def __rmod__(self, other):
205
+ return self._r_o2(other, operator.mod)
206
+
207
+ def __divmod__(self, other):
208
+ return self._o2(other, operator.divmod)
209
+
210
+ def __rdivmod__(self, other):
211
+ return self._r_o2(other, operator.divmod)
212
+
213
+ # Exponentation
214
+ def __pow__(self, other):
215
+ return self._o2(other, operator.pow)
216
+
217
+ def __rpow__(self, other):
218
+ return self._r_o2(other, operator.pow)
219
+
220
+ # Bitwise operators
221
+ def __lshift__(self, other):
222
+ return self._o2(other, operator.lshift)
223
+
224
+ def __rlshift__(self, other):
225
+ return self._r_o2(other, operator.lshift)
226
+
227
+ def __rshift__(self, other):
228
+ return self._o2(other, operator.rshift)
229
+
230
+ def __rrshift__(self, other):
231
+ return self._r_o2(other, operator.rshift)
232
+
233
+ def __and__(self, other):
234
+ return self._o2(other, operator.and_)
235
+
236
+ __rand__ = __and__
237
+
238
+ def __or__(self, other):
239
+ return self._o2(other, operator.or_)
240
+
241
+ __ror__ = __or__
242
+
243
+ def __xor__(self, other):
244
+ return self._o2(other, operator.xor)
245
+
246
+ __rxor__ = __xor__
247
+
248
+ # Unary operations
249
+ def __neg__(self):
250
+ return Vec2d(operator.neg(self.x), operator.neg(self.y))
251
+
252
+ def __pos__(self):
253
+ return Vec2d(operator.pos(self.x), operator.pos(self.y))
254
+
255
+ def __abs__(self):
256
+ return Vec2d(abs(self.x), abs(self.y))
257
+
258
+ def __invert__(self):
259
+ return Vec2d(-self.x, -self.y)
260
+
261
+ # vectory functions
262
+ def length_sqr(self):
263
+ return self.x ** 2 + self.y ** 2
264
+
265
+ def __get_length(self):
266
+ return math.sqrt(self.x ** 2 + self.y ** 2)
267
+
268
+ def __set_length(self, value):
269
+ k = value / self.__get_length()
270
+ self.x *= k
271
+ self.y *= k
272
+
273
+ length = property(__get_length, __set_length, None, "gets or sets the magnitude of the vector")
274
+
275
+ def rotate(self, angle_degrees):
276
+ radians = math.radians(angle_degrees)
277
+ cos = math.cos(radians)
278
+ sin = math.sin(radians)
279
+ x = self.x * cos - self.y * sin
280
+ y = self.x * sin + self.y * cos
281
+ self.x = x
282
+ self.y = y
283
+
284
+ def rotated(self, angle_degrees):
285
+ radians = math.radians(angle_degrees)
286
+ cos = math.cos(radians)
287
+ sin = math.sin(radians)
288
+ x = self.x * cos - self.y * sin
289
+ y = self.x * sin + self.y * cos
290
+ return Vec2d(x, y)
291
+
292
+ def __get_angle(self):
293
+ if self.length_sqr() == 0:
294
+ return 0
295
+ return math.degrees(math.atan2(self.y, self.x))
296
+
297
+ def __set_angle(self, angle_degrees):
298
+ self.x = self.length
299
+ self.y = 0
300
+ self.rotate(angle_degrees)
301
+
302
+ angle = property(__get_angle, __set_angle, None, "gets or sets the angle of a vector")
303
+
304
+ def angle_between(self, other):
305
+ cross = self.x * other[1] - self.y * other[0]
306
+ dot = self.x * other[0] + self.y * other[1]
307
+ return math.degrees(math.atan2(cross, dot))
308
+
309
+ def normalized(self):
310
+ length = self.length
311
+ if length != 0:
312
+ return self / length
313
+ return Vec2d(self)
314
+
315
+ def normalize_return_length(self):
316
+ length = self.length
317
+ if length != 0:
318
+ self.x /= length
319
+ self.y /= length
320
+ return length
321
+
322
+ def perpendicular(self):
323
+ return Vec2d(-self.y, self.x)
324
+
325
+ def perpendicular_normal(self):
326
+ length = self.length
327
+ if length != 0:
328
+ return Vec2d(-self.y / length, self.x / length)
329
+ return Vec2d(self)
330
+
331
+ def dot(self, other):
332
+ return float(self.x * other[0] + self.y * other[1])
333
+
334
+ def distance(self, other):
335
+ return math.sqrt((self.x - other[0]) ** 2 + (self.y - other[1]) ** 2)
336
+
337
+ def get_dist_sqrd(self, other):
338
+ return (self.x - other[0]) ** 2 + (self.y - other[1]) ** 2
339
+
340
+ def projection(self, other):
341
+ other_length_sqrd = other[0] * other[0] + other[1] * other[1]
342
+ projected_length_times_other_length = self.dot(other)
343
+ return other * (projected_length_times_other_length / other_length_sqrd)
344
+
345
+ def cross(self, other):
346
+ return self.x * other[1] - self.y * other[0]
347
+
348
+ def interpolate_to(self, other, range):
349
+ return Vec2d(self.x + (other[0] - self.x) * range, self.y + (other[1] - self.y) * range)
350
+
351
+ def convert_to_basis(self, x_vector, y_vector):
352
+ return Vec2d(self.dot(x_vector) / x_vector.length_sqr(), self.dot(y_vector) / y_vector.length_sqr())
353
+
354
+ def __getstate__(self):
355
+ return [self.x, self.y]
356
+
357
+ def __setstate__(self, dict):
358
+ self.x, self.y = dict
359
+
360
+
361
+ class _Meta2D(type):
362
+ @classmethod
363
+ def __prepare__(metacls, name, bases, dtype=float, default=np.nan):
364
+ cls_dict = super().__prepare__(name, bases)
365
+ slots = ['x', 'y']
366
+
367
+ def __init__(self,
368
+ first: Union[dtype, Iterable] = (default, default),
369
+ second: Optional[dtype] = None):
370
+ """
371
+ Create 2D coordinate container of {dtype}
372
+ Args:
373
+ first: x or iterable of len=2 for xy (deafults = ({default}, {default}) )
374
+ second: y (if first is x) or not needed if iterable is passed
375
+ """
376
+ self.x, self.y = ((dtype(v) for v in first) if hasattr(first, '__iter__')
377
+ else (first, second))
378
+
379
+ __init__.__doc__ = __init__.__doc__.format(dtype=dtype,
380
+ default=default) # insert data type infor into the docstring
381
+
382
+ cls_dict.update(
383
+ __slots__=slots,
384
+ __annotations__={m: dtype for m in slots},
385
+ xy=property(lambda self: (self.x, self.y)),
386
+ ij=property(lambda self: (int(self.x), int(self.y))),
387
+ array=property(lambda self: np.ndarray(self.xy)),
388
+ dtype=dtype,
389
+ __init__=__init__,
390
+ __iter__=lambda self: iter((self.x, self.y)),
391
+ __eq__=lambda self, other: ((self.x, self.y) == other),
392
+ __getitem__=lambda self, key: getattr(self, 'xy'[key]),
393
+ __setitem__=lambda self, key, val: setattr(self, 'xy'[key], val)
394
+ )
395
+ return cls_dict
396
+
397
+ def __new__(mcs, name, bases, namespace, **kargs):
398
+ return super().__new__(mcs, name, bases, namespace)
399
+
400
+
401
+ # TODO: 3D geometry classes?
402
+ zero3D = np.zeros(3)
403
+ V3D = np.ndarray
404
+
405
+
406
+ class Pose:
407
+ rot: V3D
408
+ trans: V3D
409
+
410
+ def __init__(self, rot=0, trans=0):
411
+ def prep(v):
412
+ if v == 0:
413
+ return zero3D
414
+ assert len(v) == 3
415
+ return v if isinstance(v, np.ndarray) else np.array(v)
416
+ self.rot, self.trans = (prep(v) for v in (rot, trans))
417
+
418
+ def __repr__(self):
419
+ rot, trans = ('0' if (self.rot == zero3D).all() else '{},{},{}'.format(*v)
420
+ for v in (self.rot, self.trans))
421
+ return f'R:{rot} T:{trans}'
422
+
423
+
424
+ class Rect:
425
+ __slots__ = ['low', 'high']
426
+ low: Vec2d
427
+ high: Vec2d
428
+
429
+ def __init__(self, low: Union[Vec2d, Iterable],
430
+ high: Optional[Union[Vec2d, Iterable]] = None,
431
+ *, dim: Optional[Union[Vec2d, Iterable]] = None):
432
+ """
433
+ Create rectangle from two corner points or from low point and dimension
434
+ :param low: tuple, pix2D
435
+ :param high: tuple, pix2D - optional
436
+ :param dim: tuple, pix2D - if high is not provided
437
+ """
438
+ self.low = Vec2d(low)
439
+ self.high = Vec2d(self.low + dim if high is None else high)
440
+ if self.low.x > self.high.x or self.low.y > self.high.y:
441
+ raise ValueError('Low coordinates may not be larger than the high')
442
+
443
+ def __repr__(self):
444
+ return f'Rect({self.low.x}, {self.low.y}; {self.high.x}, {self.high.y})'
445
+
446
+ def __iter__(self):
447
+ return iter((self.low, self.high))
448
+
449
+ def __eq__(self, other):
450
+ return self.low == other.low and self.high == other.high
451
+
452
+ def extend(self, add: Union[Real, Vec2d]):
453
+ self.low -= add
454
+ self.high += add
455
+
456
+ def move(self, shift: Vec2d):
457
+ self.low += shift
458
+ self.high += shift
459
+
460
+ def __contains__(self, p: Vec2d):
461
+ if isinstance(p, Rect):
462
+ return self.high.x <= p.high.x <= p.low.x <= self.low.x and \
463
+ self.high.y <= p.high.y <= p.low.y <= self.low.y
464
+ elif not isinstance(p, Vec2d):
465
+ p = Vec2d(p)
466
+ return self.low.x <= p.x <= self.high.x and self.low.y <= p.y <= self.high.y
467
+
468
+ @property
469
+ def dim(self):
470
+ return self.high - self.low
471
+
472
+ @property
473
+ def ranges(self) -> TupleXY:
474
+ """ representation as pair of two segments """
475
+ return TupleXY(Range(self.low.x, self.high.x),
476
+ Range(self.low.y, self.high.y))
477
+
478
+ def vertices(self):
479
+ """ Generator of all the vertices from low in clock-wise direction"""
480
+ yield Vec2d(self.low.x, self.low.y)
481
+ yield Vec2d(self.low.x, self.high.y)
482
+ yield Vec2d(self.high.x, self.high.y)
483
+ yield Vec2d(self.high.x, self.low.y)
484
+
485
+
486
+ def in_region(coords, region):
487
+ """
488
+ Checks if D-dim coordinates of N points are inside D-dim region
489
+ :param coords: [D x N] - iterable of D 1-d arrays of length N
490
+ :param region: [D x 2] - iterable of D segments of (low, high) for each dimension
491
+ low <= v < high
492
+ :return: np.bool(N) boolean array of length N indicating if a point inside the region
493
+ """
494
+ from functools import reduce
495
+ by_dimensions = (((x >= low) & (x < high)) for x, (low, high) in zip(coords, region))
496
+ return reduce(np.logical_and, by_dimensions)