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 ADDED
File without changes
@@ -0,0 +1 @@
1
+ from .shapes import *
@@ -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")