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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"about": "Published compression tests of printed lattices with fully specified geometry, used to check loadpath's lattice models against measurements (bench/published_experiments.py, docs/VALIDATION.md). Values are copied from the papers' tables; mean +- standard deviation where the paper gives one. Each specimen is a cube of lattice with cut-open side faces, compressed between flat platens along the build direction.",
|
|
3
|
+
"sources": {
|
|
4
|
+
"shevchenko2023": {
|
|
5
|
+
"citation": "Shevchenko V, Balabanov S, Sychov M, Karimova L. Prediction of Cellular Structure Mechanical Properties with the Geometry of Triply Periodic Minimal Surfaces (TPMS). ACS Omega 8(30):26895, 2023.",
|
|
6
|
+
"doi": "10.1021/acsomega.3c01631",
|
|
7
|
+
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10398688/",
|
|
8
|
+
"tables": "Table 1 (solid compressive E and strength of each printed material, same lab), Tables 3-5 (lattices)",
|
|
9
|
+
"notes": "30 mm cubes, 3x3x3 sheet cells (10 mm), 0.8 mm wall; SLS PA2200 (EOS Formiga P110, 0.1 mm layers), FDM PLA (Anet A6, 0.2 mm layers), LCD urethane-acrylate resin (Anycubic Photon S, 0.05 mm). ISO 604, 5 mm/min, along the build axis; modulus from the stress-strain curve (crosshead strain, no extensometer). Solid modulus and strength measured in compression by the same lab, so the material card is the paper's own. Strength is the first peak (sigma_B). Lattice phase at the faces not stated."
|
|
10
|
+
},
|
|
11
|
+
"guerrasilva2021": {
|
|
12
|
+
"citation": "Guerra Silva R, Salinas Estay C, Morales Pavez G, Zahr Vinuela J, Torres MJ. Influence of Geometric and Manufacturing Parameters on the Compressive Behavior of 3D Printed Polymer Lattice Structures. Materials 14(6):1462, 2021.",
|
|
13
|
+
"doi": "10.3390/ma14061462",
|
|
14
|
+
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC8002545/",
|
|
15
|
+
"tables": "Table 2 (feedstock properties, from datasheets), Tables 3 and 5 (configurations, measured modulus and plateau stress)",
|
|
16
|
+
"notes": "35 mm cubes, FDM (UP mini 2, 0.4 mm nozzle, raft, no supports) in ABS and PC; truncated-octahedron (Kelvin) and cubic diamond strut cells, 5 or 7 mm cells, 1.0 or 1.5 mm round struts, 0.15 or 0.25 mm layers; two specimens each, ASTM D1621, 2 mm/min, crosshead strain. Solid modulus and strength are filament datasheet values, not printed coupons. Strength reported is the plateau stress, not first yield."
|
|
17
|
+
},
|
|
18
|
+
"abouali2022": {
|
|
19
|
+
"citation": "Abou-Ali AM, Lee DW, Abu Al-Rub RK. On the Effect of Lattice Topology on Mechanical Properties of SLS Additively Manufactured Sheet-, Ligament-, and Strut-Based Polymeric Metamaterials. Polymers 14(21):4583, 2022.",
|
|
20
|
+
"doi": "10.3390/polym14214583",
|
|
21
|
+
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC9658147/",
|
|
22
|
+
"tables": "Tables 2-4 (designed and measured relative density), Table 5 (power-law fits E = C RD^n, yield = C RD^n over measured RD)",
|
|
23
|
+
"notes": "40 mm cubes, 5x5x5 cells of 8 mm, SLS PA 1102 black (polyamide 11, EOS Formiga P110, 80 um layers), 0.001/s. Only power-law fits over the measured relative density are published, so each 'measured' value here is the paper's fit evaluated at its measured RD (two specimens per point); the solid modulus is the EOS datasheet's (1560 MPa x/y, ISO 527), strength 48 MPa. Sheet lattices weighed 1.4-2.9x their designed density (sintered powder stuck to the large surface): loadpath models the designed geometry."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"materials": {
|
|
27
|
+
"shevchenko-pa12-sls": {"E": 1260.0, "strength": 57.3, "nu": 0.38, "density": 1.01, "basis": "measured in compression by the same lab (Table 1)"},
|
|
28
|
+
"shevchenko-pla-fdm": {"E": 1832.0, "strength": 78.7, "nu": 0.35, "density": 1.21, "basis": "measured in compression by the same lab (Table 1)"},
|
|
29
|
+
"shevchenko-resin-lcd": {"E": 1287.0, "strength": 58.3, "nu": 0.4, "density": 1.18, "basis": "measured in compression by the same lab (Table 1)"},
|
|
30
|
+
"guerrasilva-abs": {"E": 2180.0, "strength": 30.0, "nu": 0.35, "density": 1.195, "basis": "filament datasheet (paper's Table 2)"},
|
|
31
|
+
"guerrasilva-pc": {"E": 1958.0, "strength": 64.0, "nu": 0.37, "density": 1.2, "basis": "filament datasheet (paper's Table 2)"},
|
|
32
|
+
"abouali-pa11-sls": {"E": 1560.0, "strength": 48.0, "nu": 0.4, "density": 0.99, "basis": "EOS PA 1102 black datasheet (x/y, ISO 527)"}
|
|
33
|
+
},
|
|
34
|
+
"specimens": [
|
|
35
|
+
{"id": "shev-sls-primitive", "source": "shevchenko2023", "material": "shevchenko-pa12-sls", "process": "SLS PA12", "lattice": {"type": "schwarz-p", "cell": 10.0, "rho": 0.18, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [62.8, 3.2], "strength": [2.67, 0.12], "strengthKind": "first peak"}},
|
|
36
|
+
{"id": "shev-sls-diamond", "source": "shevchenko2023", "material": "shevchenko-pa12-sls", "process": "SLS PA12", "lattice": {"type": "diamond", "cell": 10.0, "rho": 0.33, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [105.4, 4.1], "strength": [5.90, 0.19], "strengthKind": "first peak"}},
|
|
37
|
+
{"id": "shev-sls-gyroid", "source": "shevchenko2023", "material": "shevchenko-pa12-sls", "process": "SLS PA12", "lattice": {"type": "gyroid", "cell": 10.0, "rho": 0.24, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [68.9, 3.7], "strength": [3.10, 0.15], "strengthKind": "first peak"}},
|
|
38
|
+
{"id": "shev-fdm-primitive", "source": "shevchenko2023", "material": "shevchenko-pla-fdm", "process": "FDM PLA", "lattice": {"type": "schwarz-p", "cell": 10.0, "rho": 0.18, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [38.3, 2.1], "strength": [1.72, 0.15], "strengthKind": "first peak"}},
|
|
39
|
+
{"id": "shev-fdm-diamond", "source": "shevchenko2023", "material": "shevchenko-pla-fdm", "process": "FDM PLA", "lattice": {"type": "diamond", "cell": 10.0, "rho": 0.33, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [134.1, 4.2], "strength": [8.13, 0.21], "strengthKind": "first peak"}},
|
|
40
|
+
{"id": "shev-fdm-gyroid", "source": "shevchenko2023", "material": "shevchenko-pla-fdm", "process": "FDM PLA", "lattice": {"type": "gyroid", "cell": 10.0, "rho": 0.24, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [79.2, 2.2], "strength": [4.73, 0.16], "strengthKind": "first peak"}},
|
|
41
|
+
{"id": "shev-lcd-primitive", "source": "shevchenko2023", "material": "shevchenko-resin-lcd", "process": "LCD resin", "lattice": {"type": "schwarz-p", "cell": 10.0, "rho": 0.18, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [31.3, 0.8], "strength": [1.86, 0.07], "strengthKind": "first peak"}},
|
|
42
|
+
{"id": "shev-lcd-diamond", "source": "shevchenko2023", "material": "shevchenko-resin-lcd", "process": "LCD resin", "lattice": {"type": "diamond", "cell": 10.0, "rho": 0.33, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [113.9, 2.1], "strength": [6.40, 0.11], "strengthKind": "first peak"}},
|
|
43
|
+
{"id": "shev-lcd-gyroid", "source": "shevchenko2023", "material": "shevchenko-resin-lcd", "process": "LCD resin", "lattice": {"type": "gyroid", "cell": 10.0, "rho": 0.24, "wall": 0.8}, "size": [30, 30, 30], "measured": {"E": [68.4, 0.9], "strength": [3.88, 0.06], "strengthKind": "first peak"}},
|
|
44
|
+
|
|
45
|
+
{"id": "gs-001", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.25 mm", "lattice": {"type": "kelvin", "cell": 5.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [63.43, 4.72], "strength": [4.17, 0.03], "strengthKind": "plateau"}},
|
|
46
|
+
{"id": "gs-002", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.15 mm", "lattice": {"type": "kelvin", "cell": 7.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [15.48, 0.30], "strength": [1.00, 0.05], "strengthKind": "plateau"}},
|
|
47
|
+
{"id": "gs-003", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.15 mm", "lattice": {"type": "diamond-strut", "cell": 5.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [19.65, 2.33], "strength": [1.10, 0.01], "strengthKind": "plateau"}},
|
|
48
|
+
{"id": "gs-004", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.25 mm", "lattice": {"type": "diamond-strut", "cell": 7.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [4.49, 0.10], "strength": [0.28, 0.02], "strengthKind": "plateau"}},
|
|
49
|
+
{"id": "gs-005", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.15 mm", "lattice": {"type": "kelvin", "cell": 5.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [89.62, 0.66], "strength": [7.82, 0.21], "strengthKind": "plateau"}},
|
|
50
|
+
{"id": "gs-006", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.25 mm", "lattice": {"type": "kelvin", "cell": 7.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [42.27, 1.34], "strength": [2.68, 0.03], "strengthKind": "plateau"}},
|
|
51
|
+
{"id": "gs-007", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.25 mm", "lattice": {"type": "diamond-strut", "cell": 5.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [57.57, 3.00], "strength": [3.08, 0.02], "strengthKind": "plateau"}},
|
|
52
|
+
{"id": "gs-008", "source": "guerrasilva2021", "material": "guerrasilva-abs", "process": "FDM ABS 0.15 mm", "lattice": {"type": "diamond-strut", "cell": 7.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [18.21, 2.47], "strength": [1.11, 0.01], "strengthKind": "plateau"}},
|
|
53
|
+
{"id": "gs-009", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.15 mm", "lattice": {"type": "kelvin", "cell": 5.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [60.42, 4.55], "strength": [4.79, 0.03], "strengthKind": "plateau"}},
|
|
54
|
+
{"id": "gs-010", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.25 mm", "lattice": {"type": "kelvin", "cell": 7.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [20.39, 2.48], "strength": [1.52, 0.03], "strengthKind": "plateau"}},
|
|
55
|
+
{"id": "gs-011", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.25 mm", "lattice": {"type": "diamond-strut", "cell": 5.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [21.19, 1.39], "strength": [1.55, 0.07], "strengthKind": "plateau"}},
|
|
56
|
+
{"id": "gs-012", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.15 mm", "lattice": {"type": "diamond-strut", "cell": 7.0, "diameter": 1.0}, "size": [35, 35, 35], "measured": {"E": [3.05, 1.02], "strength": [0.29, 0.01], "strengthKind": "plateau"}},
|
|
57
|
+
{"id": "gs-013", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.25 mm", "lattice": {"type": "kelvin", "cell": 5.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [130.66, 0.22], "strength": [12.84, 0.13], "strengthKind": "plateau"}},
|
|
58
|
+
{"id": "gs-014", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.15 mm", "lattice": {"type": "kelvin", "cell": 7.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [33.72, 1.90], "strength": [2.48, 0.93], "strengthKind": "plateau"}},
|
|
59
|
+
{"id": "gs-015", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.15 mm", "lattice": {"type": "diamond-strut", "cell": 5.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [59.83, 0.68], "strength": [3.88, 0.06], "strengthKind": "plateau"}},
|
|
60
|
+
{"id": "gs-016", "source": "guerrasilva2021", "material": "guerrasilva-pc", "process": "FDM PC 0.25 mm", "lattice": {"type": "diamond-strut", "cell": 7.0, "diameter": 1.5}, "size": [35, 35, 35], "measured": {"E": [25.60, 4.62], "strength": [1.54, 0.01], "strengthKind": "plateau"}},
|
|
61
|
+
|
|
62
|
+
{"id": "aa-sheet-gyroid-10", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "gyroid", "cell": 8.0, "rho": 0.104}, "size": [40, 40, 40], "measured": {"rho": 0.181, "fit": {"E": [329.7, 1.08], "strength": [8.9, 1.01]}, "strengthKind": "yield (fit)"}},
|
|
63
|
+
{"id": "aa-sheet-gyroid-18", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "gyroid", "cell": 8.0, "rho": 0.182}, "size": [40, 40, 40], "measured": {"rho": 0.250, "fit": {"E": [329.7, 1.08], "strength": [8.9, 1.01]}, "strengthKind": "yield (fit)"}},
|
|
64
|
+
{"id": "aa-sheet-diamond-13", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "diamond", "cell": 8.0, "rho": 0.126}, "size": [40, 40, 40], "measured": {"rho": 0.279, "fit": {"E": [225.5, 0.75], "strength": [17.4, 1.52]}, "strengthKind": "yield (fit)"}},
|
|
65
|
+
{"id": "aa-sheet-diamond-21", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "diamond", "cell": 8.0, "rho": 0.209}, "size": [40, 40, 40], "measured": {"rho": 0.342, "fit": {"E": [225.5, 0.75], "strength": [17.4, 1.52]}, "strengthKind": "yield (fit)"}},
|
|
66
|
+
{"id": "aa-octet-11", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "octet", "cell": 8.0, "rho": 0.114}, "size": [40, 40, 40], "measured": {"rho": 0.118, "fit": {"E": [79.0, 0.74], "strength": [4.5, 0.91]}, "strengthKind": "yield (fit)"}},
|
|
67
|
+
{"id": "aa-octet-25", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "octet", "cell": 8.0, "rho": 0.25}, "size": [40, 40, 40], "measured": {"rho": 0.284, "fit": {"E": [79.0, 0.74], "strength": [4.5, 0.91]}, "strengthKind": "yield (fit)"}},
|
|
68
|
+
{"id": "aa-kelvin-14", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "kelvin", "cell": 8.0, "rho": 0.14}, "size": [40, 40, 40], "measured": {"rho": 0.153, "fit": {"E": [808.5, 1.85], "strength": [32.7, 2.00]}, "strengthKind": "yield (fit)"}},
|
|
69
|
+
{"id": "aa-kelvin-25", "source": "abouali2022", "material": "abouali-pa11-sls", "process": "SLS PA11", "lattice": {"type": "kelvin", "cell": 8.0, "rho": 0.247}, "size": [40, 40, 40], "measured": {"rho": 0.276, "fit": {"E": [808.5, 1.85], "strength": [32.7, 2.00]}, "strengthKind": "yield (fit)"}}
|
|
70
|
+
]
|
|
71
|
+
}
|
loadpath/decimate.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Adaptive decimation of a field mesh against the field itself.
|
|
2
|
+
|
|
3
|
+
Marching cubes puts triangles a pitch apart everywhere: a flat face gets
|
|
4
|
+
as many as a fillet. `decimate` collapses edges, shortest first, wherever
|
|
5
|
+
the result stays within `tol` mm of the true surface, so flat and gently
|
|
6
|
+
curved regions thin out and creases, small fillets and thin lattice necks
|
|
7
|
+
keep their triangles.
|
|
8
|
+
|
|
9
|
+
Each pass works on many edges at once (numpy, no priority queue):
|
|
10
|
+
|
|
11
|
+
1. candidates: edges whose two ends share exactly two neighbours (the
|
|
12
|
+
link condition, so the collapse keeps the mesh a closed manifold) and
|
|
13
|
+
touch no locked vertex;
|
|
14
|
+
2. an independent set: an edge goes ahead when it is the shortest
|
|
15
|
+
candidate around both its ends and their neighbours, so no two
|
|
16
|
+
collapses in a pass share a triangle;
|
|
17
|
+
3. each collapse moves the pair to its midpoint projected onto the field's
|
|
18
|
+
zero level (two Newton steps) and is kept only if every triangle it
|
|
19
|
+
reshapes stays within `tol` of the surface at its centroid and edge
|
|
20
|
+
midpoints (|field| there, which is the distance for the distance-like
|
|
21
|
+
fields in fields.py) and does not turn over (normal within ~60 degrees
|
|
22
|
+
of before).
|
|
23
|
+
|
|
24
|
+
Passes repeat until one removes under 1% of the triangles. The mesh
|
|
25
|
+
stays watertight, and every vertex stays on the surface.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
__all__ = ["decimate"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normals(T):
|
|
35
|
+
n = np.cross(T[:, 1] - T[:, 0], T[:, 2] - T[:, 0])
|
|
36
|
+
return n / np.maximum(np.linalg.norm(n, axis=1, keepdims=True), 1e-300)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decimate(V, F, field, tol, pitch, locked=None, max_passes=80, min_gain=0.01, progress=None,
|
|
40
|
+
target=None, place=None, turn=0.5, labels=None, allow=None, max_reject=3, quality=None):
|
|
41
|
+
"""(V, F, stats, kept): the mesh with edges collapsed wherever it stays within
|
|
42
|
+
`tol` mm of field = 0. `locked` vertices (a mask) never move or go:
|
|
43
|
+
the streamed export's seams. Unused vertices are dropped (locked ones
|
|
44
|
+
keep their order relative to each other); `kept` is the mask of the
|
|
45
|
+
input vertices that remain.
|
|
46
|
+
|
|
47
|
+
With `field` None there is no surface to hold to: a collapse of edges
|
|
48
|
+
(a, b) puts the new vertex at `place(V, a, b)` (the midpoints if not
|
|
49
|
+
given), and passes stop once the mesh is down to `target` faces.
|
|
50
|
+
Shortest edges first makes that an even coarsening. `turn` is the
|
|
51
|
+
least cosine between a reshaped face's normal and its old one.
|
|
52
|
+
`labels` (one per face) ride along with the faces and come back in
|
|
53
|
+
stats["labels"]; `allow(E, F, labels)` filters each pass's candidate
|
|
54
|
+
edges (a mask), say to keep region seams in place. A vertex whose
|
|
55
|
+
collapses failed `max_reject` times is left alone. `quality(Told, Tnew,
|
|
56
|
+
labels)` (the reshaped triangles' corners before and after, (k, 3, 3),
|
|
57
|
+
and their labels) can veto collapses too."""
|
|
58
|
+
from .fields import _project
|
|
59
|
+
from .narrowband import peval
|
|
60
|
+
|
|
61
|
+
V = np.array(V, float)
|
|
62
|
+
F = np.array(F, np.int64)
|
|
63
|
+
n = len(V)
|
|
64
|
+
lock = np.zeros(n, bool) if locked is None else np.array(locked, bool)
|
|
65
|
+
alive = np.ones(n, bool)
|
|
66
|
+
rej = np.zeros(n, np.int64)
|
|
67
|
+
f0 = len(F)
|
|
68
|
+
lab = None if labels is None else np.asarray(labels).copy()
|
|
69
|
+
rng = np.random.default_rng(0)
|
|
70
|
+
passes = 0
|
|
71
|
+
for passes in range(1, max_passes + 1):
|
|
72
|
+
nf = len(F)
|
|
73
|
+
a = np.r_[F[:, 0], F[:, 1], F[:, 2]]
|
|
74
|
+
b = np.r_[F[:, 1], F[:, 2], F[:, 0]]
|
|
75
|
+
# closed manifold: every edge is a half-edge each way, so the edges
|
|
76
|
+
# are the half-edges with a < b
|
|
77
|
+
h = a < b
|
|
78
|
+
E = np.stack([a[h], b[h]], axis=1)
|
|
79
|
+
# an edge that failed before yields to its neighbours, or it would
|
|
80
|
+
# block them pass after pass; a vertex that failed three times is done
|
|
81
|
+
frozen = lock | (rej >= max_reject)
|
|
82
|
+
E = E[~(frozen[E[:, 0]] | frozen[E[:, 1]])]
|
|
83
|
+
if allow is not None and len(E):
|
|
84
|
+
E = E[allow(E, F, lab)]
|
|
85
|
+
if not len(E):
|
|
86
|
+
break
|
|
87
|
+
d = V[E[:, 0]] - V[E[:, 1]]
|
|
88
|
+
prio = (np.einsum("ij,ij->i", d, d) * np.exp2(2 * (rej[E[:, 0]] + rej[E[:, 1]]))
|
|
89
|
+
* (1.0 + 1e-6 * rng.random(len(E))))
|
|
90
|
+
# independent set: shortest around both ends and their neighbours
|
|
91
|
+
m = np.full(n, np.inf)
|
|
92
|
+
np.minimum.at(m, E[:, 0], prio)
|
|
93
|
+
np.minimum.at(m, E[:, 1], prio)
|
|
94
|
+
M = m.copy()
|
|
95
|
+
np.minimum.at(M, a, m[b])
|
|
96
|
+
win = (prio == M[E[:, 0]]) & (prio == M[E[:, 1]])
|
|
97
|
+
W = E[win]
|
|
98
|
+
if not len(W):
|
|
99
|
+
break
|
|
100
|
+
# the triangles around each collapse: no triangle holds ends of two
|
|
101
|
+
# collapses (their ends are not neighbours), so each belongs to at
|
|
102
|
+
# most one
|
|
103
|
+
wid = np.full(n, -1)
|
|
104
|
+
wid[W[:, 0]] = np.arange(len(W))
|
|
105
|
+
wid[W[:, 1]] = np.arange(len(W))
|
|
106
|
+
fw = wid[F].max(axis=1)
|
|
107
|
+
fi = np.flatnonzero(fw >= 0)
|
|
108
|
+
tri, w = F[fi], fw[fi]
|
|
109
|
+
wa, wb = W[w, 0], W[w, 1]
|
|
110
|
+
in_a = (tri == wa[:, None]).any(axis=1)
|
|
111
|
+
in_b = (tri == wb[:, None]).any(axis=1)
|
|
112
|
+
# link condition: the two ends share exactly the two opposite vertices
|
|
113
|
+
nk = []
|
|
114
|
+
for side in (in_a, in_b):
|
|
115
|
+
t, ww = tri[side], w[side]
|
|
116
|
+
v = t.ravel()
|
|
117
|
+
k = np.repeat(ww, 3) * n + v
|
|
118
|
+
k = k[(v != np.repeat(W[ww, 0], 3)) & (v != np.repeat(W[ww, 1], 3))]
|
|
119
|
+
nk.append(np.unique(k))
|
|
120
|
+
common = np.bincount(np.intersect1d(nk[0], nk[1], assume_unique=True) // n, minlength=len(W))
|
|
121
|
+
link = common == 2
|
|
122
|
+
rej[W[~link].ravel()] += 1
|
|
123
|
+
sel = link[w] & ~(in_a & in_b) # the two triangles on the edge go
|
|
124
|
+
tri, w, wa, wb = tri[sel], w[sel], wa[sel], wb[sel]
|
|
125
|
+
pr = np.stack([w, fi[sel]], axis=1)
|
|
126
|
+
# the new vertex: midpoint, onto the surface
|
|
127
|
+
P = np.zeros((len(W), 3))
|
|
128
|
+
li = np.flatnonzero(link)
|
|
129
|
+
if len(li) and field is None:
|
|
130
|
+
P[li] = (place(V, W[li, 0], W[li, 1]) if place is not None
|
|
131
|
+
else 0.5 * (V[W[li, 0]] + V[W[li, 1]]))
|
|
132
|
+
elif len(li):
|
|
133
|
+
q = _project(field, 0.5 * (V[W[li, 0]] + V[W[li, 1]]), pitch)
|
|
134
|
+
P[li] = _project(field, q, pitch)
|
|
135
|
+
Told = V[tri]
|
|
136
|
+
Tnew = Told.copy()
|
|
137
|
+
hit = (tri == wa[:, None]) | (tri == wb[:, None])
|
|
138
|
+
Tnew[hit] = np.repeat(P[pr[:, 0]], hit.sum(axis=1), axis=0)
|
|
139
|
+
good = (_normals(Told) * _normals(Tnew)).sum(axis=1) > turn
|
|
140
|
+
if quality is not None and len(Tnew):
|
|
141
|
+
good &= quality(Told, Tnew, None if lab is None else lab[pr[:, 1]])
|
|
142
|
+
if field is not None:
|
|
143
|
+
S = np.concatenate([Tnew.mean(axis=1), 0.5 * (Tnew[:, 0] + Tnew[:, 1]),
|
|
144
|
+
0.5 * (Tnew[:, 1] + Tnew[:, 2]), 0.5 * (Tnew[:, 2] + Tnew[:, 0])])
|
|
145
|
+
dev = np.abs(peval(field, S)).reshape(4, -1).max(axis=0)
|
|
146
|
+
good &= dev <= tol
|
|
147
|
+
bad = ~link
|
|
148
|
+
np.logical_or.at(bad, pr[:, 0], ~good)
|
|
149
|
+
acc = ~bad
|
|
150
|
+
rej[W[bad & link].ravel()] += 1
|
|
151
|
+
if target is not None: # each collapse takes two triangles: do not overshoot
|
|
152
|
+
room = max(0, (len(F) - int(target)) // 2)
|
|
153
|
+
acc[np.flatnonzero(acc)[room:]] = False
|
|
154
|
+
if not acc.any():
|
|
155
|
+
break
|
|
156
|
+
Wa, Wb = W[acc, 0], W[acc, 1]
|
|
157
|
+
V[Wa] = P[acc]
|
|
158
|
+
remap = np.arange(n)
|
|
159
|
+
remap[Wb] = Wa
|
|
160
|
+
alive[Wb] = False
|
|
161
|
+
F = remap[F]
|
|
162
|
+
keepf = (F[:, 0] != F[:, 1]) & (F[:, 1] != F[:, 2]) & (F[:, 0] != F[:, 2])
|
|
163
|
+
F = F[keepf]
|
|
164
|
+
if lab is not None:
|
|
165
|
+
lab = lab[keepf]
|
|
166
|
+
if progress:
|
|
167
|
+
progress({"stage": "decimate", "pass": passes, "faces": int(len(F))})
|
|
168
|
+
if nf - len(F) < min_gain * nf or (target is not None and len(F) <= target):
|
|
169
|
+
break
|
|
170
|
+
used = np.zeros(n, bool)
|
|
171
|
+
used[F] = True
|
|
172
|
+
used |= lock & alive
|
|
173
|
+
new = np.cumsum(used) - 1
|
|
174
|
+
stats = {"tolerance": float(tol), "facesBefore": int(f0), "faces": int(len(F)), "passes": int(passes)}
|
|
175
|
+
if lab is not None:
|
|
176
|
+
stats["labels"] = lab
|
|
177
|
+
return V[used], new[F], stats, used
|
loadpath/delivery.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The part to hand over, in one place: report.json["deliver"].
|
|
3
|
+
|
|
4
|
+
A run's report lists every outcome it tried; an agent wants the one it
|
|
5
|
+
should give the user, whether it passes, which number to quote as its safety
|
|
6
|
+
factor (and from which analysis), what set its size, and every warning that
|
|
7
|
+
matters for it, gathered from the outcome, its print and process checks, its
|
|
8
|
+
durability and the run. Agents testing the brief evals kept missing print and
|
|
9
|
+
fatigue warnings that sat three levels down, and quoting the voxel safety
|
|
10
|
+
factor where the report had replaced it with a finer one.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
_VF_CEILING = 0.7 # study._VF_MAX: the search never goes past it
|
|
15
|
+
_VF_FLOOR = 0.02 # study._VF_MIN
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def attention(o: dict, study: dict) -> list[str]:
|
|
19
|
+
"""What SKILL.md tells an agent to look for, already looked for."""
|
|
20
|
+
out = []
|
|
21
|
+
if o.get("passes") is False:
|
|
22
|
+
# which limit it misses, so an agent can tell a weak part from a floppy one
|
|
23
|
+
goal = study.get("goal") or {}
|
|
24
|
+
why = []
|
|
25
|
+
sf, want = o.get("safetyFactor"), goal.get("safetyFactor")
|
|
26
|
+
if want and sf is not None and sf < want:
|
|
27
|
+
why.append(f"safety factor {sf:.2f} under {want:g}")
|
|
28
|
+
dmax = goal.get("maxDeflection")
|
|
29
|
+
defl = max((c.get("deflection") or 0.0 for c in o.get("cases") or []), default=0.0)
|
|
30
|
+
if dmax and defl > dmax:
|
|
31
|
+
why.append(f"deflection {defl:.2f} mm over {dmax:g}")
|
|
32
|
+
out.append("does not meet the goal" + (f" ({', '.join(why)})" if why else "") +
|
|
33
|
+
": raise the volume fraction or the material, or check the loads")
|
|
34
|
+
if (o.get("bodies") or 1) > 1:
|
|
35
|
+
out.append(f"{o['bodies']} separate bodies: something is not connected to a support")
|
|
36
|
+
if (o.get("keepOutIntrusion") or 0) > 1.0:
|
|
37
|
+
out.append(f"keepOutIntrusion {o['keepOutIntrusion']:.1f} mm³: material in a keep-out (report a bug)")
|
|
38
|
+
if (o.get("preserveMissing") or 0) > 1.0:
|
|
39
|
+
out.append(f"preserveMissing {o['preserveMissing']:.1f} mm³: preserved geometry lost (report a bug)")
|
|
40
|
+
mm = study.get("minMember")
|
|
41
|
+
p5 = _p5(o)
|
|
42
|
+
if mm and p5 is not None and p5 < 0.8 * mm:
|
|
43
|
+
out.append(f"p5Member {p5:.2f} mm is under minMember {mm} mm: raise the resolution")
|
|
44
|
+
if o.get("closedBores"):
|
|
45
|
+
out.append(f"closed bores: {o['closedBores']}")
|
|
46
|
+
dur = o.get("durability") or {}
|
|
47
|
+
if dur.get("safetyFactor") is not None and dur["safetyFactor"] < dur.get("target", 1.5):
|
|
48
|
+
out.append(f"durability safety factor {dur['safetyFactor']:.2f} (fatigue/creep) is under "
|
|
49
|
+
f"{dur.get('target', 1.5):g}: set durability.constrain to size for it")
|
|
50
|
+
out.extend(dur.get("warnings") or [])
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _p5(o: dict):
|
|
55
|
+
if o.get("p5Member") is not None:
|
|
56
|
+
return o["p5Member"]
|
|
57
|
+
return (o.get("confirm") or {}).get("p5Member")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sf_method(o: dict) -> str:
|
|
61
|
+
c = o.get("confirm")
|
|
62
|
+
if not c:
|
|
63
|
+
return "voxel"
|
|
64
|
+
return c.get("method") or "voxel-fine"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _governing(o: dict, goal: dict) -> str | None:
|
|
68
|
+
"""Which of the goal's limits the delivered part is closest to."""
|
|
69
|
+
ms = {}
|
|
70
|
+
if goal.get("safetyFactor") and o.get("safetyFactor"):
|
|
71
|
+
ms["safetyFactor"] = o["safetyFactor"] / goal["safetyFactor"]
|
|
72
|
+
if goal.get("maxDeflection"):
|
|
73
|
+
d = max((c.get("deflection") or 0.0) for c in o.get("cases") or [{}])
|
|
74
|
+
if d > 0:
|
|
75
|
+
ms["maxDeflection"] = goal["maxDeflection"] / d
|
|
76
|
+
dur = o.get("durability") or {}
|
|
77
|
+
if dur.get("safetyFactor") is not None and dur.get("constrain"):
|
|
78
|
+
ms["durability"] = dur["safetyFactor"] / dur.get("target", 1.5)
|
|
79
|
+
return min(ms, key=ms.get) if ms else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def delivery(summary: dict, study: dict) -> dict | None:
|
|
83
|
+
"""report.json["deliver"]: the best outcome, flattened, with every
|
|
84
|
+
warning that applies to it."""
|
|
85
|
+
outs = summary.get("outcomes") or []
|
|
86
|
+
best = summary.get("best")
|
|
87
|
+
if best is None:
|
|
88
|
+
if not outs:
|
|
89
|
+
return None
|
|
90
|
+
best = max(range(len(outs)), key=lambda i: outs[i].get("safetyFactor") or 0.0)
|
|
91
|
+
o = outs[best]
|
|
92
|
+
goal = study.get("goal") or {}
|
|
93
|
+
warn = []
|
|
94
|
+
for w in (list(summary.get("warnings") or []) + list(o.get("warnings") or [])
|
|
95
|
+
+ list((o.get("print") or {}).get("warnings") or [])
|
|
96
|
+
+ list((o.get("process") or {}).get("warnings") or []) + attention(o, study)):
|
|
97
|
+
if w not in warn:
|
|
98
|
+
warn.append(w)
|
|
99
|
+
peaks = {c.get("name"): c.get("peakStress") for c in (o.get("confirm") or {}).get("cases") or []}
|
|
100
|
+
cases = [{"name": c.get("name"), "safetyFactor": c.get("safetyFactor"), "deflection": c.get("deflection"),
|
|
101
|
+
"peakStress": c.get("peakStress") if c.get("peakStress") is not None else peaks.get(c.get("name")),
|
|
102
|
+
**({"peakAt": c["peakAt"]} if c.get("peakAt") is not None else {}),
|
|
103
|
+
**({"worstDirection": c["worstDirection"]} if c.get("worstDirection") else {})}
|
|
104
|
+
for c in o.get("cases") or []]
|
|
105
|
+
d = {
|
|
106
|
+
"outcome": best,
|
|
107
|
+
"passes": o.get("passes") if summary.get("best") is not None else False,
|
|
108
|
+
"stl": o.get("stl"),
|
|
109
|
+
**({"3mf": o["3mf"]} if o.get("3mf") else {}),
|
|
110
|
+
**({"render": o["render"]} if o.get("render") else {}),
|
|
111
|
+
"material": o.get("material"),
|
|
112
|
+
"mass": o.get("mass"),
|
|
113
|
+
"volumeFraction": o.get("volumeFraction"),
|
|
114
|
+
"safetyFactor": o.get("safetyFactor"),
|
|
115
|
+
# which analysis that number comes from: "tet" (body-fitted cross-check),
|
|
116
|
+
# "voxel-fine" (a finer voxel re-analysis) or "voxel" (the design grid)
|
|
117
|
+
"safetyFactorMethod": _sf_method(o),
|
|
118
|
+
**({"target": {k: goal[k] for k in ("safetyFactor", "maxDeflection", "maxMass") if k in goal}}
|
|
119
|
+
if any(k in goal for k in ("safetyFactor", "maxDeflection", "maxMass")) else {}),
|
|
120
|
+
"governedBy": _governing(o, goal),
|
|
121
|
+
"maxDeflection": max((c.get("deflection") or 0.0) for c in o.get("cases") or [{}]) or None,
|
|
122
|
+
"cases": cases,
|
|
123
|
+
"p5Member": _p5(o),
|
|
124
|
+
**({"durabilitySafetyFactor": o["durability"].get("safetyFactor")} if o.get("durability") else {}),
|
|
125
|
+
**{k: o[k] for k in ("bodies", "keepOutIntrusion", "preserveMissing") if o.get(k) is not None},
|
|
126
|
+
**({"process": o["process"]} if o.get("process") else {}),
|
|
127
|
+
**({"print": {k: v for k, v in o["print"].items() if k != "warnings"}} if o.get("print") else {}),
|
|
128
|
+
"warnings": warn,
|
|
129
|
+
}
|
|
130
|
+
cap = goal.get("maxMass")
|
|
131
|
+
if cap and d.get("mass") is not None and d["mass"] > cap:
|
|
132
|
+
d["passes"] = False
|
|
133
|
+
d["warnings"].insert(0, f"over the mass cap: {d['mass']:.1f} g against goal.maxMass {cap:g} g; this is "
|
|
134
|
+
"already the lightest that meets the other limits, so use a lighter material, "
|
|
135
|
+
"trim the preserves or lower the safety factor")
|
|
136
|
+
dur = o.get("durability") or {}
|
|
137
|
+
if d["passes"] and dur.get("safetyFactor") is not None and dur["safetyFactor"] < dur.get("target", 1.5):
|
|
138
|
+
# a permanently loaded PLA bracket read static SF 2.6 and passes: true
|
|
139
|
+
# at creep SF 1.09; "passes" means the whole part, durability included
|
|
140
|
+
d["passes"] = False
|
|
141
|
+
d["warnings"].insert(0, f"fails on durability: fatigue/creep safety factor {dur['safetyFactor']:.2f} is "
|
|
142
|
+
f"under {dur.get('target', 1.5):g} (the static goal passes); set "
|
|
143
|
+
"durability.constrain: true to size the part for it")
|
|
144
|
+
proc = study.get("process")
|
|
145
|
+
kind = proc.get("type") if isinstance(proc, dict) else proc
|
|
146
|
+
if str(kind or "").lower() == "fdm" and "slicer" not in o:
|
|
147
|
+
d["slicer"] = "not run: no slicer found or none asked for (install PrusaSlicer or set LOADPATH_SLICER " \
|
|
148
|
+
"for print time and filament)"
|
|
149
|
+
# the one number to quote: the factor of whichever limit sets the part
|
|
150
|
+
d["governingSafetyFactor"] = d.get("durabilitySafetyFactor") if d["governedBy"] == "durability" \
|
|
151
|
+
else d.get("safetyFactor")
|
|
152
|
+
if d["governedBy"] == "durability" and d.get("durabilitySafetyFactor") is not None:
|
|
153
|
+
d["warnings"].insert(0, f"durability (fatigue/creep) sets this part: quote its safety factor "
|
|
154
|
+
f"{d['durabilitySafetyFactor']:.2f}, not the static {d['safetyFactor']:.2f}")
|
|
155
|
+
if summary.get("best") is not None and (o.get("volumeFraction") or 1.0) <= _VF_FLOOR + 1e-6:
|
|
156
|
+
d["governedBy"] = "volumeFraction floor"
|
|
157
|
+
d["warnings"].insert(0, f"the search stopped at its smallest volume fraction ({_VF_FLOOR:.0%}) with the "
|
|
158
|
+
"safety factor still over target: the preserved interfaces, not the loads, set "
|
|
159
|
+
"the mass; trim the preserves or the design space to go lighter")
|
|
160
|
+
if summary.get("best") is None:
|
|
161
|
+
d["warnings"].insert(0, "no outcome meets the goal: this is the strongest one tried, not a passing part")
|
|
162
|
+
sat = _saturated(outs)
|
|
163
|
+
if sat:
|
|
164
|
+
d["warnings"].insert(1, sat)
|
|
165
|
+
top = max((x.get("volumeFraction") or 0.0) for x in outs)
|
|
166
|
+
if top >= _VF_CEILING - 1e-6:
|
|
167
|
+
d["warnings"].insert(1, f"the search stopped at the volume-fraction ceiling ({_VF_CEILING:.0%} of the design "
|
|
168
|
+
"space): more material is not allowed, so change the material, the design space or the loads")
|
|
169
|
+
return d
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _saturated(outs: list) -> str | None:
|
|
173
|
+
"""When more material hardly helps, the shape is not the problem: a notch
|
|
174
|
+
(a keep-out's corner, a thin interface) caps the safety factor."""
|
|
175
|
+
pts = sorted((o["volumeFraction"], o["safetyFactor"], o) for o in outs
|
|
176
|
+
if o.get("volumeFraction") and o.get("safetyFactor"))
|
|
177
|
+
import math
|
|
178
|
+
|
|
179
|
+
# a sound layout's safety factor grows at least as fast as material
|
|
180
|
+
# (roughly vf^1 to vf^2); under vf^0.6 the material is not where it fails
|
|
181
|
+
if len(pts) < 2 or pts[-1][0] < 1.3 * pts[0][0] or pts[-1][1] <= 0 or pts[0][1] <= 0 or \
|
|
182
|
+
math.log(pts[-1][1] / pts[0][1]) > 0.6 * math.log(pts[-1][0] / pts[0][0]):
|
|
183
|
+
return None
|
|
184
|
+
worst = min(pts[-1][2].get("cases") or [{}], key=lambda c: c.get("safetyFactor") or float("inf"))
|
|
185
|
+
at = worst.get("peakAt")
|
|
186
|
+
where = f" at {[round(v, 1) for v in at]} (case {worst.get('name')!r})" if at else ""
|
|
187
|
+
return (f"the safety factor barely rises with material ({pts[0][1]:.2f} at {pts[0][0]:.0%} of the design "
|
|
188
|
+
f"space, {pts[-1][1]:.2f} at {pts[-1][0]:.0%}): a local hot spot{where} sets it, so look at the "
|
|
189
|
+
"geometry there (a keep-out notching an interface, a thin preserved ring, a fastener's head path) "
|
|
190
|
+
"rather than adding material")
|