loadpath 0.1.1__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.
Files changed (220) hide show
  1. loadpath/README.md +294 -0
  2. loadpath/SKILL.md +584 -0
  3. loadpath/__init__.py +17 -0
  4. loadpath/__main__.py +1202 -0
  5. loadpath/am.py +316 -0
  6. loadpath/amgfast.py +326 -0
  7. loadpath/assembly.py +985 -0
  8. loadpath/benchmarks/__init__.py +0 -0
  9. loadpath/benchmarks/beams.py +266 -0
  10. loadpath/benchmarks/cases/ge-bracket.json +380 -0
  11. loadpath/benchmarks/evaluate.py +172 -0
  12. loadpath/benchmarks/mechanisms.py +416 -0
  13. loadpath/benchmarks/problems.py +140 -0
  14. loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
  15. loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
  16. loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
  17. loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
  18. loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
  19. loadpath/benchmarks/reference.py +138 -0
  20. loadpath/benchmarks/run.py +301 -0
  21. loadpath/brep.py +1470 -0
  22. loadpath/bucklingopt.py +300 -0
  23. loadpath/buildsim.py +1490 -0
  24. loadpath/cadimport.py +904 -0
  25. loadpath/calibration.py +885 -0
  26. loadpath/cavities.py +211 -0
  27. loadpath/checks/__init__.py +0 -0
  28. loadpath/checks/dfm.py +354 -0
  29. loadpath/checks/exchanger.py +7 -0
  30. loadpath/checks/fea.py +212 -0
  31. loadpath/checks/fit.py +475 -0
  32. loadpath/checks/networks.py +260 -0
  33. loadpath/checks/opening.py +67 -0
  34. loadpath/checks/sdf_kit.py +7 -0
  35. loadpath/conjugate.py +267 -0
  36. loadpath/constrained.py +329 -0
  37. loadpath/contact.py +691 -0
  38. loadpath/coopt.py +342 -0
  39. loadpath/cores.py +163 -0
  40. loadpath/crease.py +1138 -0
  41. loadpath/critic.py +212 -0
  42. loadpath/crosscheck.py +1107 -0
  43. loadpath/cutcell.py +510 -0
  44. loadpath/data/lattice_buckling.json +439 -0
  45. loadpath/data/lattice_tensors.json +27449 -0
  46. loadpath/data/published_experiments.json +71 -0
  47. loadpath/decimate.py +177 -0
  48. loadpath/delivery.py +190 -0
  49. loadpath/directions.py +262 -0
  50. loadpath/domain.py +630 -0
  51. loadpath/drains.py +615 -0
  52. loadpath/durability.py +413 -0
  53. loadpath/editor.html +3135 -0
  54. loadpath/editor.py +610 -0
  55. loadpath/escape.py +354 -0
  56. loadpath/evals/__init__.py +0 -0
  57. loadpath/evals/baselines/coarse.json +1543 -0
  58. loadpath/evals/baselines/full.json +1497 -0
  59. loadpath/evals/brief_kinds/__init__.py +1 -0
  60. loadpath/evals/brief_kinds/heatx.py +387 -0
  61. loadpath/evals/brief_kinds/lattice.py +571 -0
  62. loadpath/evals/brief_kinds/multimat.py +593 -0
  63. loadpath/evals/briefs/assets/bracket.step +1201 -0
  64. loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
  65. loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
  66. loadpath/evals/briefs/assets/motor-body.stl +0 -0
  67. loadpath/evals/briefs/assets/sketch.png +0 -0
  68. loadpath/evals/briefs/base-adapter-fdm.json +492 -0
  69. loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
  70. loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
  71. loadpath/evals/briefs/camera-boom-mjf.json +339 -0
  72. loadpath/evals/briefs/camera-mount-pla.json +203 -0
  73. loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
  74. loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
  75. loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
  76. loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
  77. loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
  78. loadpath/evals/briefs/forearm-fdm.json +146 -0
  79. loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
  80. loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
  81. loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
  82. loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
  83. loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
  84. loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
  85. loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
  86. loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
  87. loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
  88. loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
  89. loadpath/evals/briefs/lighten-step-bracket.json +220 -0
  90. loadpath/evals/briefs/nema17-mount-stl.json +429 -0
  91. loadpath/evals/briefs/pillow-block-sls.json +220 -0
  92. loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
  93. loadpath/evals/briefs/rod-hook-pla.json +154 -0
  94. loadpath/evals/briefs/sensor-arm-resin.json +150 -0
  95. loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
  96. loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
  97. loadpath/evals/briefs/sketch-clevis.json +229 -0
  98. loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
  99. loadpath/evals/briefs/tool-flange-ti64.json +280 -0
  100. loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
  101. loadpath/evals/briefs/turret-fdm.json +281 -0
  102. loadpath/evals/briefs/upper-arm-sls.json +179 -0
  103. loadpath/evals/briefs/urdf-forearm.json +233 -0
  104. loadpath/evals/briefs/wrist-link-mjf.json +278 -0
  105. loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
  106. loadpath/evals/briefs.py +393 -0
  107. loadpath/evals/cases/arm-link-cast.json +157 -0
  108. loadpath/evals/cases/arm-link-driven.json +171 -0
  109. loadpath/evals/cases/arm-link-sandwich.json +315 -0
  110. loadpath/evals/cases/arm-link.json +111 -0
  111. loadpath/evals/cases/bell-crank.json +116 -0
  112. loadpath/evals/cases/grip-turret.json +479 -0
  113. loadpath/evals/cases/grip-upper-arm.json +253 -0
  114. loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
  115. loadpath/evals/cases/grip-wrist-yoke.json +831 -0
  116. loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
  117. loadpath/evals/cases/mbb-beam.json +61 -0
  118. loadpath/evals/cases/motor-body.stl +0 -0
  119. loadpath/evals/cases/motor-mount.json +265 -0
  120. loadpath/evals/cases/printed-hook.json +77 -0
  121. loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
  122. loadpath/evals/cases/wall-bracket.json +126 -0
  123. loadpath/evals/corpus.py +179 -0
  124. loadpath/evals/pareto.py +90 -0
  125. loadpath/evals/run.py +860 -0
  126. loadpath/examples/arm-link.json +25 -0
  127. loadpath/examples/bumpered-bracket-compression.json +225 -0
  128. loadpath/examples/bumpered-bracket-lattice.json +204 -0
  129. loadpath/examples/bumpered-bracket.json +34 -0
  130. loadpath/examples/gripper-finger-mm.json +185 -0
  131. loadpath/examples/heat-exchanger-counterflow.json +21 -0
  132. loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
  133. loadpath/examples/heat-exchanger-crossflow.json +20 -0
  134. loadpath/examples/pedal-lattice.json +23 -0
  135. loadpath/examples/pressure-plenum.json +12 -0
  136. loadpath/examples/shelf-bracket.json +27 -0
  137. loadpath/exchanger.py +831 -0
  138. loadpath/fasteners.py +199 -0
  139. loadpath/fem.py +668 -0
  140. loadpath/femops.py +70 -0
  141. loadpath/fieldjit.py +981 -0
  142. loadpath/fieldops.py +119 -0
  143. loadpath/fields.py +3697 -0
  144. loadpath/fieldtorch.py +464 -0
  145. loadpath/flow.py +984 -0
  146. loadpath/flowtopo.py +300 -0
  147. loadpath/gpu.py +521 -0
  148. loadpath/heatx.py +1067 -0
  149. loadpath/heatx_thermal.py +432 -0
  150. loadpath/homogenized.py +886 -0
  151. loadpath/ibrep.py +552 -0
  152. loadpath/impact.py +693 -0
  153. loadpath/infill.py +376 -0
  154. loadpath/interface_library.py +340 -0
  155. loadpath/interlock.py +186 -0
  156. loadpath/isolator.py +149 -0
  157. loadpath/knn.py +204 -0
  158. loadpath/lattice.py +1065 -0
  159. loadpath/lattice3mf.py +760 -0
  160. loadpath/lattice_buckling.py +245 -0
  161. loadpath/lattice_grade.py +325 -0
  162. loadpath/lattice_orient.py +595 -0
  163. loadpath/lattice_print.py +159 -0
  164. loadpath/lattice_tensors.py +558 -0
  165. loadpath/latticestep.py +1210 -0
  166. loadpath/manufacture.py +446 -0
  167. loadpath/material_library.py +489 -0
  168. loadpath/materials.py +144 -0
  169. loadpath/mcp_server.py +786 -0
  170. loadpath/mechanism.py +218 -0
  171. loadpath/mechanism_nl.py +811 -0
  172. loadpath/meshops.py +452 -0
  173. loadpath/mg.py +853 -0
  174. loadpath/mma.py +214 -0
  175. loadpath/modal.py +210 -0
  176. loadpath/multimat.py +1788 -0
  177. loadpath/narrowband.py +418 -0
  178. loadpath/nlkernel.py +241 -0
  179. loadpath/nonlinear.py +1095 -0
  180. loadpath/orient.py +334 -0
  181. loadpath/porous.py +994 -0
  182. loadpath/pressure.py +171 -0
  183. loadpath/preview.py +335 -0
  184. loadpath/printcheck.py +263 -0
  185. loadpath/printsetup.py +207 -0
  186. loadpath/processcheck.py +376 -0
  187. loadpath/quick.py +113 -0
  188. loadpath/recall.py +406 -0
  189. loadpath/reconstruct.py +797 -0
  190. loadpath/render.py +436 -0
  191. loadpath/resolved.py +189 -0
  192. loadpath/schema.py +1701 -0
  193. loadpath/sdf_kit.py +843 -0
  194. loadpath/selfx.py +292 -0
  195. loadpath/shapes.py +584 -0
  196. loadpath/sharp.py +717 -0
  197. loadpath/simp.py +1170 -0
  198. loadpath/sizing.py +312 -0
  199. loadpath/slicer.py +321 -0
  200. loadpath/spec.py +1072 -0
  201. loadpath/step.py +806 -0
  202. loadpath/struts.py +639 -0
  203. loadpath/study.py +1937 -0
  204. loadpath/tasks.py +822 -0
  205. loadpath/thermal.py +1691 -0
  206. loadpath/threemf.py +487 -0
  207. loadpath/tune.py +437 -0
  208. loadpath/validation.py +145 -0
  209. loadpath/verify.py +1215 -0
  210. loadpath/viewer.html +694 -0
  211. loadpath/viewer.py +446 -0
  212. loadpath/viscoelastic.py +106 -0
  213. loadpath/voxelprint.py +582 -0
  214. loadpath/voxels.py +402 -0
  215. loadpath-0.1.1.dist-info/METADATA +319 -0
  216. loadpath-0.1.1.dist-info/RECORD +220 -0
  217. loadpath-0.1.1.dist-info/WHEEL +5 -0
  218. loadpath-0.1.1.dist-info/entry_points.txt +2 -0
  219. loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
  220. loadpath-0.1.1.dist-info/top_level.txt +1 -0
