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.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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
|