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/orient.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Which way up to print it.
|
|
3
|
+
|
|
4
|
+
Orientation decides three things on a layered printer, and they pull
|
|
5
|
+
against each other:
|
|
6
|
+
|
|
7
|
+
strength - FDM layers bond at 50-70% of the in-plane strength, so a part
|
|
8
|
+
whose main stress runs across its layers is a weaker part.
|
|
9
|
+
supports - every overhang past 45 degrees costs support material, time
|
|
10
|
+
and a scarred surface.
|
|
11
|
+
the bed - the first layer has to hold: enough contact, no tipping.
|
|
12
|
+
|
|
13
|
+
Two entry points, for the two moments the choice is made:
|
|
14
|
+
|
|
15
|
+
choose_build_direction(study)
|
|
16
|
+
Before designing. Designs the study for each candidate direction on a
|
|
17
|
+
coarse grid at a fixed volume fraction, each with that direction's
|
|
18
|
+
overhang control, by successive halving (all of them half way and
|
|
19
|
+
checked cheaply, the leaders finished), and keeps the direction whose
|
|
20
|
+
verified safety factor per gram (layered stiffness and interlayer bond
|
|
21
|
+
included) is highest; support volume and height break ties. Because the
|
|
22
|
+
design is grown for its direction it is self-supporting in all of them,
|
|
23
|
+
so this is mostly a strength choice: which way can material carry the
|
|
24
|
+
loads in its layers' plane. A study opts in with
|
|
25
|
+
"process": {"type": "fdm", "buildDirection": "auto"}.
|
|
26
|
+
|
|
27
|
+
rank_orientations(mesh, study=None)
|
|
28
|
+
After designing, or for any mesh. Tries the six axis directions and the
|
|
29
|
+
part's own large flat faces (convex hull facets, the ways it can rest on
|
|
30
|
+
the bed), slices each (printcheck.check_print) and, given the study, reads
|
|
31
|
+
the interlayer tension each direction would see from one stress solve.
|
|
32
|
+
Ranks by: meets the interlayer safety factor, then support volume,
|
|
33
|
+
height and bed risk.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import dataclasses
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
|
|
41
|
+
from .am import build_vector, direction_label, parse_build_direction
|
|
42
|
+
from .printcheck import check_print
|
|
43
|
+
|
|
44
|
+
__all__ = ["choose_build_direction", "rank_orientations", "AXIS_DIRECTIONS"]
|
|
45
|
+
|
|
46
|
+
AXIS_DIRECTIONS = [(2, 1), (2, -1), (0, 1), (0, -1), (1, 1), (1, -1)]
|
|
47
|
+
# Coarse comparison budget per direction (cells) and the fraction tried
|
|
48
|
+
# when the goal has none of its own.
|
|
49
|
+
_COMPARE_CELLS = 12_000
|
|
50
|
+
_COMPARE_VF = 0.25
|
|
51
|
+
# Successive halving (choose_build_direction): every direction runs this
|
|
52
|
+
# many iterations and is checked on a grid of _SCREEN_CELLS without
|
|
53
|
+
# hotspot refinement; at most _FINALISTS go on to the full count, and none
|
|
54
|
+
# below _FINALIST_FLOOR of the leader's strength per gram.
|
|
55
|
+
_SCREEN_ITERATIONS = 40
|
|
56
|
+
_SCREEN_CELLS = 15_000
|
|
57
|
+
_FINALISTS = 3
|
|
58
|
+
_FINALIST_FLOOR = 0.35
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _cost(pc: dict) -> float:
|
|
62
|
+
"""Print cost of one orientation, dimensionless: support as a fraction of
|
|
63
|
+
the part's volume, height over the part's size (time), bed risks."""
|
|
64
|
+
vol = max(pc["volume"], 1e-9)
|
|
65
|
+
size = vol ** (1.0 / 3.0)
|
|
66
|
+
bed = pc["bed"]
|
|
67
|
+
c = pc["supportVolume"] / vol + 0.15 * pc["height"] / size
|
|
68
|
+
if bed["comMargin"] < 0:
|
|
69
|
+
c += 1.0
|
|
70
|
+
if bed["feet"] and bed["smallestFoot"] < 9.0:
|
|
71
|
+
c += 0.2
|
|
72
|
+
c += 0.1 * max(0.0, bed["aspect"] - 3.0)
|
|
73
|
+
return float(c)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _stress_nodes(study, mesh, max_cells=60_000):
|
|
77
|
+
"""Nodal stress tensors of the delivered part, per load case, from one
|
|
78
|
+
isotropic voxel solve (as verify_part runs it)."""
|
|
79
|
+
from scipy import ndimage
|
|
80
|
+
|
|
81
|
+
from .domain import boundary_conditions, gravity_force
|
|
82
|
+
from .fem import HexMesh, Solver, Stiffness
|
|
83
|
+
from .verify import nodal_stress, voxelize
|
|
84
|
+
|
|
85
|
+
mat = study.material
|
|
86
|
+
vol = float(np.prod(mesh.extents))
|
|
87
|
+
pitch = (vol / max_cells) ** (1.0 / 3.0)
|
|
88
|
+
solid, origin = voxelize(mesh, pitch)
|
|
89
|
+
lbl, _ = ndimage.label(solid, structure=np.ones((3, 3, 3), bool))
|
|
90
|
+
ijk = np.argwhere(solid)
|
|
91
|
+
hm = HexMesh(ijk, pitch, origin)
|
|
92
|
+
bc = boundary_conditions(study, hm, check_loads=False, tol_floor=pitch)
|
|
93
|
+
keep_lbl = np.unique(lbl[tuple(ijk[bc.support_nodes[hm.enode].any(axis=1)].T)])
|
|
94
|
+
keep = np.isin(lbl[tuple(ijk.T)], keep_lbl)
|
|
95
|
+
if not keep.all():
|
|
96
|
+
hm = HexMesh(ijk[keep], pitch, origin)
|
|
97
|
+
bc = boundary_conditions(study, hm, check_loads=False, tol_floor=pitch)
|
|
98
|
+
st = Stiffness(hm, mat.nu, bc.fixed)
|
|
99
|
+
k = st.assemble(np.full(hm.n_elem, mat.E))
|
|
100
|
+
if bc.springs is not None:
|
|
101
|
+
k = (k + mat.E * bc.springs).tocsr()
|
|
102
|
+
fg = gravity_force(hm, np.ones(hm.n_elem), bc.gravity, bc.fixed) if bc.gravity is not None else 0.0
|
|
103
|
+
us = Solver(hm, rtol=1e-6, maxiter=3000).solve_many(k, [f + fg for f in bc.loads])
|
|
104
|
+
from scipy.spatial import cKDTree
|
|
105
|
+
contact = np.zeros(hm.n_nodes, bool)
|
|
106
|
+
contact |= bc.support_nodes
|
|
107
|
+
for ln in bc.case_load_nodes:
|
|
108
|
+
contact |= ln
|
|
109
|
+
band = 2.0 * pitch
|
|
110
|
+
body = cKDTree(hm.node_xyz[contact]).query(hm.node_xyz, k=1, distance_upper_bound=band)[0] == np.inf
|
|
111
|
+
return [nodal_stress(st, u, mat.E)[body] for u in us]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _normal_stress(sig, n):
|
|
115
|
+
return (sig[:, :3] @ (n * n)
|
|
116
|
+
+ 2.0 * (sig[:, 3] * n[0] * n[1] + sig[:, 4] * n[1] * n[2] + sig[:, 5] * n[2] * n[0]))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _facet_directions(mesh, count=8, min_area_frac=0.03):
|
|
120
|
+
"""Build directions that put a large flat face of the convex hull on the
|
|
121
|
+
bed (up = minus that face's outward normal)."""
|
|
122
|
+
hull = mesh.convex_hull
|
|
123
|
+
normals = hull.face_normals
|
|
124
|
+
areas = hull.area_faces
|
|
125
|
+
# merge coplanar hull triangles by rounded normal
|
|
126
|
+
key = np.round(normals, 3)
|
|
127
|
+
uniq, inv = np.unique(key, axis=0, return_inverse=True)
|
|
128
|
+
tot = np.bincount(inv.ravel(), weights=areas)
|
|
129
|
+
order = np.argsort(-tot)
|
|
130
|
+
out = []
|
|
131
|
+
for i in order[:count]:
|
|
132
|
+
if tot[i] < min_area_frac * hull.area:
|
|
133
|
+
break
|
|
134
|
+
n = uniq[i] / np.linalg.norm(uniq[i])
|
|
135
|
+
out.append(parse_build_direction(list(-n)))
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _same(a, b):
|
|
140
|
+
return float(build_vector(a) @ build_vector(b)) > 1 - 1e-6
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def rank_orientations(mesh, study=None, candidates=None, process: str = "fdm", interlayer: float | None = None,
|
|
144
|
+
safety_factor: float | None = None, pitch: float | None = None,
|
|
145
|
+
stress_cells: int = 60_000) -> list[dict]:
|
|
146
|
+
"""Every candidate build direction with its print check, interlayer
|
|
147
|
+
reading and cost, best first. `study` (optional) supplies loads,
|
|
148
|
+
material, interlayer ratio and goal; without it only printability is
|
|
149
|
+
ranked."""
|
|
150
|
+
cands = list(candidates) if candidates is not None else AXIS_DIRECTIONS + _facet_directions(mesh)
|
|
151
|
+
uniq = []
|
|
152
|
+
for c in cands:
|
|
153
|
+
c = parse_build_direction(c) if not isinstance(c, tuple) or len(c) not in (2, 3) else c
|
|
154
|
+
if not any(_same(c, u) for u in uniq):
|
|
155
|
+
uniq.append(c)
|
|
156
|
+
sigs = None
|
|
157
|
+
mat = None
|
|
158
|
+
if study is not None:
|
|
159
|
+
mat = study.material
|
|
160
|
+
proc = study.process
|
|
161
|
+
if proc is not None:
|
|
162
|
+
process = proc.kind
|
|
163
|
+
interlayer = proc.interlayer if interlayer is None else interlayer
|
|
164
|
+
safety_factor = safety_factor or study.goal.safety_factor
|
|
165
|
+
if study.load_cases:
|
|
166
|
+
sigs = _stress_nodes(study, mesh, stress_cells)
|
|
167
|
+
from .verify import STRESS_ALLOWANCE
|
|
168
|
+
|
|
169
|
+
rows = []
|
|
170
|
+
for c in uniq:
|
|
171
|
+
pc = check_print(mesh, c, process=process, pitch=pitch, material=mat)
|
|
172
|
+
row = {"direction": direction_label(c), "build": c, "cost": _cost(pc), "print": pc}
|
|
173
|
+
if sigs is not None and interlayer is not None and interlayer < 1.0:
|
|
174
|
+
n = build_vector(c)
|
|
175
|
+
tens = max(float(np.maximum(_normal_stress(s, n), 0.0).max(initial=0.0)) for s in sigs)
|
|
176
|
+
row["interlayerStress"] = tens
|
|
177
|
+
row["interlayerSafetyFactor"] = (interlayer * mat.strength / (tens * STRESS_ALLOWANCE)
|
|
178
|
+
if tens > 0 else float("inf"))
|
|
179
|
+
rows.append(row)
|
|
180
|
+
sf_ok = [r.get("interlayerSafetyFactor", np.inf) >= (safety_factor or 0.0) for r in rows]
|
|
181
|
+
for r, ok in zip(rows, sf_ok):
|
|
182
|
+
r["meetsInterlayer"] = bool(ok)
|
|
183
|
+
rows.sort(key=lambda r: (not r["meetsInterlayer"], r["cost"],
|
|
184
|
+
-r.get("interlayerSafetyFactor", 0.0)))
|
|
185
|
+
return rows
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _candidate(args):
|
|
189
|
+
"""One build direction's design (a process pool worker; module level so
|
|
190
|
+
it pickles): (study, x0, beta0, iterations, verify_cells, refine)."""
|
|
191
|
+
from .domain import build_domain
|
|
192
|
+
from .simp import filter_radius
|
|
193
|
+
from .study import design_at
|
|
194
|
+
|
|
195
|
+
st, x0, beta0, iters, cells, refine = args
|
|
196
|
+
domain = build_domain(st)
|
|
197
|
+
o = design_at(st, domain, filter_radius(domain, st.length_scale, st.min_hole), float(st.goal.volume_fraction),
|
|
198
|
+
x0=x0, beta0=beta0, max_iterations=iters, verify_cells=cells, refine=refine)
|
|
199
|
+
o.xphys = None # not needed back; keep the pickle small
|
|
200
|
+
return o
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _row(c, o, proc, study):
|
|
204
|
+
rep = o.report
|
|
205
|
+
pc = check_print(o.mesh, c, process=proc.kind, material=study.material, bridge_span=proc.bridge_span)
|
|
206
|
+
inter = [cs.get("interlayerStress") for cs in rep.get("cases", []) if cs.get("interlayerStress") is not None]
|
|
207
|
+
return {"direction": direction_label(c), "build": c, "safetyFactor": rep["safetyFactor"],
|
|
208
|
+
"mass": rep["mass"], "interlayerStress": max(inter) if inter else None,
|
|
209
|
+
"supportVolume": pc["supportVolume"], "supportArea": pc["supportArea"],
|
|
210
|
+
"height": pc["height"], "bed": pc["bed"], "cost": _cost(pc),
|
|
211
|
+
"efficiency": rep["safetyFactor"] / max(rep["mass"], 1e-9)}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _run_all(jobs, workers):
|
|
215
|
+
if workers <= 1 or len(jobs) == 1:
|
|
216
|
+
return [_candidate(j) for j in jobs]
|
|
217
|
+
import multiprocessing as mp
|
|
218
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
219
|
+
with ProcessPoolExecutor(min(workers, len(jobs)), mp_context=mp.get_context("spawn")) as ex:
|
|
220
|
+
return list(ex.map(_candidate, jobs))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def choose_build_direction(study, candidates=None, max_elements: int = _COMPARE_CELLS, on_progress=None,
|
|
224
|
+
exhaustive: bool = False) -> dict:
|
|
225
|
+
"""Compare build directions for `study` on a coarse grid (see the module
|
|
226
|
+
note). Returns {"study": the study with the winner set as its build
|
|
227
|
+
direction, "chosen": label, "candidates": [{direction, safetyFactor,
|
|
228
|
+
mass, supportVolume, ...}]}. The ranking is stored on the returned study
|
|
229
|
+
too (study.raw["orientation"]).
|
|
230
|
+
|
|
231
|
+
With overhang control, by successive halving: every direction is
|
|
232
|
+
designed for _SCREEN_ITERATIONS and checked cheaply (a coarser analysis
|
|
233
|
+
grid, no hotspot refinement); only the best few by strength per gram
|
|
234
|
+
(_FINALISTS) are carried on from where they stopped to the full count
|
|
235
|
+
and verified in full. On the PETG arm link at 12k cells the half-way
|
|
236
|
+
reading put the full search's winner (-x, SF 10.3 against 8.8 for the
|
|
237
|
+
runner-up) first by a factor of two; the full search, every direction
|
|
238
|
+
to 80 iterations, took 44 minutes on 4 cores. (Two cheaper screens did
|
|
239
|
+
not hold up: stress read off one shared unconstrained design ranked -x
|
|
240
|
+
among the worst, since the overhang filter reshapes each direction's
|
|
241
|
+
part; and a grid of an eighth of the cells lost the 4 mm bearing ring.)
|
|
242
|
+
|
|
243
|
+
Without overhang control (and with isotropic design stiffness) every
|
|
244
|
+
direction's design is the same one, so it is designed once and verified
|
|
245
|
+
once per build axis (opposite directions load the layers alike).
|
|
246
|
+
|
|
247
|
+
`exhaustive=True` runs every direction to the full count."""
|
|
248
|
+
from .domain import build_domain
|
|
249
|
+
from .simp import filter_radius
|
|
250
|
+
from .spec import Goal
|
|
251
|
+
from .study import _COARSE_MIN_CELLS, _WARM_BETA, _coarse_start, _workers, design_at
|
|
252
|
+
|
|
253
|
+
proc = study.process
|
|
254
|
+
if proc is None:
|
|
255
|
+
raise ValueError("choose_build_direction needs a layered process (\"process\": {\"type\": \"fdm\"})")
|
|
256
|
+
cands = [parse_build_direction(c) if not isinstance(c, tuple) else c for c in (candidates or AXIS_DIRECTIONS)]
|
|
257
|
+
vf = float(study.goal.volume_fraction or _COMPARE_VF)
|
|
258
|
+
|
|
259
|
+
def variant(c, coarse=True):
|
|
260
|
+
p = dataclasses.replace(proc, build=c)
|
|
261
|
+
kw = dict(process=p, print_direction=c if proc.overhangs else study.print_direction)
|
|
262
|
+
if coarse:
|
|
263
|
+
kw.update(goal=Goal(volume_fraction=vf), materials=[study.material],
|
|
264
|
+
max_elements=min(study.max_elements, max_elements), pitch=None,
|
|
265
|
+
max_iterations=min(study.max_iterations, 80))
|
|
266
|
+
return dataclasses.replace(study, **kw)
|
|
267
|
+
|
|
268
|
+
def emit(ev):
|
|
269
|
+
if on_progress is not None:
|
|
270
|
+
on_progress({"stage": "orient", **ev}, None)
|
|
271
|
+
|
|
272
|
+
emit({"candidates": [direction_label(c) for c in cands]})
|
|
273
|
+
rows = []
|
|
274
|
+
if not proc.overhangs and not proc.anisotropic:
|
|
275
|
+
# one design serves every direction: verify it once per build axis
|
|
276
|
+
# and print-check each
|
|
277
|
+
from .verify import verify_part
|
|
278
|
+
|
|
279
|
+
st0 = variant(cands[0])
|
|
280
|
+
domain = build_domain(st0)
|
|
281
|
+
radius = filter_radius(domain, st0.length_scale, st0.min_hole)
|
|
282
|
+
big = domain.n_elem >= _COARSE_MIN_CELLS
|
|
283
|
+
x0 = _coarse_start(st0, domain, vf, radius, None) if big else None
|
|
284
|
+
shared = design_at(st0, domain, radius, vf, x0=x0, beta0=_WARM_BETA if big else 1.0, verify=False)
|
|
285
|
+
by_axis = {}
|
|
286
|
+
for c in cands:
|
|
287
|
+
key = tuple(np.round(np.abs(build_vector(c)), 6))
|
|
288
|
+
if key not in by_axis:
|
|
289
|
+
by_axis[key] = verify_part(variant(c), shared.mesh, min_pitch=0.5 * domain.pitch,
|
|
290
|
+
contact_band=domain.pitch)
|
|
291
|
+
rows.append(_row(c, dataclasses.replace(shared, report=by_axis[key]), proc, study) | {"designed": True})
|
|
292
|
+
else:
|
|
293
|
+
full = min(study.max_iterations, 80)
|
|
294
|
+
workers = _workers(len(cands), cells=min(study.max_elements, max_elements))
|
|
295
|
+
if exhaustive or full <= _SCREEN_ITERATIONS:
|
|
296
|
+
outs = _run_all([(variant(c), None, 1.0, full, 60_000, 3) for c in cands], workers)
|
|
297
|
+
rows = [_row(c, o, proc, study) | {"designed": True} for c, o in zip(cands, outs)]
|
|
298
|
+
else:
|
|
299
|
+
first = _run_all([(variant(c), None, 1.0, _SCREEN_ITERATIONS, _SCREEN_CELLS, 0) for c in cands],
|
|
300
|
+
workers)
|
|
301
|
+
screen = [_row(c, o, proc, study) for c, o in zip(cands, first)]
|
|
302
|
+
emit({"screen": [(r["direction"], round(r["safetyFactor"], 2)) for r in screen]})
|
|
303
|
+
lead = max(r["efficiency"] for r in screen)
|
|
304
|
+
order = sorted(range(len(cands)), key=lambda i: -screen[i]["efficiency"])
|
|
305
|
+
go = [i for i in order[:_FINALISTS] if screen[i]["efficiency"] >= _FINALIST_FLOOR * lead]
|
|
306
|
+
# carry each finalist on from where it stopped: its design and
|
|
307
|
+
# its projection sharpness
|
|
308
|
+
jobs = [(variant(cands[i]), first[i].x, first[i].optimizer.get("beta", _WARM_BETA),
|
|
309
|
+
full - _SCREEN_ITERATIONS, 60_000, 3) for i in go]
|
|
310
|
+
final = dict(zip(go, _run_all(jobs, workers)))
|
|
311
|
+
for i, c in enumerate(cands):
|
|
312
|
+
half = {"screenSafetyFactor": screen[i]["safetyFactor"], "screenMass": screen[i]["mass"]}
|
|
313
|
+
if i in final:
|
|
314
|
+
rows.append(_row(c, final[i], proc, study) | {"designed": True} | half)
|
|
315
|
+
else:
|
|
316
|
+
rows.append({k: v for k, v in screen[i].items() if k not in ("safetyFactor", "efficiency")}
|
|
317
|
+
| {"designed": False, "safetyFactor": None} | half)
|
|
318
|
+
designed = [r for r in rows if r["designed"]]
|
|
319
|
+
if not designed:
|
|
320
|
+
raise RuntimeError("no build direction produced a design")
|
|
321
|
+
# strength per gram decides; within 5% of the best, print cost does
|
|
322
|
+
top = max(r["efficiency"] for r in designed)
|
|
323
|
+
designed.sort(key=lambda r: (r["efficiency"] < 0.95 * top, r["cost"], -r["efficiency"]))
|
|
324
|
+
dropped = sorted((r for r in rows if not r["designed"]), key=lambda r: -r["screenSafetyFactor"])
|
|
325
|
+
rows = designed + dropped
|
|
326
|
+
best = rows[0]
|
|
327
|
+
chosen = variant(best["build"], coarse=False)
|
|
328
|
+
raw = dict(chosen.raw)
|
|
329
|
+
raw["orientation"] = [{k: v for k, v in r.items() if k != "build"} for r in rows]
|
|
330
|
+
chosen = dataclasses.replace(chosen, raw=raw)
|
|
331
|
+
emit({"chosen": best["direction"],
|
|
332
|
+
"ranking": [(r["direction"], round(r["safetyFactor"], 2)) for r in designed],
|
|
333
|
+
"dropped": [r["direction"] for r in dropped]})
|
|
334
|
+
return {"study": chosen, "chosen": best["direction"], "candidates": raw["orientation"]}
|