loadpath/isolator.py ADDED
@@ -0,0 +1,149 @@
1
+ """
2
+ Lattice vibration isolators: elastomer (TPU) lattice feet under a payload,
3
+ tuned to a natural frequency, levelled under an off-centre load, and
4
+ checked against shock.
5
+
6
+ An isolator is a spring chosen soft enough that the payload's mounted
7
+ frequency f_n sits well below the excitation: above sqrt(2) f_n the base
8
+ motion is attenuated. Four identical feet under a payload whose centre of
9
+ gravity is off the middle of the footprint do three things wrong:
10
+
11
+ - the payload sits tilted (the feet near the CG squash more);
12
+ - bounce and rocking couple, so a vertical shake also rocks the payload and
13
+ the mount resonates at several frequencies instead of one, pushing the
14
+ start of isolation up;
15
+ - the most loaded foot reaches the end of its linear range first, so the
16
+ mount takes less shock before it starts to buckle.
17
+
18
+ The classic fix is the "elastic centre" rule: give each foot a stiffness
19
+ so that the stiffness-weighted centre of the feet sits under the CG
20
+ (sum k_i = K, sum k_i x_i = K x_cg). Then gravity squashes every foot by
21
+ the same amount (level), vertical bounce is a pure translation, and every
22
+ foot carries load in proportion to its stiffness. With a lattice the
23
+ stiffness of each foot is a density (k = E C rho^n A / H), so a graded
24
+ set of feet does this at no cost in parts or footprint.
25
+
26
+ - `foot_stiffness`, `density_for_stiffness`: homogenized axial and lateral
27
+ stiffness of a block foot.
28
+ - `rigid_body`: mass, CG and inertia of a set of point masses.
29
+ - `mount_modes`: the six rigid-body modes of a payload on springs.
30
+ - `tune_mount`: per-foot stiffness for a target bounce frequency with the
31
+ elastic centre under the CG (bounded by the printable density range).
32
+ - `shock_margin`: the acceleration (in g, on top of gravity) at which the
33
+ first foot reaches its elastic buckling stress.
34
+
35
+ Voxel FE of the full mount (modal_analysis, base_transmissibility) checks
36
+ all of it: see examples/isolator/mount.py and docs/IMPACT.md.
37
+
38
+ Units: mm, N, MPa, kg; stiffness N/mm.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import numpy as np
43
+
44
+ from .lattice import SHEETS, sheet_modulus
45
+
46
+ __all__ = ["foot_stiffness", "density_for_stiffness", "rigid_body", "mount_modes", "tune_mount",
47
+ "shock_margin", "G"]
48
+
49
+ G = 9810.0 # mm/s^2
50
+
51
+
52
+ def foot_stiffness(kind: str, rho, E: float, width: float, height: float, nu_eff: float = 0.3):
53
+ """Axial and lateral stiffness (N/mm) of a square `width` x `width` foot
54
+ `height` tall, homogenized lattice of `kind` at density rho, glued at
55
+ both ends. Lateral: shear and guided bending in series."""
56
+ e = E * sheet_modulus(kind, rho)
57
+ a = width * width
58
+ kz = e * a / height
59
+ g = e / (2 * (1 + nu_eff))
60
+ k_shear = g * a / height
61
+ k_bend = 12 * e * (width ** 4 / 12) / height ** 3
62
+ return kz, 1.0 / (1.0 / k_shear + 1.0 / k_bend)
63
+
64
+
65
+ def density_for_stiffness(kind: str, k, E: float, width: float, height: float):
66
+ """Density giving axial stiffness k (N/mm); inverse of foot_stiffness."""
67
+ s = SHEETS[kind]
68
+ rel = np.asarray(k, float) * height / (E * width * width)
69
+ return (rel / s["C"]) ** (1.0 / s["n"])
70
+
71
+
72
+ def rigid_body(points, masses):
73
+ """Total mass (kg), CG (mm) and inertia tensor about the CG (kg mm^2)
74
+ of point masses."""
75
+ p = np.asarray(points, float).reshape(-1, 3)
76
+ m = np.asarray(masses, float).ravel()
77
+ tot = float(m.sum())
78
+ cg = (p * m[:, None]).sum(axis=0) / tot
79
+ r = p - cg
80
+ inertia = (m[:, None, None] * ((r * r).sum(axis=1)[:, None, None] * np.eye(3)[None]
81
+ - r[:, :, None] * r[:, None, :])).sum(axis=0)
82
+ return tot, cg, inertia
83
+
84
+
85
+ def _skew(r):
86
+ return np.array([[0, -r[2], r[1]], [r[2], 0, -r[0]], [-r[1], r[0], 0]])
87
+
88
+
89
+ def mount_modes(mass: float, cg, inertia, feet, kz, ks):
90
+ """Rigid payload on spring feet. feet: (n, 3) top-centre of each foot
91
+ (where it meets the payload); kz, ks: axial and lateral stiffness per
92
+ foot (N/mm). Returns frequencies (Hz, ascending), mode vectors
93
+ (6, 6: translation mm, rotation rad, about the CG) and each mode's
94
+ effective mass fraction per translation axis."""
95
+ from scipy.linalg import eigh
96
+
97
+ feet = np.asarray(feet, float)
98
+ kz = np.broadcast_to(np.asarray(kz, float), (len(feet),))
99
+ ks = np.broadcast_to(np.asarray(ks, float), (len(feet),))
100
+ # SI inside: translations in m, rotations in rad, stiffness N/m, inertia kg m^2
101
+ kmat = np.zeros((6, 6))
102
+ for x, a, b in zip(feet, kz, ks):
103
+ bm = np.hstack([np.eye(3), -_skew((x - np.asarray(cg, float)) * 1e-3)]) # u = t + theta x r
104
+ kmat += bm.T @ (np.diag([b, b, a]) * 1e3) @ bm
105
+ mmat = np.zeros((6, 6))
106
+ mmat[:3, :3] = np.eye(3) * mass
107
+ mmat[3:, 3:] = np.asarray(inertia, float) * 1e-6
108
+ w2, v = eigh(kmat, mmat)
109
+ gam = v[:3, :].T * mass # phi^T M r for unit translations (v is M-normalized)
110
+ return np.sqrt(np.maximum(w2, 0)) / (2 * np.pi), v, gam ** 2 / mass
111
+
112
+
113
+ def tune_mount(cg_xy, feet_xy, k_total: float, k_min=None, k_max=None):
114
+ """Per-foot axial stiffness with sum k = k_total and the stiffness
115
+ centre under the CG (sum k x = k_total cg), closest to equal feet (least
116
+ squares), within [k_min, k_max] per foot. Returns (k, residual): the
117
+ residual is the CG offset (mm) left over when the bounds bind."""
118
+ from scipy.optimize import lsq_linear
119
+
120
+ p = np.asarray(feet_xy, float)[:, :2]
121
+ n = len(p)
122
+ a = np.vstack([np.ones(n), p[:, 0], p[:, 1]])
123
+ b = k_total * np.array([1.0, cg_xy[0], cg_xy[1]])
124
+ k0 = np.full(n, k_total / n)
125
+ # the constraints, weighted heavily, plus a small pull toward equal feet
126
+ w = 1e3
127
+ span = max(np.ptp(p[:, 0]), np.ptp(p[:, 1]), 1.0)
128
+ aa = np.vstack([w * a / np.array([[1.0], [span], [span]]), np.eye(n)])
129
+ bb = np.concatenate([w * b / np.array([1.0, span, span]), k0])
130
+ lo = np.full(n, 0.0 if k_min is None else k_min)
131
+ hi = np.full(n, np.inf if k_max is None else k_max)
132
+ k = lsq_linear(aa, bb, bounds=(lo, hi)).x
133
+ centre = (k[:, None] * p).sum(axis=0) / k.sum()
134
+ return k, float(np.hypot(*(centre - np.asarray(cg_xy, float)[:2])))
135
+
136
+
137
+ def shock_margin(kind: str, rho, E: float, width: float, load, creep: float = 1.0, rate: float = 1.0):
138
+ """Acceleration (in g, on top of the 1 g static load) at which the most
139
+ loaded foot buckles. load: static force per foot (N). Buckling of thin
140
+ elastic walls is set by strain, so the margin is a strain budget: the
141
+ buckling strain (0.05 E rho^2 / E_foot, the elastic crush law in
142
+ impact.py) less the static strain, which grows by `creep`
143
+ (viscoelastic.creep_factor) over the years under load, with the shock's
144
+ extra strain taken at the stiffer modulus of a fast event (`rate`,
145
+ viscoelastic.event_factor of the pulse length)."""
146
+ from .impact import plateau_stress
147
+
148
+ fb = plateau_stress(f"{kind}:elastic", np.asarray(rho, float), E) * width * width
149
+ return float(((fb / np.asarray(load, float)) - creep).min() * rate)
loadpath/knn.py ADDED
@@ -0,0 +1,204 @@
1
+ """
2
+ k nearest neighbours, compiled: the same neighbours, in the same order, as
3
+ scipy's cKDTree.query(P, k), several times faster on the queries a mesher
4
+ makes.
5
+
6
+ MeshField reads a mesh's exact distance from the triangles behind the k=12
7
+ nearest centroids of each point; on the GRIP links (500k triangles, a 10 mm
8
+ band) that query was ~80% of evaluating the whole field graph. This is a
9
+ k-d tree (median splits on the widest axis, 16 points a leaf) searched
10
+ depth first, nearer child first, in numba (nogil: peval's threads share
11
+ it). What makes it fast is the warm start: the points a mesher asks about
12
+ come in order (a block of grid samples, a mesh's vertices), so the previous
13
+ point's k neighbours, measured from this one, bound its k-th distance
14
+ before the search starts, and the search only opens the leaves inside that
15
+ ball.
16
+
17
+ The answer is the exact k nearest by squared distance, nearest first;
18
+ centroids at exactly equal distances come in index order here and in the
19
+ tree's own order in scipy, the only way the two can differ (none did over
20
+ millions of GRIP and arm-link queries, tests/test_fieldjit.py).
21
+
22
+ `available()` is False without numba or with LOADPATH_KNN=kdtree.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+
28
+ import numpy as np
29
+
30
+ try:
31
+ import numba as nb
32
+
33
+ from . import meshops as _meshops # noqa: F401 - caps numba's threads
34
+ except ImportError: # pragma: no cover
35
+ nb = None
36
+
37
+ __all__ = ["available", "KDTree"]
38
+
39
+ _LEAF = 16
40
+
41
+
42
+ def available():
43
+ return nb is not None and os.environ.get("LOADPATH_KNN", "").lower() != "kdtree"
44
+
45
+
46
+ if nb is not None:
47
+
48
+ @nb.njit(cache=True, nogil=True)
49
+ def _build(pts, leaf):
50
+ n = pts.shape[0]
51
+ idx = np.arange(n)
52
+ cap = 4 * (n // leaf + 1) + 8 # leaves hold leaf/2 .. leaf points
53
+ start = np.empty(cap, np.int64)
54
+ end = np.empty(cap, np.int64)
55
+ left = np.full(cap, -1, np.int64)
56
+ lo = np.empty((cap, 3))
57
+ hi = np.empty((cap, 3))
58
+ stack = np.empty(cap, np.int64)
59
+ start[0], end[0] = 0, n
60
+ nn = 1
61
+ sp = 0
62
+ stack[sp] = 0
63
+ sp += 1
64
+ while sp > 0:
65
+ sp -= 1
66
+ nd = stack[sp]
67
+ s, e = start[nd], end[nd]
68
+ for d in range(3):
69
+ a = np.inf
70
+ b = -np.inf
71
+ for t in range(s, e):
72
+ v = pts[idx[t], d]
73
+ if v < a:
74
+ a = v
75
+ if v > b:
76
+ b = v
77
+ lo[nd, d] = a
78
+ hi[nd, d] = b
79
+ if e - s <= leaf:
80
+ continue
81
+ dim = 0
82
+ w = hi[nd, 0] - lo[nd, 0]
83
+ for d in range(1, 3):
84
+ if hi[nd, d] - lo[nd, d] > w:
85
+ w = hi[nd, d] - lo[nd, d]
86
+ dim = d
87
+ sub = idx[s:e].copy()
88
+ key = np.empty(e - s)
89
+ for t in range(e - s):
90
+ key[t] = pts[sub[t], dim]
91
+ order = np.argsort(key, kind="mergesort")
92
+ for t in range(e - s):
93
+ idx[s + t] = sub[order[t]]
94
+ mid = (s + e) // 2
95
+ left[nd] = nn
96
+ start[nn], end[nn] = s, mid
97
+ start[nn + 1], end[nn + 1] = mid, e
98
+ stack[sp] = nn
99
+ stack[sp + 1] = nn + 1
100
+ sp += 2
101
+ nn += 2
102
+ return idx, start[:nn].copy(), end[:nn].copy(), left[:nn].copy(), lo[:nn].copy(), hi[:nn].copy()
103
+
104
+ @nb.njit(cache=True, nogil=True, inline="always")
105
+ def _box2(px, py, pz, lo, hi, nd):
106
+ # squared distance from the point to the node's box
107
+ d = 0.0
108
+ for a in range(3):
109
+ v = px if a == 0 else (py if a == 1 else pz)
110
+ if v < lo[nd, a]:
111
+ u = lo[nd, a] - v
112
+ d += u * u
113
+ elif v > hi[nd, a]:
114
+ u = v - hi[nd, a]
115
+ d += u * u
116
+ return d
117
+
118
+ @nb.njit(cache=True, nogil=True)
119
+ def _query(Q, pts, spts, idx, start, end, left, lo, hi, k, out_i):
120
+ bestd = np.empty(k)
121
+ besti = np.empty(k, np.int64)
122
+ stack = np.empty(256, np.int64)
123
+ for i in range(Q.shape[0]):
124
+ px, py, pz = Q[i, 0], Q[i, 1], Q[i, 2]
125
+ bound = np.inf
126
+ if i > 0:
127
+ # the previous point's k neighbours: k points within the largest
128
+ # of their distances from this one, so its k-th nearest is too
129
+ bound = 0.0
130
+ for j in range(k):
131
+ p = out_i[i - 1, j]
132
+ dx = px - pts[p, 0]
133
+ dy = py - pts[p, 1]
134
+ dz = pz - pts[p, 2]
135
+ d = dx * dx + dy * dy + dz * dz
136
+ if d > bound:
137
+ bound = d
138
+ bound = bound * (1.0 + 1e-12) + 1e-300
139
+ nb_ = 0
140
+ sp = 0
141
+ stack[sp] = 0
142
+ sp += 1
143
+ while sp > 0:
144
+ sp -= 1
145
+ nd = stack[sp]
146
+ lim = bestd[k - 1] if nb_ == k else bound
147
+ if _box2(px, py, pz, lo, hi, nd) > lim * (1.0 + 1e-12):
148
+ continue
149
+ c = left[nd]
150
+ if c < 0:
151
+ for t in range(start[nd], end[nd]):
152
+ p = idx[t]
153
+ dx = px - spts[t, 0] # the points in tree order: a leaf is contiguous
154
+ dy = py - spts[t, 1]
155
+ dz = pz - spts[t, 2]
156
+ d = dx * dx + dy * dy + dz * dz
157
+ if nb_ < k:
158
+ if d > bound:
159
+ continue
160
+ j = nb_
161
+ nb_ += 1
162
+ elif d < bestd[k - 1] or (d == bestd[k - 1] and p < besti[k - 1]):
163
+ j = k - 1
164
+ else:
165
+ continue
166
+ # insert: by distance, then index (a fixed order for ties)
167
+ while j > 0 and (bestd[j - 1] > d or (bestd[j - 1] == d and besti[j - 1] > p)):
168
+ bestd[j] = bestd[j - 1]
169
+ besti[j] = besti[j - 1]
170
+ j -= 1
171
+ bestd[j] = d
172
+ besti[j] = p
173
+ continue
174
+ # the nearer child on top of the stack
175
+ d0 = _box2(px, py, pz, lo, hi, c)
176
+ d1 = _box2(px, py, pz, lo, hi, c + 1)
177
+ if d0 <= d1:
178
+ stack[sp] = c + 1
179
+ stack[sp + 1] = c
180
+ else:
181
+ stack[sp] = c
182
+ stack[sp + 1] = c + 1
183
+ sp += 2
184
+ for j in range(k):
185
+ out_i[i, j] = besti[j]
186
+
187
+
188
+ class KDTree:
189
+ """cKDTree(pts).query(P, k)[1], compiled (see the module docstring)."""
190
+
191
+ def __init__(self, pts, leaf=_LEAF):
192
+ self.pts = np.ascontiguousarray(pts, np.float64)
193
+ if len(self.pts) == 0:
194
+ raise ValueError("KDTree needs points")
195
+ self.idx, self.start, self.end, self.left, self.lo, self.hi = _build(self.pts, int(leaf))
196
+ self.spts = np.ascontiguousarray(self.pts[self.idx])
197
+
198
+ def query(self, P, k):
199
+ P = np.ascontiguousarray(np.asarray(P, np.float64).reshape(-1, 3))
200
+ k = min(int(k), len(self.pts))
201
+ out = np.empty((len(P), k), np.int64)
202
+ if len(P):
203
+ _query(P, self.pts, self.spts, self.idx, self.start, self.end, self.left, self.lo, self.hi, k, out)
204
+ return out