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.
- iad/maths/__init__.py +0 -0
- iad/maths/geom/__init__.py +1 -0
- iad/maths/geom/plane.py +656 -0
- iad/maths/geom/shapes.py +496 -0
- iad/maths/hist.py +1458 -0
- iad/maths/regress.py +336 -0
- ialdev_maths-0.1.0.dist-info/METADATA +19 -0
- ialdev_maths-0.1.0.dist-info/RECORD +9 -0
- ialdev_maths-0.1.0.dist-info/WHEEL +4 -0
iad/maths/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .shapes import *
|
iad/maths/geom/plane.py
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from typing import Collection, Literal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
VT = list | tuple | np.ndarray
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def vec_len(v, axis=1):
|
|
10
|
+
"""Return length of vectors with coordinates along the given axis"""
|
|
11
|
+
v = np.asarray(v)
|
|
12
|
+
if v.ndim == 1:
|
|
13
|
+
axis = 0
|
|
14
|
+
return np.sum(v ** 2, axis=axis) ** .5
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot_vector(vec, org=(0, 0, 0), *, labels=None, ax=None, **kws):
|
|
18
|
+
"""
|
|
19
|
+
Plot one or more 3D vectors
|
|
20
|
+
|
|
21
|
+
:param vec: 1+ of triplets (coordinates)
|
|
22
|
+
:param org: 1+ (of same len as vec) of triplets - origins of the vectors
|
|
23
|
+
:param ax: axis to draw in
|
|
24
|
+
:param kws: Line3DCollection arguments
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def prep(x):
|
|
28
|
+
x = np.asarray(x)
|
|
29
|
+
x = x[None, :] if x.ndim == 1 else x
|
|
30
|
+
assert x.ndim == 2 and x.shape[1] == 3
|
|
31
|
+
return x
|
|
32
|
+
|
|
33
|
+
org, vec = map(prep, [org, vec])
|
|
34
|
+
assert org.shape[0] in (1, vec.shape[0])
|
|
35
|
+
|
|
36
|
+
if ax is None:
|
|
37
|
+
from matplotlib.pyplot import gca
|
|
38
|
+
|
|
39
|
+
ax = gca()
|
|
40
|
+
|
|
41
|
+
hs = ax.quiver3D(*org.T, *vec.T, arrow_length_ratio=0.1, **kws)
|
|
42
|
+
|
|
43
|
+
if labels:
|
|
44
|
+
assert len(labels) == len(vec)
|
|
45
|
+
for s, xyz in zip(labels, org + vec):
|
|
46
|
+
ax.text(*xyz, s)
|
|
47
|
+
return hs
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def plane_fit(points: Collection[VT], normalized=True):
|
|
51
|
+
"""
|
|
52
|
+
SVD based pain fit given points and return array plane equation parameters
|
|
53
|
+
`ax + by + cz + d = 0` (in normalized form |a,b,c| = 1 if requested)
|
|
54
|
+
|
|
55
|
+
:param points: collections of 3-vectors
|
|
56
|
+
:param normalized: if `True` ensures that the *normal* part is a unit vector
|
|
57
|
+
:return: array of 4 plane equation parameters: [a,b,c,d]
|
|
58
|
+
"""
|
|
59
|
+
points = np.column_stack([points, np.ones(points.shape[0])]) # type: ignore
|
|
60
|
+
# SVD best-fitting plane for the test points after subtracting the centroid
|
|
61
|
+
U, S, Vt = np.linalg.svd(points, full_matrices=False) # type: ignore
|
|
62
|
+
pn = Vt[np.argmin(S), :]
|
|
63
|
+
return pn / (pn[:3] ** 2).sum() ** 0.5 if normalized else pn
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
AxisIDT = Literal[0, 1, 2, 3, 'x', 'y', 'z']
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AxisID:
|
|
70
|
+
|
|
71
|
+
def __init__(self, ax: 'AxisID' | AxisIDT, *, base: Literal[0, 1] = None,
|
|
72
|
+
axes: Collection[str] | str = None):
|
|
73
|
+
"""
|
|
74
|
+
Axis identificator can be initialized either by its name or axis (0 or 1 based) index
|
|
75
|
+
- default `base=0`
|
|
76
|
+
- default `axes` names: `(x, y, z)`
|
|
77
|
+
|
|
78
|
+
Also, may be initialized by another `AxisID` instance with optional reset of `base`.
|
|
79
|
+
|
|
80
|
+
:param ax: name | idx | axis_id
|
|
81
|
+
:param base: 0 | 1
|
|
82
|
+
:param axes: collection of names to change from `xyz`
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def valid_base(b):
|
|
86
|
+
if b not in (0, 1):
|
|
87
|
+
raise ValueError("Axis enumeration base must be 0 or 1")
|
|
88
|
+
return b
|
|
89
|
+
|
|
90
|
+
# ------ copy constructor ------
|
|
91
|
+
if isinstance(ax, AxisID):
|
|
92
|
+
if axes is not None:
|
|
93
|
+
raise SyntaxError("Argument `axes` not allowed when initialized by `AxisID`")
|
|
94
|
+
self.axes = ax.axes
|
|
95
|
+
if base is None:
|
|
96
|
+
self.id = ax.id
|
|
97
|
+
self.base = ax.base
|
|
98
|
+
else:
|
|
99
|
+
self.base = valid_base(base)
|
|
100
|
+
self.id = ax.id - ax.base + self.base
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
self.base = 0 if base is None else valid_base(base) # default 0
|
|
104
|
+
self.axes = tuple(_.lower() for _ in (axes or 'xyz')) # default 'xyz'
|
|
105
|
+
if len(self.axes) > len(set(self.axes)):
|
|
106
|
+
raise ValueError(f"Repeating axis names: {self.axes}")
|
|
107
|
+
|
|
108
|
+
if isinstance(ax, str):
|
|
109
|
+
self.name = ax.lower()
|
|
110
|
+
self.id = self.axes.index(self.name) + self.base
|
|
111
|
+
else:
|
|
112
|
+
self.name = self.axes[ax - self.base]
|
|
113
|
+
self.id = ax
|
|
114
|
+
|
|
115
|
+
def __repr__(self):
|
|
116
|
+
return f"Axis('{self.name}'({self.id})"
|
|
117
|
+
|
|
118
|
+
def __int__(self):
|
|
119
|
+
return self.id
|
|
120
|
+
|
|
121
|
+
def __str__(self):
|
|
122
|
+
return self.name
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def id0(self):
|
|
126
|
+
"""Zero based index of the axis"""
|
|
127
|
+
return self.id - self.base
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def id1(self):
|
|
131
|
+
"""One based index of the axis"""
|
|
132
|
+
return self.id - self.base + 1
|
|
133
|
+
|
|
134
|
+
def __eq__(self, other: 'AxisID' | AxisIDT):
|
|
135
|
+
if not isinstance(other, AxisID):
|
|
136
|
+
other = AxisID(other)
|
|
137
|
+
return other.name == self.name
|
|
138
|
+
|
|
139
|
+
def __ieq__(self, other: 'AxisID' | AxisIDT):
|
|
140
|
+
return self.__eq__(other)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def vector(self):
|
|
144
|
+
"""Base vector along the axis"""
|
|
145
|
+
base = np.zeros(len(self.axes))
|
|
146
|
+
base[self.id0] = 1
|
|
147
|
+
return base
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def basis_vectors(cls, axes='xyz'):
|
|
151
|
+
"""Return basis vectors as rows of in 2d array"""
|
|
152
|
+
return np.eye(len(axes))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Plane:
|
|
156
|
+
"""
|
|
157
|
+
Plane can be constructed from different initial types of information:
|
|
158
|
+
- parameters of general equation (ax + by + cz + d = 0)
|
|
159
|
+
- a point and normal at this point
|
|
160
|
+
- 3 points, if not on the same line
|
|
161
|
+
- N > 3 points, from fitting, keep std errors of estimated parameters
|
|
162
|
+
|
|
163
|
+
Independently of how it was created, plane internally is represented by its
|
|
164
|
+
*Hessian Normal Form* (https://mathworld.wolfram.com/HessianNormalForm.html).
|
|
165
|
+
|
|
166
|
+
Plane provides a convenience access to some standard attributes:
|
|
167
|
+
- normal - normalized vector orthogonal to the plane
|
|
168
|
+
- closest - shortest vector from the coordinates origin to the plane
|
|
169
|
+
- distance - distance from the coordinates origin to plane (len(position))
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, *abcd, stderr=None) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Initialize plane with general equation parameters and optionally their std errors
|
|
175
|
+
ax + by + cz + d = 0
|
|
176
|
+
|
|
177
|
+
:param abcd: parameters of the plane
|
|
178
|
+
:param stderr: optional std errors for those parameters
|
|
179
|
+
"""
|
|
180
|
+
abcd = np.asarray(abcd[0] if len(abcd) == 1 else abcd, dtype=float).flatten()
|
|
181
|
+
if abcd.size != 4:
|
|
182
|
+
raise ValueError("plane must be defined by 4 parameters")
|
|
183
|
+
|
|
184
|
+
n = abcd[:3]
|
|
185
|
+
n_len = (n @ n) ** .5
|
|
186
|
+
if np.isclose(n_len, 0):
|
|
187
|
+
raise ValueError(f"Normal length may not be 0!")
|
|
188
|
+
self._n = n / n_len
|
|
189
|
+
self._p = abcd[-1] / n_len
|
|
190
|
+
|
|
191
|
+
if stderr is not None:
|
|
192
|
+
if len(stderr) != 4: raise ValueError(f"Invalid {len(stderr)=} != 4!")
|
|
193
|
+
stderr = np.asarray(stderr, dtype=float)
|
|
194
|
+
self._stderr = stderr
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def abcd(self):
|
|
198
|
+
"""Parameters (a,b,c,d) in form of plane equation ax + by + cz + d == 0"""
|
|
199
|
+
return namedtuple('PlaneParams', ['a', 'b', 'c', 'd'])(*self._n, self._p)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def normal(self):
|
|
203
|
+
"""Normal of the plane (normalized)"""
|
|
204
|
+
return self._n
|
|
205
|
+
|
|
206
|
+
def distance(self, points: Collection[VT]):
|
|
207
|
+
"""Return array of signed distances FROM the given points TO the plane
|
|
208
|
+
(negative if point on the opposite side relative to the normal)
|
|
209
|
+
|
|
210
|
+
:param points: collection of points vectors, or a single vector
|
|
211
|
+
:return: array of distances, for a single point - a single number
|
|
212
|
+
"""
|
|
213
|
+
points = np.asarray(points)
|
|
214
|
+
assert points.ndim == 1 and points.size == 3 or points.ndim == 2 and points.shape[1] == 3
|
|
215
|
+
return points @ self._n + self._p
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def closest(self):
|
|
219
|
+
"""Shortest vector from the coordinates' origin to the plane"""
|
|
220
|
+
return -self._n * self._p
|
|
221
|
+
|
|
222
|
+
def closest_to(self, point):
|
|
223
|
+
"""Find point on the plane closest to the given `point`"""
|
|
224
|
+
|
|
225
|
+
# point on plane is defined as:
|
|
226
|
+
# q' = q + (d - q.n)n
|
|
227
|
+
# where:
|
|
228
|
+
# q' is the point on the plane
|
|
229
|
+
# q is the point we are checking
|
|
230
|
+
# d is the value of normal dot position
|
|
231
|
+
# n is the plane normal
|
|
232
|
+
return point - self.normal * self.distance(point)
|
|
233
|
+
|
|
234
|
+
def intersect_axes(self, *, vector=False) -> tuple[float] | tuple[np.ndarray]:
|
|
235
|
+
"""Return intersection points with each xyz axes.
|
|
236
|
+
|
|
237
|
+
If `vector` - in form of their 3d coordinates, otherwise as distances from the origin:
|
|
238
|
+
|
|
239
|
+
:param vector: if True rerun vectors to the intersection points
|
|
240
|
+
:return: 3-tuple of intersection coordinates for every axes
|
|
241
|
+
"""
|
|
242
|
+
if self._p:
|
|
243
|
+
cs = tuple(np.nan if np.isclose(c, 0) else -self._p / c for c in self._n)
|
|
244
|
+
if vector:
|
|
245
|
+
return tuple(c * b.vector for c, b in zip(cs, map(AxisID, 'xyz'))) # type: ignore
|
|
246
|
+
return cs
|
|
247
|
+
return tuple([np.zeros(3) if vector else 0.] * 3) # origin
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def kxy_params(self):
|
|
251
|
+
"""Parameters (kx, ky, b) of the form z = (kx * x + ky * y + b)"""
|
|
252
|
+
a, b, c, d = self.abcd
|
|
253
|
+
return namedtuple('PlaneXYParams', ['kx', 'ky', 'b'])(-a / c, -b / c, -d / c)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def xy_coef(self):
|
|
257
|
+
"""Linear model coefficients array: `y = <xy_coef> * <x> + xy_intercept`"""
|
|
258
|
+
return -np.array(self.normal[:2]) / self.normal[2] # - <a,b> / c
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def xy_intercept(self):
|
|
262
|
+
"""Linear model intercept: `y = <coef> * <x> + xy_intercept`"""
|
|
263
|
+
return -self._p / self._n[2] # -d/c
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def xy_params(self):
|
|
267
|
+
"""Linear model params array `[p0..p3]` (`y = p0 + p1 x1 + p2 x2`)"""
|
|
268
|
+
return -np.array([self._p, *self._n[:2]]) / self._n[2] # -[d,a,b]/c
|
|
269
|
+
|
|
270
|
+
def z(self, xy):
|
|
271
|
+
"""
|
|
272
|
+
Calculate `z` for given coordinates (x,y) or collection of coordinates.
|
|
273
|
+
"""
|
|
274
|
+
return np.asarray(xy) @ self.xy_coef + self.xy_intercept
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_point_normal(cls, point, normal):
|
|
278
|
+
point, normal = (np.asarray(_, dtype=float) for _ in [point, normal])
|
|
279
|
+
length = (normal ** 2).sum() ** 0.5
|
|
280
|
+
if np.isclose(length, 0):
|
|
281
|
+
raise ValueError("Plane's normal must be non-zero vector")
|
|
282
|
+
normal = normal / length
|
|
283
|
+
distance = -normal.dot(point)
|
|
284
|
+
return cls([normal, distance])
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def from_points(cls, points: Collection[VT], *, robust=True, **kws):
|
|
288
|
+
"""
|
|
289
|
+
Create plane from 3 or more points.
|
|
290
|
+
|
|
291
|
+
For >3 points supports robust regression using ransac.
|
|
292
|
+
|
|
293
|
+
:param points: N × 3
|
|
294
|
+
:param robust: allow robust regression removing outliers
|
|
295
|
+
:param kws: robust RANSAC arguments
|
|
296
|
+
:return:
|
|
297
|
+
"""
|
|
298
|
+
points = np.asarray(points, dtype=float)
|
|
299
|
+
assert points.ndim == 2 and points.shape[1] == 3
|
|
300
|
+
from .. import regress as reg
|
|
301
|
+
|
|
302
|
+
if (N := len(points)) < 3:
|
|
303
|
+
raise ValueError(f"at least 3 points are required to define a plane (given {N})")
|
|
304
|
+
|
|
305
|
+
if np.linalg.matrix_rank(points) < 3:
|
|
306
|
+
raise ValueError("Plane can't be passed through the given {N} points")
|
|
307
|
+
|
|
308
|
+
if N == 3:
|
|
309
|
+
return cls(plane_fit(points))
|
|
310
|
+
else:
|
|
311
|
+
X = np.c_[points, np.ones(N)]
|
|
312
|
+
params, stderr, _, _ = reg.robust_linear_regression(
|
|
313
|
+
X, np.zeros(N), robust=robust, fit_intercept=False, **kws)
|
|
314
|
+
return cls(params, stderr=stderr)
|
|
315
|
+
|
|
316
|
+
def __repr__(self):
|
|
317
|
+
if self._stderr:
|
|
318
|
+
_str = lambda v: f"{0:.3g}±{1:.3g}".format(*v)
|
|
319
|
+
params = zip(self.abcd, self._stderr)
|
|
320
|
+
else:
|
|
321
|
+
_str = lambda v: f"{v:.3g}"
|
|
322
|
+
params = self.abcd
|
|
323
|
+
|
|
324
|
+
params = list(map(_str, params))
|
|
325
|
+
params = ", ".join(params[:3]) + f'; {params[3]}'
|
|
326
|
+
return f"Plane({params})"
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def is_axes_plane(self):
|
|
330
|
+
"""Check if plane is one of the axes planes: 'xy', 'yz' 'zx'"""
|
|
331
|
+
return np.isclose(self._p, 0) and np.isclose(self._n[abs(np.argmax(self._n))], 1)
|
|
332
|
+
|
|
333
|
+
def belong(self, points: VT | Collection[VT], atol=None, **kws):
|
|
334
|
+
"""For given collection of points check if each of them belong to the plane within given tolerance
|
|
335
|
+
:param points: point(s) to check
|
|
336
|
+
:param atol: `isclose`(,..., atol=tol)`
|
|
337
|
+
"""
|
|
338
|
+
distances = self.distance(points)
|
|
339
|
+
if atol is not None:
|
|
340
|
+
kws[atol] = atol
|
|
341
|
+
return np.isclose(distances, 0, **kws) # type: ignore
|
|
342
|
+
|
|
343
|
+
def project(self, vec):
|
|
344
|
+
"""
|
|
345
|
+
Project vector onto the plane, that it subtract its component
|
|
346
|
+
perpendicular to the plane.
|
|
347
|
+
:param vec:
|
|
348
|
+
:return: vector parallel to the plane
|
|
349
|
+
"""
|
|
350
|
+
vec = np.atleast_2d(vec)
|
|
351
|
+
return (vec - (self._n[:, None] @ self._n.dot(vec.T)[None, :]).T).squeeze()
|
|
352
|
+
|
|
353
|
+
def intersect_line(self, point, direction: AxisIDT | AxisID | np.ndarray):
|
|
354
|
+
"""
|
|
355
|
+
Find where line(s) defined by a point(s) (origin) and direction vector
|
|
356
|
+
intersects the plane.
|
|
357
|
+
|
|
358
|
+
Return vector of `np.nan` if they don't intersect
|
|
359
|
+
|
|
360
|
+
Basic case requires both `point` and `direction` to be 3d vectors.
|
|
361
|
+
Supported also shortcuts, when
|
|
362
|
+
- `point=0` stands for [0,0,0],
|
|
363
|
+
- `direction` can be axis vector from 'xyz` or 0,1,2 or `AxisID`
|
|
364
|
+
|
|
365
|
+
Can be used to find intersections of multiple lines.
|
|
366
|
+
|
|
367
|
+
Then in the basic form `point` and `direction` are sequences of N 3d vectors.
|
|
368
|
+
If either of them is a single 3d vector, other is broadcasted accordingly.
|
|
369
|
+
|
|
370
|
+
:param point: a vector or 0 for (0,0,0)
|
|
371
|
+
:param direction: a vector or axis (name or zero-based index)
|
|
372
|
+
"""
|
|
373
|
+
if isinstance(direction, (str, int)):
|
|
374
|
+
direction = AxisID(direction)
|
|
375
|
+
if isinstance(direction, AxisID):
|
|
376
|
+
direction = direction.vector
|
|
377
|
+
|
|
378
|
+
if np.isscalar(point) and point == 0:
|
|
379
|
+
point = np.zeros(3)
|
|
380
|
+
|
|
381
|
+
# print(f"{point.shape=}, {direction.shape=}")
|
|
382
|
+
point, direction = np.broadcast_arrays(np.atleast_2d(point), direction)
|
|
383
|
+
assert point.ndim == 2, point.shape[1] == 3
|
|
384
|
+
|
|
385
|
+
# --- main part
|
|
386
|
+
p, n = self.closest, self.normal
|
|
387
|
+
pd = p.dot(n)
|
|
388
|
+
|
|
389
|
+
rd_n = n.dot(direction.T)
|
|
390
|
+
# if np.isclose(rd_n, 0.0):
|
|
391
|
+
# return np.full((3,), np.nan)
|
|
392
|
+
#
|
|
393
|
+
p0_n = n.dot(point.T)
|
|
394
|
+
t = np.atleast_2d((pd - p0_n) / rd_n).T
|
|
395
|
+
|
|
396
|
+
# print(f"{(point + (direction * t)).shape=}")
|
|
397
|
+
return (point + (direction * t)).squeeze()
|
|
398
|
+
|
|
399
|
+
def _plotting_patch(self, xlim=None, ylim=None, zlim=None):
|
|
400
|
+
"""Calculate 3d-coordinates of rectangular patch representing plane in the plot.
|
|
401
|
+
Return the patch and list of points of intersection with axis (could be 1,2,3)
|
|
402
|
+
"""
|
|
403
|
+
def ort(_ax):
|
|
404
|
+
return np.roll(np.array([0, 1, 1], bool), _ax) # mask for other axes
|
|
405
|
+
|
|
406
|
+
def to_base_vec(v, _ax):
|
|
407
|
+
return np.roll(np.array([v, 0, 0], dtype=float), _ax)
|
|
408
|
+
|
|
409
|
+
def rect_around_segment(s1, s2, offset1, offset2=None):
|
|
410
|
+
"""Build 3D rectangle from a segment (two 3D points) by offsetting it
|
|
411
|
+
into 2 directions: along and inversed to the offsets"""
|
|
412
|
+
if offset2 is None:
|
|
413
|
+
offset2 = -offset1
|
|
414
|
+
return np.array([s1 + offset1, s2 + offset1, s2 + offset2, s1 + offset2])
|
|
415
|
+
|
|
416
|
+
basis = AxisID.basis_vectors()
|
|
417
|
+
|
|
418
|
+
# special case - not generic version!
|
|
419
|
+
if xlim and ylim and self.normal @ basis[2] > 0.01:
|
|
420
|
+
inters = [to_base_vec(_, d) for d, lims in enumerate([xlim, ylim]) for _ in lims]
|
|
421
|
+
inters = rect_around_segment(*inters)
|
|
422
|
+
patch = inters = self.intersect_line(inters, basis[2])
|
|
423
|
+
else:
|
|
424
|
+
# First find intersections points with every axis
|
|
425
|
+
inters = np.asarray(self.intersect_axes(vector=True)) # axes x points
|
|
426
|
+
inters_mask = np.all(~np.isnan(inters), axis=1) # binary mask of axis with intersections
|
|
427
|
+
inters = inters[inters_mask]
|
|
428
|
+
|
|
429
|
+
sel_axis = np.argmax(abs(self.normal))
|
|
430
|
+
inters_num = len(inters)
|
|
431
|
+
if inters_num == 1 or inters_num == 3 and self.is_axes_plane:
|
|
432
|
+
b1, b2 = basis[ort(sel_axis)] # 2 basis vectors orthogonal to max_axis
|
|
433
|
+
patch = self.intersect_line(rect_around_segment(-b1, b1, b2), basis[sel_axis])
|
|
434
|
+
elif inters_num == 2:
|
|
435
|
+
patch = rect_around_segment(*inters, *basis[~inters_mask])
|
|
436
|
+
elif inters_num == 3:
|
|
437
|
+
basis2 = (basis if self.belong([0, 0, 0]) else inters)[ort(sel_axis)]
|
|
438
|
+
(p1, l1), (p2, l2) = sorted(zip(basis2, map(vec_len, basis2)), key=lambda _: _[1])
|
|
439
|
+
# complete rectangle from two points p1, p2 laying on the axes
|
|
440
|
+
p4 = -p2 * (l1 / l2)
|
|
441
|
+
p3 = p2 + p4 - p1
|
|
442
|
+
patch = self.intersect_line((p1, p2, p3, p4), basis[sel_axis])
|
|
443
|
+
else:
|
|
444
|
+
raise RuntimeError
|
|
445
|
+
|
|
446
|
+
return patch, inters
|
|
447
|
+
|
|
448
|
+
def plot(self, *, ax=None, color="lightblue", alpha=0.3,
|
|
449
|
+
aspect='equal', origin=True, normal=True, **opts):
|
|
450
|
+
"""
|
|
451
|
+
Create figure and plot 3D the plane and optionally additional points
|
|
452
|
+
|
|
453
|
+
Axis set options, like `xlim` can be provided as kw arguments.
|
|
454
|
+
|
|
455
|
+
:param ax: if provided, draw on this axis
|
|
456
|
+
:param color: color of the plane
|
|
457
|
+
:param alpha: transparency (0,1)
|
|
458
|
+
:param aspect: argument for `ax.set_aspect()`
|
|
459
|
+
:param opts: options to set: ax.set(**opts)
|
|
460
|
+
"""
|
|
461
|
+
from matplotlib import colors, pyplot as plt
|
|
462
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
463
|
+
|
|
464
|
+
lim_ops_names = {'xlim', 'ylim', 'zlim'}
|
|
465
|
+
lim_ops = {k: opts.get(k, None) for k in lim_ops_names}
|
|
466
|
+
|
|
467
|
+
if ax is None:
|
|
468
|
+
ax = plt.figure().add_subplot(projection="3d")
|
|
469
|
+
ax.set_title(str(self))
|
|
470
|
+
ax.set(xlabel="x", ylabel="y")
|
|
471
|
+
|
|
472
|
+
patch, inters = self._plotting_patch(**lim_ops)
|
|
473
|
+
ax.add_collection3d(Poly3DCollection(
|
|
474
|
+
[patch], alpha=alpha, shade=True, facecolors=[color],
|
|
475
|
+
edgecolors=0.8 * colors.to_rgba_array(color, alpha=alpha),
|
|
476
|
+
)) # type: ignore
|
|
477
|
+
ax.plot(*inters.T, '.:', color=color)
|
|
478
|
+
|
|
479
|
+
if normal:
|
|
480
|
+
plot_vector(self.normal, self.closest, color='m', lw=1)
|
|
481
|
+
if origin:
|
|
482
|
+
ax.scatter(0, 0, 0, color='red')
|
|
483
|
+
|
|
484
|
+
# if aspect == 'equal': # make sure the minimal axis limit is not too small
|
|
485
|
+
|
|
486
|
+
if not lim_ops_names.intersection(opts):
|
|
487
|
+
limits = np.asarray(ax.axis()).reshape(3, 2)
|
|
488
|
+
spans = np.diff(limits).squeeze()
|
|
489
|
+
if spans.min() / spans.max() < 0.2:
|
|
490
|
+
i_min = np.argmin(spans)
|
|
491
|
+
limits[i_min] *= (spans.max() / spans.min()) / 4
|
|
492
|
+
ax.axis(limits.flatten())
|
|
493
|
+
# ax.set_adjustable('box')
|
|
494
|
+
ax.set(aspect=aspect, **opts)
|
|
495
|
+
return ax
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def random_points_between_vectors(v1, v2, num: int, sigma: float = 0):
|
|
499
|
+
"""
|
|
500
|
+
Generate `num` random points in the parallelogram region of the plane
|
|
501
|
+
(A, B, C, D) where `D` = `B` + (`A` - `B`) + (`C` - `B`).
|
|
502
|
+
|
|
503
|
+
Points in ABC not on the plane, are replaced by the closest on the plane.
|
|
504
|
+
|
|
505
|
+
:param ABC: list of 3 points in the plane.
|
|
506
|
+
:param num: number of points to generate
|
|
507
|
+
:param sigma: allow normal deviation from the plane with given `sigma`
|
|
508
|
+
"""
|
|
509
|
+
v1, v2 = map(np.asarray, (v1, v2))
|
|
510
|
+
points = np.random.rand(num, 2) @ [v1, v2]
|
|
511
|
+
if not sigma:
|
|
512
|
+
return points
|
|
513
|
+
n = np.cross(v1, v2) # normal direction
|
|
514
|
+
n = (n / vec_len(n))[None, :]
|
|
515
|
+
return points + n * sigma * np.random.randn(num, 1)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
from matplotlib import pyplot as plt, use
|
|
520
|
+
|
|
521
|
+
use("QTAgg")
|
|
522
|
+
|
|
523
|
+
ex, ey, ez = (AxisID(_).vector for _ in 'xyz')
|
|
524
|
+
|
|
525
|
+
pn = Plane([0.1, 0.2, .8, -10])
|
|
526
|
+
|
|
527
|
+
v0 = pn.intersect_line(-(ex+ey)/2, ez)
|
|
528
|
+
v1, v2 = pn.project([ex, ey])
|
|
529
|
+
|
|
530
|
+
points = v0 + random_points_between_vectors(v1, v2, 125, .1)
|
|
531
|
+
# points = pn.intersect_line(points, ez)
|
|
532
|
+
|
|
533
|
+
ax = pn.plot(xlim=(-2,2), ylim=(-2,2))
|
|
534
|
+
ax.scatter(*points.T)
|
|
535
|
+
|
|
536
|
+
pnf = Plane.from_points(points)
|
|
537
|
+
pnf.plot(color='yellow', ax=ax)
|
|
538
|
+
|
|
539
|
+
# ax.scatter(*np.asarray(v0, v1, ))
|
|
540
|
+
|
|
541
|
+
ax.figure.canvas.manager.window.move(100, 100)
|
|
542
|
+
ax.figure.set_size_inches(8, 8)
|
|
543
|
+
|
|
544
|
+
# Plane([1, 3, 4, 0]).plot()
|
|
545
|
+
# Plane([0, 0, 1, 0]).plot()
|
|
546
|
+
# Plane([1, 3, 4, 6]).plot()
|
|
547
|
+
# Plane([0, 1, 0, 0]).plot()
|
|
548
|
+
|
|
549
|
+
π = np.pi
|
|
550
|
+
rnd = np.random
|
|
551
|
+
|
|
552
|
+
# p = Plane([1, 1, 1], [1, 1, 1])
|
|
553
|
+
# print(f"{p = }\n{p.closest = }\n{p.normal = }\n{p.distance = }")
|
|
554
|
+
# p.belong(p.closest)
|
|
555
|
+
# ax = p.plot()
|
|
556
|
+
# ax.scatter(*p.closest[:, None])
|
|
557
|
+
# # %%
|
|
558
|
+
# print(p)
|
|
559
|
+
# p.plot(points=True).scatter(*p.points.T, c="g")
|
|
560
|
+
# # %%
|
|
561
|
+
# p.belong([0, -1, -1])
|
|
562
|
+
plt.show()
|
|
563
|
+
# %%
|
|
564
|
+
# plt.close('all')
|
|
565
|
+
|
|
566
|
+
# %%
|
|
567
|
+
# A, B, C = i, j, k
|
|
568
|
+
# normal = ((B - A) ^ (C - A)).normalized
|
|
569
|
+
#
|
|
570
|
+
# # pyrr.plane.create_from_position(B, normal)
|
|
571
|
+
# pyrr.plane.create(normal, np.sum(normal * B))
|
|
572
|
+
# # %%
|
|
573
|
+
# pyrr.plane.normal(pyrr.plane.create_from_points(i, j, k))
|
|
574
|
+
# # %%
|
|
575
|
+
# pp = PlanePoints(1 * i, 2 * j, 3 * k)
|
|
576
|
+
# pp.gen_region_points(60, 0.1)
|
|
577
|
+
# _ax = pp.plot()
|
|
578
|
+
# # %%
|
|
579
|
+
# # SVD best-fitting plane for the test points after subtracting the centroid
|
|
580
|
+
# points = pp.points
|
|
581
|
+
# I = np.ones(len(points))
|
|
582
|
+
# U, S, Vt = np.linalg.svd(np.column_stack([points, I]))
|
|
583
|
+
# nv = Vt[np.argmin(S), :]
|
|
584
|
+
#
|
|
585
|
+
# pn = pyrr.plane.create(nv[:-1], nv[-1])
|
|
586
|
+
# pn
|
|
587
|
+
# # %%
|
|
588
|
+
# pp.base_points
|
|
589
|
+
# # %%
|
|
590
|
+
# plane.create_from_points(i, j, k)
|
|
591
|
+
# # %%
|
|
592
|
+
# plane.position(pn)
|
|
593
|
+
# # %%
|
|
594
|
+
# pn0 = plane.create_from_points(*pp.base_points)
|
|
595
|
+
# plane.position(pn0)
|
|
596
|
+
# # %%
|
|
597
|
+
# pp.base_points[0]
|
|
598
|
+
# # %%
|
|
599
|
+
# ABC_ = [
|
|
600
|
+
# pyrr.geometric_tests.point_closest_point_on_plane(p, pn) for p in pp.base_points
|
|
601
|
+
# ]
|
|
602
|
+
# ABC_
|
|
603
|
+
#
|
|
604
|
+
# plot_vector(ABC_, color="g", ax=_ax)
|
|
605
|
+
# # %%
|
|
606
|
+
# import pandas as pd
|
|
607
|
+
# from sklearn.model_selection import train_test_split
|
|
608
|
+
#
|
|
609
|
+
# data_url = "http://lib.stat.cmu.edu/datasets/boston"
|
|
610
|
+
# raw_df = pd.read_csv(data_url, sep="\s+", skiprows=22, header=None)
|
|
611
|
+
# data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
|
|
612
|
+
# target = raw_df.values[1::2, 2]
|
|
613
|
+
# A, b = data, target
|
|
614
|
+
# # %%
|
|
615
|
+
# # append a columns of 1s (these are the biases)
|
|
616
|
+
# A = np.column_stack([np.ones(A.shape[0]), A])
|
|
617
|
+
#
|
|
618
|
+
# # split the data into train and test sets
|
|
619
|
+
# X_train, X_test, y_train, y_test = train_test_split(
|
|
620
|
+
# A, b, test_size=0.50, random_state=42
|
|
621
|
+
# )
|
|
622
|
+
#
|
|
623
|
+
# # calculate the economy SVD for the data matrix A
|
|
624
|
+
# U, S, Vt = np.linalg.svd(X_train, full_matrices=False)
|
|
625
|
+
#
|
|
626
|
+
# # solve Ax = b for the best possible approximate solution in terms of least squares
|
|
627
|
+
# x_hat = Vt.T @ np.linalg.inv(np.diag(S)) @ U.T @ y_train
|
|
628
|
+
#
|
|
629
|
+
# # perform train and test inference
|
|
630
|
+
# train_predictions = X_train @ x_hat
|
|
631
|
+
# test_predictions = X_test @ x_hat
|
|
632
|
+
#
|
|
633
|
+
# # compute train and test MSE
|
|
634
|
+
# train_mse = np.mean((train_predictions - y_train) ** 2)
|
|
635
|
+
# test_mse = np.mean((test_predictions - y_test) ** 2)
|
|
636
|
+
#
|
|
637
|
+
# print("Train Mean Squared Error:", train_mse)
|
|
638
|
+
# print("Test Mean Squared Error:", test_mse)
|
|
639
|
+
# # %%
|
|
640
|
+
# (Vt.T @ np.linalg.inv(np.diag(S)) @ U.T @ y_train).shape
|
|
641
|
+
# # %%
|
|
642
|
+
# Vt.shape, S.shape, U.shape, y_train.shape, x_hat.shape
|
|
643
|
+
# # %%
|
|
644
|
+
# (U[:, -1] @ A.normalized)
|
|
645
|
+
# # %%
|
|
646
|
+
# (1 / 3) ** 0.5
|
|
647
|
+
# # %%
|
|
648
|
+
# 3 * U[:, -1] ** 2
|
|
649
|
+
# # %%
|
|
650
|
+
# U[:, -1] @ ((C - A) ^ (B - A))
|
|
651
|
+
# # %%
|
|
652
|
+
# # plot raw data
|
|
653
|
+
# plt.figure()
|
|
654
|
+
# ax = plt.subplot(111, projection="3d")
|
|
655
|
+
# xs, ys, zs = points
|
|
656
|
+
# ax.scatter(xs, ys, zs, color="b")
|