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/recall.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Design memory: what earlier runs of the same design space found, so a rerun
|
|
3
|
+
starts at the answer instead of searching for it again.
|
|
4
|
+
|
|
5
|
+
A safety-factor study spends most of its time finding the volume fraction:
|
|
6
|
+
the bell crank optimizes six fractions (0.25, 0.125, 0.0625, 0.094, 0.078,
|
|
7
|
+
0.070) and confirms two picks on a finer grid before it settles, each
|
|
8
|
+
optimization 20-30 s and each confirm about a minute. Agents rerun studies
|
|
9
|
+
with small edits (another target, a stronger material, a heavier load), and
|
|
10
|
+
every rerun used to walk that whole path again.
|
|
11
|
+
|
|
12
|
+
What carries over, and why it is safe:
|
|
13
|
+
|
|
14
|
+
- The optimizer minimizes compliance at a fixed volume fraction. Its design
|
|
15
|
+
does not depend on the load's overall scale or the material's E or
|
|
16
|
+
strength, only on the domain, the loads' directions and relative sizes,
|
|
17
|
+
Poisson's ratio and the code. Stress is linear in load and in nothing else
|
|
18
|
+
here (verification is linear elastic). So for a past outcome on the same
|
|
19
|
+
domain under the same load pattern, its safety factor and deflection under
|
|
20
|
+
the new study are its old ones rescaled by strength, E and load scale, and
|
|
21
|
+
its margin is known before anything runs.
|
|
22
|
+
- With the same model (Poisson's ratio, process, code) a past outcome is
|
|
23
|
+
*replayed*: the stored density field is meshed and verified afresh (the
|
|
24
|
+
part delivered is always verified by today's code) and the optimizer is
|
|
25
|
+
skipped. The fine-grid confirmation of a replayed pick is rescaled from
|
|
26
|
+
the stored one rather than run again.
|
|
27
|
+
- With a different model (another Poisson's ratio, a changed solver), the
|
|
28
|
+
stored margins still say roughly where the answer is, and the nearest
|
|
29
|
+
stored design is the starting design instead of a coarse solve.
|
|
30
|
+
|
|
31
|
+
Off unless asked for: LOADPATH_RECALL=1 (or a folder path) turns it on for
|
|
32
|
+
everything, `loadpath run --recall` for one run. Eval runs leave it off so a
|
|
33
|
+
scorecard measures the optimizer, not the cache. Stored under
|
|
34
|
+
~/.cache/loadpath/designs, one folder per domain; LOADPATH_RECALL_MB caps the
|
|
35
|
+
total (default 1000, oldest domains dropped first).
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import hashlib
|
|
40
|
+
import json
|
|
41
|
+
import os
|
|
42
|
+
import shutil
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
|
|
48
|
+
__all__ = ["Recall", "enabled"]
|
|
49
|
+
|
|
50
|
+
_MAX_ENTRIES = 32 # per domain, newest kept
|
|
51
|
+
# A replayed fraction stands in for a requested one this close to it
|
|
52
|
+
# (relative): the search's own stopping rule accepts a bracket of 10%.
|
|
53
|
+
_SNAP = 0.04
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enabled() -> bool:
|
|
57
|
+
v = os.environ.get("LOADPATH_RECALL", "").strip()
|
|
58
|
+
return bool(v) and v.lower() not in ("0", "false", "off", "no")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _root() -> str:
|
|
62
|
+
v = os.environ.get("LOADPATH_RECALL", "").strip()
|
|
63
|
+
if v and v not in ("1", "true", "on", "yes") and (os.sep in v or v.startswith(".")):
|
|
64
|
+
return os.path.expanduser(v)
|
|
65
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache")
|
|
66
|
+
return os.path.join(base, "loadpath", "designs")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _h(*parts) -> str:
|
|
70
|
+
h = hashlib.sha1()
|
|
71
|
+
for p in parts:
|
|
72
|
+
if isinstance(p, np.ndarray):
|
|
73
|
+
h.update(str((p.dtype.str, p.shape)).encode())
|
|
74
|
+
h.update(np.ascontiguousarray(p).tobytes())
|
|
75
|
+
elif hasattr(p, "tocsr"):
|
|
76
|
+
c = p.tocsr()
|
|
77
|
+
h.update(_h(c.indptr, c.indices, np.round(c.data, 9)).encode())
|
|
78
|
+
else:
|
|
79
|
+
h.update(repr(p).encode())
|
|
80
|
+
h.update(b"|")
|
|
81
|
+
return h.hexdigest()[:20]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_CODE = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# The modules a design, its mesh and its verification come out of (and what
|
|
88
|
+
# they import). A change to any of them retires replays; a change elsewhere
|
|
89
|
+
# (the search in study.py, rendering, the server) leaves them standing.
|
|
90
|
+
_DESIGN_CODE = ("am", "constrained", "domain", "durability", "fasteners", "fem", "manufacture", "materials",
|
|
91
|
+
"mg", "mma", "pressure", "printcheck", "reconstruct", "shapes", "simp", "spec", "thermal",
|
|
92
|
+
"verify", "voxels")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _code_key() -> str:
|
|
96
|
+
global _CODE
|
|
97
|
+
if _CODE is None:
|
|
98
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
99
|
+
h = hashlib.sha1()
|
|
100
|
+
for m in _DESIGN_CODE:
|
|
101
|
+
with open(os.path.join(here, m + ".py"), "rb") as fh:
|
|
102
|
+
h.update(fh.read())
|
|
103
|
+
_CODE = h.hexdigest()[:20]
|
|
104
|
+
return _CODE
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_scale(domain) -> float:
|
|
108
|
+
s = max((float(np.abs(f).max()) for f in domain.loads if f is not None and np.size(f)), default=0.0)
|
|
109
|
+
return s if s > 0 else 1.0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class _Pred:
|
|
114
|
+
entry: dict
|
|
115
|
+
margin: float | None # predicted, under this study
|
|
116
|
+
confirm: float | None # predicted fine-grid margin, when one is stored
|
|
117
|
+
exact: bool # same model: replayable
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Recall:
|
|
121
|
+
"""One study's view of the memory for its domain. Build with `open`."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, folder, keys, study, domain, scale):
|
|
124
|
+
self.folder = folder
|
|
125
|
+
self.keys = keys
|
|
126
|
+
self.study = study
|
|
127
|
+
self.domain = domain
|
|
128
|
+
self.scale = scale
|
|
129
|
+
self.index = []
|
|
130
|
+
self.dirty = False
|
|
131
|
+
self.used = {"replayed": 0, "warm": 0, "confirmsSkipped": 0}
|
|
132
|
+
try:
|
|
133
|
+
with open(os.path.join(folder, "index.json")) as fh:
|
|
134
|
+
self.index = json.load(fh).get("entries", [])
|
|
135
|
+
except (OSError, ValueError):
|
|
136
|
+
self.index = []
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def open(cls, study, domain, radius: float, making: dict) -> "Recall | None":
|
|
140
|
+
if not enabled():
|
|
141
|
+
return None
|
|
142
|
+
d = domain
|
|
143
|
+
mirror = {k: _h(np.asarray(v)) for k, v in sorted(d.mirror.items())}
|
|
144
|
+
geometry = _h(d.dims, round(float(d.pitch), 6), np.round(np.asarray(d.origin, float), 6),
|
|
145
|
+
d.mesh.elem_ijk.astype(np.int32), d.preserve, d.fixed, d.support_nodes, d.load_nodes,
|
|
146
|
+
d.springs, mirror, sorted((k, repr(v)) for k, v in making.items()),
|
|
147
|
+
round(float(radius), 6))
|
|
148
|
+
scale = _load_scale(d)
|
|
149
|
+
loads = _h([np.round(np.asarray(f, float) / scale, 9) for f in d.loads],
|
|
150
|
+
None if d.gravity is None else np.round(np.asarray(d.gravity, float) / scale, 12),
|
|
151
|
+
list(d.case_weights), study.max_iterations)
|
|
152
|
+
proc = study.raw.get("process")
|
|
153
|
+
model = _h(repr(study.elastic(design=True)), repr(study.elastic()),
|
|
154
|
+
json.dumps(proc, sort_keys=True, default=str), _code_key())
|
|
155
|
+
folder = os.path.join(_root(), geometry)
|
|
156
|
+
return cls(folder, {"geometry": geometry, "loads": loads, "model": model}, study, domain, scale)
|
|
157
|
+
|
|
158
|
+
# -- predictions -----------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def _margin(self, sf, defl, e) -> float | None:
|
|
161
|
+
"""`e`'s reading (sf, max deflection) rescaled to this study."""
|
|
162
|
+
st = self.study
|
|
163
|
+
g = st.goal
|
|
164
|
+
mat = st.material
|
|
165
|
+
k = self.scale / e["loadScale"]
|
|
166
|
+
if st.durability is not None and st.durability.constrain:
|
|
167
|
+
return None
|
|
168
|
+
ms = []
|
|
169
|
+
if g.safety_factor is not None:
|
|
170
|
+
if sf is None:
|
|
171
|
+
return None
|
|
172
|
+
ms.append(sf * (mat.strength / e["strength"]) / k / g.safety_factor)
|
|
173
|
+
if g.max_deflection is not None:
|
|
174
|
+
if defl is None:
|
|
175
|
+
return None
|
|
176
|
+
d = defl * k * (e["E"] / mat.E)
|
|
177
|
+
ms.append(g.max_deflection / d if d > 0 else float("inf"))
|
|
178
|
+
return min(ms) if ms else None
|
|
179
|
+
|
|
180
|
+
def predictions(self) -> list[_Pred]:
|
|
181
|
+
out = []
|
|
182
|
+
for e in self.index:
|
|
183
|
+
if e["loads"] != self.keys["loads"]:
|
|
184
|
+
continue
|
|
185
|
+
m = self._margin(e.get("sf"), e.get("defl"), e)
|
|
186
|
+
c = e.get("confirm")
|
|
187
|
+
mc = self._margin(c.get("safetyFactor"), c.get("defl"), e) if c else None
|
|
188
|
+
out.append(_Pred(e, m, mc, e["model"] == self.keys["model"]))
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
def _decisive(self, p: _Pred, ratio: float = 1.0) -> float | None:
|
|
192
|
+
"""The margin a pick is judged by: its confirmation when stored, else
|
|
193
|
+
its analysis-grid margin discounted by `ratio`, the worst confirm /
|
|
194
|
+
analysis ratio seen (the bell crank's 0.083 read 1.03 rescaled and
|
|
195
|
+
0.97 on the fine grid)."""
|
|
196
|
+
if p.margin is None:
|
|
197
|
+
return None
|
|
198
|
+
return min(p.margin, p.confirm) if p.confirm is not None else p.margin * ratio
|
|
199
|
+
|
|
200
|
+
def _confirm_ratio(self, preds) -> float:
|
|
201
|
+
rs = [p.confirm / p.margin for p in preds if p.confirm is not None and p.margin]
|
|
202
|
+
return min(1.0, min(rs)) if rs else 1.0
|
|
203
|
+
|
|
204
|
+
def first_fraction(self, default: float) -> tuple[float, str | None]:
|
|
205
|
+
"""Where a safety-factor search should start: the lightest stored
|
|
206
|
+
fraction predicted to pass, or one step past the heaviest that is
|
|
207
|
+
predicted to fail."""
|
|
208
|
+
ps = self.predictions()
|
|
209
|
+
ratio = self._confirm_ratio(ps)
|
|
210
|
+
preds = [(p.entry["vf"], self._decisive(p, ratio), p.exact) for p in ps]
|
|
211
|
+
preds = [q for q in preds if q[1] is not None]
|
|
212
|
+
if not preds:
|
|
213
|
+
return default, None
|
|
214
|
+
passing = [q for q in preds if q[1] >= 1.0]
|
|
215
|
+
failing = [q for q in preds if q[1] < 1.0]
|
|
216
|
+
if passing:
|
|
217
|
+
vf, m, _ = min(passing, key=lambda q: q[0])
|
|
218
|
+
lo = max((q for q in failing if q[0] < vf), key=lambda q: q[0], default=None)
|
|
219
|
+
if m >= 1.12 and lo is not None:
|
|
220
|
+
# the answer lies between: interpolate log-log (margin ~ vf^a)
|
|
221
|
+
a = np.log(m / lo[1]) / np.log(vf / lo[0]) if vf > lo[0] and m > lo[1] > 0 else 1.0
|
|
222
|
+
guess = vf * (1.05 / m) ** (1.0 / max(a, 0.3))
|
|
223
|
+
return float(self.snap(np.clip(guess, lo[0], vf))), f"between recalled {lo[0]:.3f} and {vf:.3f}"
|
|
224
|
+
if m >= 1.12:
|
|
225
|
+
return float(self.snap(vf * 1.05 / m)), f"below recalled {vf:.3f} (margin {m:.2f})"
|
|
226
|
+
return float(vf), f"recalled {vf:.3f} (margin {m:.2f})"
|
|
227
|
+
vf, m, _ = max(failing, key=lambda q: q[0])
|
|
228
|
+
return float(self.snap(min(vf * 1.05 / max(m, 1e-9), vf * 2.0))), f"above recalled {vf:.3f} (margin {m:.2f})"
|
|
229
|
+
|
|
230
|
+
def _replayable(self):
|
|
231
|
+
return [p.entry for p in self.predictions() if p.exact]
|
|
232
|
+
|
|
233
|
+
def snap(self, vf: float, lo: float | None = None, hi: float | None = None, skip=()) -> float:
|
|
234
|
+
"""A replayable stored fraction near `vf` (within _SNAP), or strictly
|
|
235
|
+
inside (lo, hi) when the search is bisecting a bracket."""
|
|
236
|
+
cands = [e["vf"] for e in self._replayable() if all(abs(e["vf"] - v) > 1e-9 for v in skip)]
|
|
237
|
+
if lo is not None and hi is not None:
|
|
238
|
+
inside = [v for v in cands if lo < v < hi]
|
|
239
|
+
if inside:
|
|
240
|
+
return min(inside, key=lambda v: abs(v - vf))
|
|
241
|
+
near = [v for v in cands if abs(v - vf) <= _SNAP * vf]
|
|
242
|
+
return min(near, key=lambda v: abs(v - vf)) if near else vf
|
|
243
|
+
|
|
244
|
+
def replay(self, vf: float):
|
|
245
|
+
"""A stored outcome at exactly this fraction under the same model:
|
|
246
|
+
(x, xphys, entry), or None."""
|
|
247
|
+
for e in sorted(self._replayable(), key=lambda e: -e["time"]):
|
|
248
|
+
if abs(e["vf"] - vf) <= 1e-9:
|
|
249
|
+
arrays = self._arrays(e)
|
|
250
|
+
if arrays is not None:
|
|
251
|
+
self.used["replayed"] += 1
|
|
252
|
+
return arrays[0], arrays[1], e
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def warm_start(self, vf: float):
|
|
256
|
+
"""The stored design under these loads nearest to `vf` (any model):
|
|
257
|
+
(x, label), or None."""
|
|
258
|
+
cands = [e for e in self.index if e["loads"] == self.keys["loads"]]
|
|
259
|
+
if not cands:
|
|
260
|
+
return None
|
|
261
|
+
e = min(cands, key=lambda e: (abs(np.log(e["vf"] / vf)), -e["time"]))
|
|
262
|
+
arrays = self._arrays(e)
|
|
263
|
+
if arrays is None:
|
|
264
|
+
return None
|
|
265
|
+
self.used["warm"] += 1
|
|
266
|
+
return arrays[0], f"warm from recalled {e['vf']:.3f}"
|
|
267
|
+
|
|
268
|
+
def _rescaled(self, e) -> dict | None:
|
|
269
|
+
"""`e`'s stored fine-grid reading in this study's units, in
|
|
270
|
+
verify_part's shape (the fields study._confirm reads)."""
|
|
271
|
+
c = e.get("confirm")
|
|
272
|
+
if not c or self.study.durability is not None:
|
|
273
|
+
return None
|
|
274
|
+
mat = self.study.material
|
|
275
|
+
k = self.scale / e["loadScale"]
|
|
276
|
+
s = (mat.strength / e["strength"]) / k
|
|
277
|
+
d = k * (e["E"] / mat.E)
|
|
278
|
+
return {"safetyFactor": c["safetyFactor"] * s, "analysisPitch": c["analysisPitch"],
|
|
279
|
+
"analysisCells": c["analysisCells"], "p5Member": c.get("p5Member"),
|
|
280
|
+
"minMember": c.get("minMember"),
|
|
281
|
+
"cases": [{**cs, "peakStress": cs["peakStress"] * k, "deflection": cs["deflection"] * d,
|
|
282
|
+
**({"safetyFactor": cs["safetyFactor"] * s} if "safetyFactor" in cs else {})}
|
|
283
|
+
for cs in c["cases"]]}
|
|
284
|
+
|
|
285
|
+
def confirm_for(self, vf: float) -> dict | None:
|
|
286
|
+
"""The stored fine-grid reading of a replayed outcome, rescaled."""
|
|
287
|
+
for e in self._replayable():
|
|
288
|
+
if abs(e["vf"] - vf) <= 1e-9:
|
|
289
|
+
rep = self._rescaled(e)
|
|
290
|
+
if rep is not None:
|
|
291
|
+
self.used["confirmsSkipped"] += 1
|
|
292
|
+
return rep
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
# -- storing -----------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _arrays(self, e):
|
|
298
|
+
try:
|
|
299
|
+
with np.load(os.path.join(self.folder, e["file"])) as z:
|
|
300
|
+
x, xp = z["x"], z["xphys"]
|
|
301
|
+
except (OSError, KeyError, ValueError):
|
|
302
|
+
return None
|
|
303
|
+
if len(x) != self.domain.n_elem:
|
|
304
|
+
return None
|
|
305
|
+
return x.astype(float), xp.astype(float)
|
|
306
|
+
|
|
307
|
+
def add(self, vf: float, x, xphys, report: dict):
|
|
308
|
+
"""Remember one outcome (replacing a stored one at the same fraction
|
|
309
|
+
under the same loads and model)."""
|
|
310
|
+
if x is None or xphys is None:
|
|
311
|
+
return
|
|
312
|
+
cases = report.get("cases") or []
|
|
313
|
+
st = self.study
|
|
314
|
+
e = {"vf": float(vf), "loads": self.keys["loads"], "model": self.keys["model"],
|
|
315
|
+
"loadScale": self.scale, "strength": st.material.strength, "E": st.material.E,
|
|
316
|
+
"sf": report.get("safetyFactor"),
|
|
317
|
+
"defl": max((c["deflection"] for c in cases), default=None),
|
|
318
|
+
"mass": report.get("mass"), "material": st.material.name, "time": time.time()}
|
|
319
|
+
old = [o for o in self.index if o["loads"] == e["loads"] and o["model"] == e["model"]
|
|
320
|
+
and abs(o["vf"] - e["vf"]) <= 1e-9]
|
|
321
|
+
if old and old[0].get("confirm") and self._rescaled(old[0]) is not None:
|
|
322
|
+
e["confirm"] = _stored_confirm(self._rescaled(old[0]))
|
|
323
|
+
name = _h(e["loads"], e["model"], round(e["vf"], 12)) + ".npz"
|
|
324
|
+
e["file"] = name
|
|
325
|
+
try:
|
|
326
|
+
os.makedirs(self.folder, exist_ok=True)
|
|
327
|
+
np.savez_compressed(os.path.join(self.folder, name), x=np.asarray(x, np.float32),
|
|
328
|
+
xphys=np.asarray(xphys, np.float32))
|
|
329
|
+
except OSError:
|
|
330
|
+
return
|
|
331
|
+
self.index = [o for o in self.index if o not in old] + [e]
|
|
332
|
+
self.dirty = True
|
|
333
|
+
|
|
334
|
+
def confirmed(self, vf: float, rep: dict):
|
|
335
|
+
"""Attach a fine-grid reading (verify_part's report, in this study's
|
|
336
|
+
units) to the outcome stored at `vf` by this run."""
|
|
337
|
+
for e in self.index:
|
|
338
|
+
if e["loads"] == self.keys["loads"] and e["model"] == self.keys["model"] \
|
|
339
|
+
and abs(e["vf"] - vf) <= 1e-9 and e["strength"] == self.study.material.strength \
|
|
340
|
+
and e["E"] == self.study.material.E and e["loadScale"] == self.scale:
|
|
341
|
+
e["confirm"] = _stored_confirm(rep)
|
|
342
|
+
self.dirty = True
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
def save(self):
|
|
346
|
+
if not self.dirty:
|
|
347
|
+
return
|
|
348
|
+
try:
|
|
349
|
+
# merge what other processes stored meanwhile (outcomes run in parallel)
|
|
350
|
+
with open(os.path.join(self.folder, "index.json")) as fh:
|
|
351
|
+
theirs = json.load(fh).get("entries", [])
|
|
352
|
+
except (OSError, ValueError):
|
|
353
|
+
theirs = []
|
|
354
|
+
mine = {e["file"] for e in self.index}
|
|
355
|
+
self.index += [e for e in theirs if e["file"] not in mine]
|
|
356
|
+
keep = sorted(self.index, key=lambda e: -e["time"])[:_MAX_ENTRIES]
|
|
357
|
+
live = {e["file"] for e in keep}
|
|
358
|
+
for e in self.index:
|
|
359
|
+
if e["file"] not in live:
|
|
360
|
+
try:
|
|
361
|
+
os.remove(os.path.join(self.folder, e["file"]))
|
|
362
|
+
except OSError:
|
|
363
|
+
pass
|
|
364
|
+
self.index = keep
|
|
365
|
+
try:
|
|
366
|
+
os.makedirs(self.folder, exist_ok=True)
|
|
367
|
+
tmp = os.path.join(self.folder, f"index.json.{os.getpid()}")
|
|
368
|
+
with open(tmp, "w") as fh:
|
|
369
|
+
json.dump({"entries": keep, "study": self.study.name}, fh)
|
|
370
|
+
os.replace(tmp, os.path.join(self.folder, "index.json"))
|
|
371
|
+
except OSError:
|
|
372
|
+
return
|
|
373
|
+
self.dirty = False
|
|
374
|
+
_prune(os.path.dirname(self.folder), keep=self.folder)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _stored_confirm(rep: dict) -> dict:
|
|
378
|
+
return {"safetyFactor": rep["safetyFactor"],
|
|
379
|
+
"defl": max((c["deflection"] for c in rep["cases"]), default=None),
|
|
380
|
+
"analysisPitch": rep.get("analysisPitch"), "analysisCells": rep.get("analysisCells"),
|
|
381
|
+
"p5Member": rep.get("p5Member"), "minMember": rep.get("minMember"),
|
|
382
|
+
"cases": [{k: c[k] for k in ("name", "peakStress", "peakAt", "deflection", "safetyFactor") if k in c}
|
|
383
|
+
for c in rep["cases"]]}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _prune(root: str, keep: str):
|
|
387
|
+
cap = float(os.environ.get("LOADPATH_RECALL_MB", "1000")) * 1e6
|
|
388
|
+
try:
|
|
389
|
+
dirs = [os.path.join(root, d) for d in os.listdir(root)]
|
|
390
|
+
except OSError:
|
|
391
|
+
return
|
|
392
|
+
sizes = []
|
|
393
|
+
for d in dirs:
|
|
394
|
+
try:
|
|
395
|
+
files = [os.path.join(d, f) for f in os.listdir(d)]
|
|
396
|
+
sizes.append((max(os.path.getmtime(f) for f in files) if files else 0.0,
|
|
397
|
+
sum(os.path.getsize(f) for f in files), d))
|
|
398
|
+
except OSError:
|
|
399
|
+
continue
|
|
400
|
+
total = sum(s for _, s, _ in sizes)
|
|
401
|
+
for _, s, d in sorted(sizes):
|
|
402
|
+
if total <= cap:
|
|
403
|
+
break
|
|
404
|
+
if d != keep:
|
|
405
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
406
|
+
total -= s
|