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/infill.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stress-graded FDM infill: the slicer grades the part, loadpath says where.
|
|
3
|
+
|
|
4
|
+
A slicer already knows how to print a part sparse inside and solid at the
|
|
5
|
+
skin. What it does not know is where the part can afford to be sparse, so
|
|
6
|
+
people print everything at 15-20% and hope, or at 100% and wait. This sizes
|
|
7
|
+
a ladder of (infill density, perimeter count) levels region by region from
|
|
8
|
+
the part's stress, then hands the regions to the slicer as modifier volumes
|
|
9
|
+
in a 3MF (threemf.write_3mf): no lattice mesh, no million triangles, and the
|
|
10
|
+
slicer's own toolpaths.
|
|
11
|
+
|
|
12
|
+
The sizing is the homogenized fully-stressed design lattice.py uses for TPMS
|
|
13
|
+
cores, with the slicer's construction in place of a sheet lattice:
|
|
14
|
+
|
|
15
|
+
- each analysis cell is part skin, part infill. The skin is the perimeters
|
|
16
|
+
(count x line width) and is solid; the rest is gyroid infill at the
|
|
17
|
+
level's density rho, a sheet of modulus E C rho^n whose walls carry
|
|
18
|
+
A rho^-m times the macro stress (lattice.SHEETS: PrusaSlicer's gyroid is
|
|
19
|
+
the same surface drawn one extrusion wide), at a knockdown for walls that
|
|
20
|
+
are a single line with a seam every layer;
|
|
21
|
+
- a voxel FEA of the graded part gives each cell's strain; each cell takes
|
|
22
|
+
the lowest level whose skin and infill both stay under strength / (SF x
|
|
23
|
+
verify's 1.15 allowance), holding the cell's macro stress (fully stressed
|
|
24
|
+
design), and the analysis repeats as the load moves onto what got stiffer;
|
|
25
|
+
- material round preserved shapes, keep-outs, loads and supports (a
|
|
26
|
+
`collar`) stays solid: a bolt boss is not where to save filament.
|
|
27
|
+
|
|
28
|
+
The verified safety factor of the solid part is scaled by how much the
|
|
29
|
+
graded part reads below the solid one on the same grid and loads, so the
|
|
30
|
+
grading carries the delivered part's refined verification (submodels,
|
|
31
|
+
interlayer check) instead of the coarser analysis grid's absolute numbers.
|
|
32
|
+
Deflection is scaled the same way and the whole ladder floor steps up while
|
|
33
|
+
it breaks `goal.maxDeflection`.
|
|
34
|
+
|
|
35
|
+
The infill constants are estimates: sheet-gyroid unit-cell FEA, and a 0.8
|
|
36
|
+
knockdown for single-line walls. Printed coupons (the calibration loop) are
|
|
37
|
+
what will tighten them.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
from scipy import ndimage
|
|
45
|
+
|
|
46
|
+
from .lattice import SHEETS, _block_mean, _collar_distance, _signed_distance
|
|
47
|
+
from .spec import Study
|
|
48
|
+
|
|
49
|
+
__all__ = ["GradeOptions", "grade_infill", "DEFAULT_LEVELS"]
|
|
50
|
+
|
|
51
|
+
# (infill %, perimeters) from sparsest to densest; solid (100%) is always
|
|
52
|
+
# the level above the last. One perimeter count throughout by default: a
|
|
53
|
+
# modifier that changes the perimeters makes PrusaSlicer split the layer
|
|
54
|
+
# there and run perimeters round the cut, inside the part (the shelf
|
|
55
|
+
# bracket's external perimeters went 6 -> 16 cm^3 and the print took 45 min
|
|
56
|
+
# longer than with the same infill at 3 walls throughout). Changing only the
|
|
57
|
+
# density merges the regions' perimeters.
|
|
58
|
+
DEFAULT_LEVELS = ((10, 3), (15, 3), (25, 3), (40, 3), (60, 3))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class GradeOptions:
|
|
63
|
+
levels: tuple = DEFAULT_LEVELS
|
|
64
|
+
line_width: float = 0.45 # mm per perimeter (the profile's perimeter width)
|
|
65
|
+
safety_factor: float | None = None # default: the study goal's, else 2
|
|
66
|
+
collar: float = 2.0 # mm of solid round preserves, keep-outs, loads, supports
|
|
67
|
+
knockdown: float = 0.8 # infill wall strength / solid strength
|
|
68
|
+
analysis_cells: int = 25_000
|
|
69
|
+
iterations: int = 12
|
|
70
|
+
modifier_faces: int = 20_000 # per modifier mesh, after decimation
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_spec(cls, spec, safety_factor: float | None = None) -> "GradeOptions":
|
|
74
|
+
spec = dict(spec or {})
|
|
75
|
+
o = cls()
|
|
76
|
+
if "levels" in spec:
|
|
77
|
+
lv = []
|
|
78
|
+
for item in spec["levels"]:
|
|
79
|
+
if isinstance(item, dict):
|
|
80
|
+
d, p = item.get("infill"), item.get("perimeters", 3)
|
|
81
|
+
else:
|
|
82
|
+
d, p = item
|
|
83
|
+
d, p = float(d), int(p)
|
|
84
|
+
if not 0.0 < d < 100.0 or p < 1:
|
|
85
|
+
raise ValueError("slicer.grade.levels are [infill % (0..100, exclusive), perimeters >= 1]")
|
|
86
|
+
lv.append((d, p))
|
|
87
|
+
if not lv:
|
|
88
|
+
raise ValueError("slicer.grade.levels is empty")
|
|
89
|
+
lv.sort()
|
|
90
|
+
o.levels = tuple(lv)
|
|
91
|
+
for key, attr in (("lineWidth", "line_width"), ("collar", "collar"), ("knockdown", "knockdown"),
|
|
92
|
+
("analysisCells", "analysis_cells")):
|
|
93
|
+
if key in spec:
|
|
94
|
+
setattr(o, attr, type(getattr(o, attr))(spec[key]))
|
|
95
|
+
if not 0.0 < o.knockdown <= 1.0:
|
|
96
|
+
raise ValueError("slicer.grade.knockdown is a fraction of the solid strength, in (0, 1]")
|
|
97
|
+
o.safety_factor = float(spec.get("safetyFactor") or safety_factor or 2.0)
|
|
98
|
+
return o
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _level_props(o: GradeOptions, kind: str = "gyroid"):
|
|
102
|
+
"""Per level (the solid one last): relative density, skin thickness mm,
|
|
103
|
+
sheet modulus factor and infill wall-stress factor."""
|
|
104
|
+
s = SHEETS[kind]
|
|
105
|
+
rho = np.array([d / 100.0 for d, _ in o.levels] + [1.0])
|
|
106
|
+
skin = np.array([p * o.line_width for _, p in o.levels] + [o.levels[-1][1] * o.line_width])
|
|
107
|
+
emod = np.where(rho >= 1.0, 1.0, s["C"] * rho ** s["n"])
|
|
108
|
+
wall = np.where(rho >= 1.0, 1.0, s["A"] * rho ** -s["m"])
|
|
109
|
+
return rho, skin, emod, wall
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def grade_infill(study: Study, mesh, verified: dict | None = None, opts: GradeOptions | None = None,
|
|
113
|
+
on_progress=None) -> dict:
|
|
114
|
+
"""Size per-region infill for `mesh` (the delivered part). `verified` is
|
|
115
|
+
its verification report (safety factor and per-case deflection to
|
|
116
|
+
scale). Returns {"objectSettings", "modifiers" (meshes and settings for
|
|
117
|
+
threemf.write_3mf), "levels", "safetyFactor", "predicted", ...}."""
|
|
118
|
+
from scipy.spatial import cKDTree
|
|
119
|
+
|
|
120
|
+
from .domain import boundary_conditions, gravity_force
|
|
121
|
+
from .fem import HexMesh, Solver, Stiffness, von_mises
|
|
122
|
+
from .verify import STRESS_ALLOWANCE, voxelize
|
|
123
|
+
|
|
124
|
+
o = opts or GradeOptions(safety_factor=study.goal.safety_factor or 2.0)
|
|
125
|
+
mat = study.material
|
|
126
|
+
rho_l, skin_l, emod_l, wall_l = _level_props(o)
|
|
127
|
+
n_lv = len(rho_l)
|
|
128
|
+
solid_lv = n_lv - 1
|
|
129
|
+
|
|
130
|
+
vol = float(mesh.volume)
|
|
131
|
+
pitch = (vol / o.analysis_cells) ** (1.0 / 3.0)
|
|
132
|
+
r = 3
|
|
133
|
+
step = pitch / r
|
|
134
|
+
lo = np.asarray(mesh.bounds[0], float) - pitch
|
|
135
|
+
hi = np.asarray(mesh.bounds[1], float) + pitch
|
|
136
|
+
dims = tuple(int(np.ceil((hi[i] - lo[i]) / pitch)) * r for i in range(3))
|
|
137
|
+
solid, origin = voxelize(mesh, step, box=(lo, lo + np.asarray(dims) * step))
|
|
138
|
+
if solid.shape != dims:
|
|
139
|
+
out = np.zeros(dims, bool)
|
|
140
|
+
sl = tuple(slice(0, min(a, b)) for a, b in zip(solid.shape, dims))
|
|
141
|
+
out[sl] = solid[sl]
|
|
142
|
+
solid = out
|
|
143
|
+
depth = -_signed_distance(solid, step) # mm inside the surface, > 0 inside
|
|
144
|
+
f_in = _block_mean(solid.astype(np.float32), r)
|
|
145
|
+
# share of each analysis cell's material that is skin at each level
|
|
146
|
+
fs_lv = []
|
|
147
|
+
for t in skin_l:
|
|
148
|
+
fs_lv.append(_block_mean((solid & (depth < t)).astype(np.float32), r) / np.maximum(f_in, 1e-6))
|
|
149
|
+
active = f_in >= 0.5
|
|
150
|
+
lbl, _ = ndimage.label(active)
|
|
151
|
+
ijk = np.argwhere(active)
|
|
152
|
+
hm = HexMesh(ijk, pitch, lo)
|
|
153
|
+
bc = boundary_conditions(study, hm, check_loads=False, tol_floor=0.5 * pitch)
|
|
154
|
+
sup = np.unique(lbl[tuple(ijk[bc.support_nodes[hm.enode].any(axis=1)].T)])
|
|
155
|
+
keep = np.isin(lbl[tuple(ijk.T)], sup)
|
|
156
|
+
if not keep.all():
|
|
157
|
+
ijk = ijk[keep]
|
|
158
|
+
hm = HexMesh(ijk, pitch, lo)
|
|
159
|
+
bc = boundary_conditions(study, hm, check_loads=False, tol_floor=0.5 * pitch)
|
|
160
|
+
fs = np.stack([np.clip(f[tuple(ijk.T)], 0.0, 1.0) for f in fs_lv], axis=1) # (ne, levels)
|
|
161
|
+
fs[:, solid_lv] = 1.0
|
|
162
|
+
centers = hm.elem_centers()
|
|
163
|
+
collar = np.zeros(hm.n_elem, bool)
|
|
164
|
+
if o.collar > 0:
|
|
165
|
+
collar = _collar_distance(study, centers, o.collar) < 0
|
|
166
|
+
# the stiffness of each cell at each level, as a share of solid
|
|
167
|
+
e_lv = fs + (1.0 - fs) * emod_l[None, :]
|
|
168
|
+
|
|
169
|
+
stiff = Stiffness(hm, mat.nu, bc.fixed)
|
|
170
|
+
solver = Solver(hm, rtol=1e-6, maxiter=3000)
|
|
171
|
+
contact = bc.support_nodes.copy()
|
|
172
|
+
for ln in bc.case_load_nodes:
|
|
173
|
+
contact |= ln
|
|
174
|
+
band = 1.5 * pitch
|
|
175
|
+
near = cKDTree(hm.node_xyz[contact]).query(centers, k=1, distance_upper_bound=band)[0] < np.inf
|
|
176
|
+
fg = gravity_force(hm, np.ones(hm.n_elem), bc.gravity, bc.fixed) if bc.gravity is not None else 0.0
|
|
177
|
+
loads = [bc.loads[ci] + fg for ci in range(len(study.load_cases))]
|
|
178
|
+
allow0 = mat.strength / (o.safety_factor * STRESS_ALLOWANCE)
|
|
179
|
+
rows = np.arange(hm.n_elem)
|
|
180
|
+
|
|
181
|
+
last = [None]
|
|
182
|
+
|
|
183
|
+
def analyze(lev):
|
|
184
|
+
e = e_lv[rows, lev]
|
|
185
|
+
k = stiff.assemble(mat.E * e)
|
|
186
|
+
if bc.springs is not None:
|
|
187
|
+
k = (k + mat.E * bc.springs).tocsr()
|
|
188
|
+
solver.invalidate() # a new stiffness contrast wants a new hierarchy
|
|
189
|
+
us = solver.solve_many(k, loads, last[0])
|
|
190
|
+
last[0] = us
|
|
191
|
+
se = np.max([von_mises(stiff.stress(u, mat.E)) for u in us], axis=0)
|
|
192
|
+
defl = []
|
|
193
|
+
for ci, u in enumerate(us):
|
|
194
|
+
ln = bc.case_load_nodes[ci]
|
|
195
|
+
d = u.reshape(-1, 3)
|
|
196
|
+
defl.append(float(np.linalg.norm(d[ln].mean(axis=0))) if ln.any() else
|
|
197
|
+
float(np.linalg.norm(d, axis=1).max()))
|
|
198
|
+
# the skin carries the strain at full modulus; the infill's walls
|
|
199
|
+
# carry A rho^-m times its macro stress (E* / E of the strain stress)
|
|
200
|
+
skin_s = np.where(fs[rows, lev] > 0, se, 0.0)
|
|
201
|
+
inf_s = np.where(fs[rows, lev] < 1, wall_l[lev] * emod_l[lev] * se / o.knockdown, 0.0)
|
|
202
|
+
peak = float(np.max(np.where(near, 0.0, np.maximum(skin_s, inf_s))))
|
|
203
|
+
return se, defl, peak
|
|
204
|
+
|
|
205
|
+
def report_sf(peak):
|
|
206
|
+
return mat.strength / (STRESS_ALLOWANCE * peak) if peak > 0 else float("inf")
|
|
207
|
+
|
|
208
|
+
# the solid part on the same grid: what the graded one is compared with
|
|
209
|
+
se_solid, defl_solid, peak_solid = analyze(np.full(hm.n_elem, solid_lv))
|
|
210
|
+
sf_solid = report_sf(peak_solid)
|
|
211
|
+
ver_sf = (verified or {}).get("safetyFactor")
|
|
212
|
+
ver_defl = [c.get("deflection") for c in (verified or {}).get("cases", [])]
|
|
213
|
+
target_defl = study.goal.max_deflection
|
|
214
|
+
|
|
215
|
+
def on_verified_scale(sf_pred):
|
|
216
|
+
# the graded part's safety factor on the verified part's scale
|
|
217
|
+
return ver_sf * sf_pred / sf_solid if ver_sf is not None else sf_pred
|
|
218
|
+
|
|
219
|
+
# Start from the solid part's strain and let each cell find its level;
|
|
220
|
+
# the allowable then moves (geometrically damped) until the graded part
|
|
221
|
+
# reads the target, as the load shifts onto what stayed stiff. Each
|
|
222
|
+
# cell's macro stress is held while it picks a level (fully stressed
|
|
223
|
+
# design, as lattice.py sizes TPMS): at a fixed strain a denser gyroid's
|
|
224
|
+
# walls carry more, never less, so a strain-held pick only ever chose
|
|
225
|
+
# the sparsest level or solid.
|
|
226
|
+
lev = np.full(hm.n_elem, solid_lv)
|
|
227
|
+
se = se_solid
|
|
228
|
+
floor = 0
|
|
229
|
+
history = []
|
|
230
|
+
allow = allow0 * (sf_solid / ver_sf) if ver_sf is not None else allow0
|
|
231
|
+
grid = np.zeros(active.shape)
|
|
232
|
+
best = None # (fill, levels, se, defl, peak) of the sparsest passing pass
|
|
233
|
+
short = 1.0
|
|
234
|
+
prev = lev
|
|
235
|
+
for it in range(o.iterations):
|
|
236
|
+
sig = (se * e_lv[rows, lev])[:, None] # macro stress, MPa
|
|
237
|
+
skin_s = sig / e_lv # strain-equivalent at each level
|
|
238
|
+
inf_s = wall_l[None, :] * emod_l[None, :] * skin_s / o.knockdown
|
|
239
|
+
ok = (np.where(fs < 1, inf_s, 0.0) <= allow) & (np.where(fs > 0, skin_s, 0.0) <= allow)
|
|
240
|
+
ok[:, solid_lv] = True
|
|
241
|
+
need = np.argmax(ok, axis=1)
|
|
242
|
+
# a concentration spreads over a cell before the slicer's regions can
|
|
243
|
+
# follow it
|
|
244
|
+
grid[:] = 0
|
|
245
|
+
grid[tuple(ijk.T)] = need
|
|
246
|
+
need = ndimage.maximum_filter(grid, size=3)[tuple(ijk.T)].astype(int)
|
|
247
|
+
# hysteresis, or levels flicker: after a pass under the target only
|
|
248
|
+
# raise levels; after one that held it, drop at most one
|
|
249
|
+
if it == 0:
|
|
250
|
+
new = need
|
|
251
|
+
elif short < 1.0:
|
|
252
|
+
new = np.maximum(need, lev)
|
|
253
|
+
else:
|
|
254
|
+
new = np.maximum(need, lev - 1)
|
|
255
|
+
new = np.maximum(new, floor)
|
|
256
|
+
new[collar] = solid_lv
|
|
257
|
+
se, defl, peak = analyze(new)
|
|
258
|
+
lev = new
|
|
259
|
+
sf_pred = report_sf(peak)
|
|
260
|
+
short = on_verified_scale(sf_pred) / o.safety_factor
|
|
261
|
+
ratio_d = [d / max(ds, 1e-12) for d, ds in zip(defl, defl_solid)]
|
|
262
|
+
stiff_ok = not (target_defl and ver_defl) or \
|
|
263
|
+
max(v * rr for v, rr in zip(ver_defl, ratio_d) if v is not None) <= target_defl
|
|
264
|
+
fill = float(np.mean(fs[rows, lev] + (1 - fs[rows, lev]) * rho_l[lev]))
|
|
265
|
+
history.append({"iteration": it, "predictedSF": on_verified_scale(sf_pred), "floor": floor,
|
|
266
|
+
"fill": fill, "solidCells": float(np.mean(lev == solid_lv))})
|
|
267
|
+
if on_progress is not None:
|
|
268
|
+
on_progress({"stage": "grade", **history[-1]}, None)
|
|
269
|
+
if short >= 1.0 and stiff_ok and (best is None or fill < best[0]):
|
|
270
|
+
best = (fill, lev.copy(), se, defl, peak)
|
|
271
|
+
if not stiff_ok:
|
|
272
|
+
if floor >= solid_lv:
|
|
273
|
+
break
|
|
274
|
+
floor += 1
|
|
275
|
+
continue
|
|
276
|
+
if 1.0 <= short < 1.06:
|
|
277
|
+
break
|
|
278
|
+
allow *= float(np.clip(short, 0.8, 0.99)) if short < 1.0 else float(np.clip(short, 1.0, 2.0)) ** 0.75
|
|
279
|
+
prev = lev
|
|
280
|
+
if best is None:
|
|
281
|
+
# nothing graded held the target: solid, as verified
|
|
282
|
+
lev = np.full(hm.n_elem, solid_lv)
|
|
283
|
+
se, defl, peak = se_solid, defl_solid, peak_solid
|
|
284
|
+
else:
|
|
285
|
+
_, lev, se, defl, peak = best
|
|
286
|
+
# reported on the verified part's scale
|
|
287
|
+
knock = sf_solid / report_sf(peak) if peak > 0 else 1.0
|
|
288
|
+
sf = ver_sf / knock if ver_sf is not None else report_sf(peak)
|
|
289
|
+
ratio_d = [d / max(ds, 1e-12) for d, ds in zip(defl, defl_solid)]
|
|
290
|
+
|
|
291
|
+
cell_vol = pitch ** 3 * f_in[tuple(ijk.T)]
|
|
292
|
+
fill = fs[rows, lev] + (1.0 - fs[rows, lev]) * rho_l[lev]
|
|
293
|
+
est_volume = float((cell_vol * fill).sum())
|
|
294
|
+
lv_grid = np.full(active.shape, -1, int)
|
|
295
|
+
lv_grid[tuple(ijk.T)] = lev
|
|
296
|
+
base = int(lev[~collar].min()) if (~collar).any() else solid_lv
|
|
297
|
+
modifiers = _modifier_meshes(lv_grid, lo, pitch, base, n_lv, o)
|
|
298
|
+
usage = {(_label(o, i)): float(cell_vol[lev == i].sum() / cell_vol.sum()) for i in range(n_lv)
|
|
299
|
+
if (lev == i).any()}
|
|
300
|
+
obj = _settings(o, base)
|
|
301
|
+
skin_t = skin_l[base]
|
|
302
|
+
obj.update(top_solid_min_thickness=f"{skin_t:.2f}", bottom_solid_min_thickness=f"{skin_t:.2f}")
|
|
303
|
+
return {
|
|
304
|
+
"levels": [{"infill": float(rho_l[i] * 100), "perimeters": (o.levels[i][1] if i < solid_lv else None),
|
|
305
|
+
"label": _label(o, i)} for i in range(n_lv)],
|
|
306
|
+
"base": _label(o, base),
|
|
307
|
+
"volumeShare": usage,
|
|
308
|
+
"objectSettings": obj,
|
|
309
|
+
"modifiers": modifiers,
|
|
310
|
+
"safetyFactor": sf,
|
|
311
|
+
"targetSafetyFactor": o.safety_factor,
|
|
312
|
+
"solidSafetyFactor": ver_sf,
|
|
313
|
+
"knockdown": knock,
|
|
314
|
+
"deflectionRatio": ratio_d,
|
|
315
|
+
"deflection": [v * rr for v, rr in zip(ver_defl, ratio_d)] if ver_defl else None,
|
|
316
|
+
"estimatedVolume": est_volume,
|
|
317
|
+
"estimatedMass": est_volume * mat.density / 1000.0,
|
|
318
|
+
"analysisPitch": pitch,
|
|
319
|
+
"analysisCells": int(hm.n_elem),
|
|
320
|
+
"predicted": {"solid": sf_solid, "graded": report_sf(peak)},
|
|
321
|
+
"history": history,
|
|
322
|
+
"assumptions": {"pattern": "gyroid", "sheet": SHEETS["gyroid"], "knockdown": o.knockdown,
|
|
323
|
+
"lineWidth": o.line_width, "collar": o.collar},
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _label(o: GradeOptions, i: int) -> str:
|
|
328
|
+
if i >= len(o.levels):
|
|
329
|
+
return "solid"
|
|
330
|
+
d, p = o.levels[i]
|
|
331
|
+
return f"{d:g}% x{p}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _settings(o: GradeOptions, i: int, walls: bool = True) -> dict:
|
|
335
|
+
"""PrusaSlicer keys for level i; `walls` False leaves the perimeters to
|
|
336
|
+
the object (a modifier that keeps the object's count)."""
|
|
337
|
+
if i >= len(o.levels):
|
|
338
|
+
out = {"fill_density": "100%", "fill_pattern": "rectilinear", "perimeters": str(o.levels[-1][1])}
|
|
339
|
+
else:
|
|
340
|
+
d, p = o.levels[i]
|
|
341
|
+
out = {"fill_density": f"{d:g}%", "fill_pattern": "gyroid", "perimeters": str(p)}
|
|
342
|
+
if not walls:
|
|
343
|
+
out.pop("perimeters")
|
|
344
|
+
return out
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _modifier_meshes(lv_grid, lo, pitch, base, n_lv, o: GradeOptions) -> list:
|
|
348
|
+
"""One closed mesh per level above the base, around every cell at that
|
|
349
|
+
level or above (nested: later, denser modifiers win in the slicer), a
|
|
350
|
+
cell larger all round so a region is never short of what it needs."""
|
|
351
|
+
import trimesh
|
|
352
|
+
from skimage.measure import marching_cubes
|
|
353
|
+
|
|
354
|
+
out = []
|
|
355
|
+
for i in range(base + 1, n_lv):
|
|
356
|
+
m = lv_grid >= i
|
|
357
|
+
if not (lv_grid == i).any():
|
|
358
|
+
continue # the same region as the next level up
|
|
359
|
+
m = ndimage.binary_dilation(m, iterations=1)
|
|
360
|
+
f = ndimage.gaussian_filter(np.pad(m, 2).astype(np.float32), 0.7)
|
|
361
|
+
verts, faces, _, _ = marching_cubes(f, 0.5, spacing=(pitch,) * 3)
|
|
362
|
+
verts = verts + lo - 2.0 * pitch + 0.5 * pitch
|
|
363
|
+
tm = trimesh.Trimesh(verts, faces[:, ::-1], process=True)
|
|
364
|
+
if len(tm.faces) > o.modifier_faces:
|
|
365
|
+
try:
|
|
366
|
+
import fast_simplification
|
|
367
|
+
|
|
368
|
+
v2, f2 = fast_simplification.simplify(tm.vertices, tm.faces, 1.0 - o.modifier_faces / len(tm.faces))
|
|
369
|
+
tm = trimesh.Trimesh(v2, f2, process=True)
|
|
370
|
+
except ImportError:
|
|
371
|
+
pass
|
|
372
|
+
if tm.volume < 0:
|
|
373
|
+
tm.invert()
|
|
374
|
+
walls = o.levels[min(i, len(o.levels) - 1)][1] != o.levels[min(base, len(o.levels) - 1)][1]
|
|
375
|
+
out.append({"name": f"loadpath {_label(o, i)}", "mesh": tm, "settings": _settings(o, i, walls)})
|
|
376
|
+
return out
|