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,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The interface library: how strongly two printed materials hold to each
|
|
3
|
+
other, every number with its source.
|
|
4
|
+
|
|
5
|
+
A multi-material study (an AMS FDM print of PETG and TPU, a PolyJet part of
|
|
6
|
+
Vero and Agilus30) has a boundary where one material meets the other. That
|
|
7
|
+
boundary is usually the weakest place in the part, and the optimizer checks
|
|
8
|
+
the stress across it against these numbers. Same-material boundaries are not
|
|
9
|
+
here: a material's own across-layer strength (material_library strengthZ,
|
|
10
|
+
or the process interlayer factor) covers them.
|
|
11
|
+
|
|
12
|
+
Each value is (MPa, source id, note), as in material_library.py; `SOURCES`
|
|
13
|
+
says what each id is and where to read it. The papers were read on
|
|
14
|
+
2026-10-31.
|
|
15
|
+
|
|
16
|
+
Two orientations. "tensile"/"shear" are for a boundary inside a layer (the
|
|
17
|
+
two materials side by side, the nozzle switching within each layer);
|
|
18
|
+
"tensileZ"/"shearZ" are for a boundary between layers (one material printed
|
|
19
|
+
on top of the other). A stacked boundary is the weaker one in the FDM data:
|
|
20
|
+
Xue et al. 2022 found the in-layer boundary gets a micro zig-zag interlock
|
|
21
|
+
from each extruder switch, and the stacked one is a smooth plane. Where a
|
|
22
|
+
source measured only one orientation, the other takes the same number
|
|
23
|
+
(source "estimate").
|
|
24
|
+
|
|
25
|
+
How a number is chosen. The lowest reported configuration that a slicer
|
|
26
|
+
produces by default: a flat butt joint, no designed interlocking, the lower
|
|
27
|
+
end of the reported replicates, the less favourable print order. Most
|
|
28
|
+
papers report tensile (butt joint) strength; where shear was not measured
|
|
29
|
+
it is 0.6 x tensile (source "estimate"): the von Mises ratio 1/sqrt(3),
|
|
30
|
+
which matches the two cross-checks we have (PLA/TPU stacked: 0.37 MPa shear,
|
|
31
|
+
Brancewicz-Steinmetz 2021, against 0.49-0.58 MPa tensile, Tamburrino 2019;
|
|
32
|
+
ABS/ABS lap shear 14.9 MPa, Harris 2019, against 25.4 MPa across-layer
|
|
33
|
+
tensile, Polymaker). Where only lap shear was measured the tensile value is
|
|
34
|
+
taken equal to it, not scaled up.
|
|
35
|
+
|
|
36
|
+
A pair not in the table gets `DEFAULT_FRACTION` (5%) of the weaker
|
|
37
|
+
material's across-layer strength, flagged conservative. The poorly bonding
|
|
38
|
+
pairs measured here sit around it (PLA/TPU stacked 0.29 MPa = 4.5% of TPU's
|
|
39
|
+
6.4 MPa across layers); pairs with affinity sit well above (PETG/TPU 23%,
|
|
40
|
+
ABS/TPU 73%); only PLA/PETG, which is sold as a support pair because it
|
|
41
|
+
does not bond, sits below.
|
|
42
|
+
|
|
43
|
+
Caveat: interface strength is a process property more than a material one.
|
|
44
|
+
It moves by factors of several with the nozzle temperature of the second
|
|
45
|
+
material (Xue 2022: 0.39 to 5.1 MPa for PLA/TPU), the layer height, the
|
|
46
|
+
print order (Tamburrino 2019: PETG on PLA 0.56-0.82 MPa, PLA on PETG
|
|
47
|
+
0.04-0.08), the chamber and bed temperature (Yin et al. 2018: bed
|
|
48
|
+
temperature dominates ABS/TPU), and any interlocking geometry (dovetails,
|
|
49
|
+
interleaved layers: 2-15x). PLA and PETG barely bond at all; Bambu Lab
|
|
50
|
+
uses each as a breakaway support interface for the other. A part that
|
|
51
|
+
leans on a boundary gets coupons printed on the target printer.
|
|
52
|
+
"""
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
|
|
57
|
+
from .material_library import Source
|
|
58
|
+
|
|
59
|
+
__all__ = ["SOURCES", "INTERFACES", "InterfaceCard", "DEFAULT_FRACTION", "SHEAR_FROM_TENSILE",
|
|
60
|
+
"interface_strength", "describe_interface", "interface_names"]
|
|
61
|
+
|
|
62
|
+
ACCESSED = "2026-10-31"
|
|
63
|
+
|
|
64
|
+
# unknown pair: this share of the weaker material's across-layer strength
|
|
65
|
+
DEFAULT_FRACTION = 0.05
|
|
66
|
+
# shear strength = this x tensile where only tensile was measured (1/sqrt(3))
|
|
67
|
+
SHEAR_FROM_TENSILE = 0.6
|
|
68
|
+
|
|
69
|
+
SOURCES = {
|
|
70
|
+
"tamburrino-2019": Source(
|
|
71
|
+
"Tamburrino, Graziosi, Bordegoni, 'The influence of slicing parameters on the multi-material adhesion "
|
|
72
|
+
"mechanisms of FDM printed parts: an exploratory study', Virtual and Physical Prototyping 14(4), 2019, "
|
|
73
|
+
"doi:10.1080/17452759.2019.1607758 (accepted manuscript)",
|
|
74
|
+
"https://re.public.polimi.it/retrieve/e0c31c10-852a-4599-e053-1705fe0aef77/Accepted_manuscript_VPP.pdf",
|
|
75
|
+
"butt-joined bars printed upright (the boundary is between layers), Ultimaker 3, 0.3 mm layers, open "
|
|
76
|
+
"chamber, 3 replicas; Ultimaker CPE (a PETG-family copolyester) 250 C, PLA 205 C, TPU 95A 225 C; peak "
|
|
77
|
+
"stress = breaking load / bonded area, read off Figs. 10 and 22"),
|
|
78
|
+
"xue-2022": Source(
|
|
79
|
+
"Xue, Boudaoud, Robin, Cruz Sanchez, Daya, 'Influence of layer thickness and nozzle temperature on the "
|
|
80
|
+
"interlocking adhesion strength of additive manufactured multi-material interface', Solid Freeform "
|
|
81
|
+
"Fabrication Symposium 2022",
|
|
82
|
+
"https://utw10945.utweb.utexas.edu/sites/default/files/2022/"
|
|
83
|
+
"Influence%20of%20Layer%20Thickness%20and%20Nozzle%20Temperatur.pdf",
|
|
84
|
+
"PLA/TPU butt-joint tensile, specimens printed flat (boundary inside each layer), Raise3D E2, PLA "
|
|
85
|
+
"210 C, TPU 230-250 C, 0.1-0.3 mm layers, n=4; all failed at the boundary (Table 2)"),
|
|
86
|
+
"brancewicz-2021": Source(
|
|
87
|
+
"Brancewicz-Steinmetz, Sawicki, Byczkowska, 'The Influence of 3D Printing Parameters on Adhesion "
|
|
88
|
+
"between Polylactic Acid (PLA) and Thermoplastic Polyurethane (TPU)', Materials 14(21):6464, 2021",
|
|
89
|
+
"https://pmc.ncbi.nlm.nih.gov/articles/PMC8585249",
|
|
90
|
+
"two coaxial cylinders printed one on the other, sheared across the boundary; Ultimaker PLA 200 C, "
|
|
91
|
+
"Fillamentum Flexfill TPU 98A 210 C, 0.2 mm layers; mean over surface patterns 0.37 MPa (PLA first) "
|
|
92
|
+
"and 0.39 MPa (TPU first)"),
|
|
93
|
+
"zatloukal-2025": Source(
|
|
94
|
+
"Zatloukal, Viry, Mizera, Stoklasek, Miskarik, Bednarik, 'Optimizing Interfacial Adhesion and "
|
|
95
|
+
"Mechanical Performance of Multimaterial Joints Fabricated by Material Extrusion', Materials "
|
|
96
|
+
"18(16):3846, 2025",
|
|
97
|
+
"https://pmc.ncbi.nlm.nih.gov/articles/PMC12387702/",
|
|
98
|
+
"ISO 527-1 type 1A bars with a standard butt joint (Test 2), n=10, Original Prusa XL, 0.2 mm layers, "
|
|
99
|
+
"100% infill; PLA 225 C, PETG 250 C, ASA 260 C; tooth joint and interlayer-pressed joint also tested"),
|
|
100
|
+
"harris-2019": Source(
|
|
101
|
+
"Harris, Jursik, Rochefort, Walker, 'Additive Manufacturing With Soft TPU - Adhesion Strength in "
|
|
102
|
+
"Multimaterial Flexible Joints', Frontiers in Mechanical Engineering 5:37, 2019",
|
|
103
|
+
"https://www.frontiersin.org/journals/mechanical-engineering/articles/10.3389/fmech.2019.00037/full",
|
|
104
|
+
"single lap shear (ASTM D1002/D3164), one 0.2 mm TPU layer between ABS or ASA substrates, 4-5 mm "
|
|
105
|
+
"overlap, TPU 218 C, ABS/ASA 240 C, bed 110 C, chamber 70 C, n=4; failed by disbonding at the top "
|
|
106
|
+
"(TPU-under-substrate) boundary"),
|
|
107
|
+
"aberdeen-2024": Source(
|
|
108
|
+
"Aberdeen, Forghani, Sochol, 'Geometric determinants of material jetting-enabled bi-material interface "
|
|
109
|
+
"integrity using PolyJet 3D printing', ASME MSEC2024-122414, 2024",
|
|
110
|
+
"https://par.nsf.gov/servlets/purl/10620996",
|
|
111
|
+
"VeroWhite / Agilus30 Black bars, 30 x 10 mm flat (control) boundary, Objet500 Connex3, printed "
|
|
112
|
+
"flat, matte, n=4, ASTM D638-guided at 12.5 mm/min; the flat control reached 1.074 MPa and failed "
|
|
113
|
+
"in the Agilus30, not at the boundary"),
|
|
114
|
+
"bambu-pla-petg": Source(
|
|
115
|
+
"Bambu Lab Wiki, 'Printing guide for using PLA Basic and PETG HF/PETG Basic for support'",
|
|
116
|
+
"https://wiki.bambulab.com/en/filament-acc/filament/h2d-pla-and-petg-mutual-support",
|
|
117
|
+
"'PLA and PETG don't bond well to each other - and that's exactly what makes them a great support "
|
|
118
|
+
"pair'"),
|
|
119
|
+
"stratasys-overmold": Source(
|
|
120
|
+
"Stratasys Direct, 'PolyJet Flex & PolyJet Over-Mold' material specifications (02/2018)",
|
|
121
|
+
"https://www.rcoeng.com/hubfs/pj_flex_and_overmold_material_datasheet_201802.pdf",
|
|
122
|
+
"rigid VeroWhitePlus and a Shore A Agilus30-based material printed in one build from two STL files; "
|
|
123
|
+
"Agilus30 + Vero digital materials 40A-90A reach 3-14 MPa; no interface strength given"),
|
|
124
|
+
"estimate": Source(
|
|
125
|
+
"loadpath estimate: no measured value for this pair or orientation", "",
|
|
126
|
+
"derived as the note says (shear = 0.6 x tensile; the other orientation equal to the measured one); "
|
|
127
|
+
"replace with coupons printed on your printer where it matters"),
|
|
128
|
+
"default": Source(
|
|
129
|
+
"loadpath default for a pair with no data", "",
|
|
130
|
+
f"{DEFAULT_FRACTION:.0%} of the weaker material's across-layer strength (module docstring)"),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_S = SHEAR_FROM_TENSILE
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _est_shear(t, what="tensile"):
|
|
137
|
+
return (round(_S * t, 3), "estimate", f"{_S} x the {what} value (shear not measured)")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# key: the two library material keys, sorted, joined with "+". "bondSource":
|
|
141
|
+
# the source of the bond rating where no value cites it
|
|
142
|
+
INTERFACES = {
|
|
143
|
+
# ------------------------------------------------------------- FDM
|
|
144
|
+
"petg+tpu": {
|
|
145
|
+
"process": "fdm", "bond": "good",
|
|
146
|
+
"tensile": (1.48, "estimate", "taken equal to the stacked value; no in-layer test found (in-layer "
|
|
147
|
+
"boundaries tested at least as strong for PLA/TPU, Xue 2022)"),
|
|
148
|
+
"shear": _est_shear(1.48),
|
|
149
|
+
"tensileZ": (1.48, "tamburrino-2019", "CPE/TPU butt joint, CPE printed first, lowest replicate "
|
|
150
|
+
"(zig-zag top layers 1.48-2.27, default lines 1.84-2.24)"),
|
|
151
|
+
"shearZ": _est_shear(1.48),
|
|
152
|
+
"note": "the best-bonding FDM pair tested by Tamburrino 2019: interlocking did not raise it, failed "
|
|
153
|
+
"joints stayed partly joined. Print the PETG first; TPU first was weaker.",
|
|
154
|
+
},
|
|
155
|
+
"pla+tpu": {
|
|
156
|
+
"process": "fdm", "bond": "poor",
|
|
157
|
+
"tensile": (1.55, "xue-2022", "in-layer butt joint, TPU 230 C, 0.2 mm layers (0.39-5.06 MPa over "
|
|
158
|
+
"230-250 C and 0.1-0.3 mm; 2.68 at 240 C)"),
|
|
159
|
+
"shear": _est_shear(1.55),
|
|
160
|
+
"tensileZ": (0.29, "tamburrino-2019", "stacked butt joint, PLA first, lowest replicate (zig-zag "
|
|
161
|
+
"0.29-0.44, default lines 0.49-0.58)"),
|
|
162
|
+
"shearZ": (0.37, "brancewicz-2021", "stacked coaxial cylinders in shear, PLA first, mean of the "
|
|
163
|
+
"surface-pattern series (TPU first 0.39)"),
|
|
164
|
+
"note": "low affinity; every Xue 2022 specimen failed at the boundary. Hotter TPU and thinner layers "
|
|
165
|
+
"at the boundary help most; plasma treatment lifts ~0.5 to >=0.7 MPa (LIST, 2025).",
|
|
166
|
+
},
|
|
167
|
+
"petg+pla": {
|
|
168
|
+
"process": "fdm", "bond": "none", "bondSource": "bambu-pla-petg",
|
|
169
|
+
"tensile": (1.6, "zatloukal-2025", "standard butt joint, ISO 527 1A bar (vs PLA 50, PETG 43 MPa "
|
|
170
|
+
"alone); a tooth joint reached 4.0"),
|
|
171
|
+
"shear": _est_shear(1.6),
|
|
172
|
+
"tensileZ": (0.04, "tamburrino-2019", "stacked butt joint, PLA printed first, lowest replicate "
|
|
173
|
+
"(0.04-0.08; with the PETG first 0.56-0.82). Some bars broke in the grips"),
|
|
174
|
+
"shearZ": _est_shear(0.04),
|
|
175
|
+
"note": "do not design on this boundary: Bambu Lab uses PLA and PETG as breakaway supports for each "
|
|
176
|
+
"other because they don't bond. Interleaved, pressed layers (Zatloukal 2025) reached 25 MPa, "
|
|
177
|
+
"so a mechanical interlock is the only way to carry load across it.",
|
|
178
|
+
},
|
|
179
|
+
"abs+tpu": {
|
|
180
|
+
"process": "fdm", "bond": "fair",
|
|
181
|
+
"tensile": (4.7, "estimate", "taken equal to the stacked lap-shear value, not scaled up"),
|
|
182
|
+
"shear": (4.7, "estimate", "taken equal to the stacked lap-shear value"),
|
|
183
|
+
"tensileZ": (4.7, "estimate", "taken equal to the lap-shear value, not scaled up; no butt-joint test"),
|
|
184
|
+
"shearZ": (4.7, "harris-2019", "single lap shear, TPU printed on ABS, 4.7 +- 0.2 MPa (ABS/ABS 14.9)"),
|
|
185
|
+
"note": "measured with a 70 C chamber and 110 C bed; an open printer bonds less (Yin et al. 2018: "
|
|
186
|
+
"bed temperature is the main factor for ABS/TPU). Print the ABS first.",
|
|
187
|
+
},
|
|
188
|
+
"asa+tpu": {
|
|
189
|
+
"process": "fdm", "bond": "fair",
|
|
190
|
+
"tensile": (6.4, "estimate", "taken equal to the stacked lap-shear value, not scaled up"),
|
|
191
|
+
"shear": (6.4, "estimate", "taken equal to the stacked lap-shear value"),
|
|
192
|
+
"tensileZ": (6.4, "estimate", "taken equal to the lap-shear value, not scaled up; no butt-joint test"),
|
|
193
|
+
"shearZ": (6.4, "harris-2019", "single lap shear, TPU printed on ASA, 6.4 +- 0.3 MPa (ASA/ASA 16.1)"),
|
|
194
|
+
"note": "as ABS/TPU: 70 C chamber, 110 C bed; less on an open printer. Print the ASA first.",
|
|
195
|
+
},
|
|
196
|
+
"asa+petg": {
|
|
197
|
+
"process": "fdm", "bond": "fair",
|
|
198
|
+
"tensile": (8.8, "zatloukal-2025", "standard butt joint, ISO 527 1A bar (8.9 in the discussion); a "
|
|
199
|
+
"tooth joint reached 27.1"),
|
|
200
|
+
"shear": _est_shear(8.8),
|
|
201
|
+
"tensileZ": (8.8, "estimate", "taken equal to the butt-joint value (orientation not stated)"),
|
|
202
|
+
"shearZ": _est_shear(8.8),
|
|
203
|
+
"note": "bonded mostly at the specimen edges in the standard joint (Zatloukal 2025).",
|
|
204
|
+
},
|
|
205
|
+
"pa-cf+pla": {
|
|
206
|
+
"process": "fdm", "bond": "poor",
|
|
207
|
+
"tensile": (0.85, "estimate", "no published test found: the default, 5% of PLA's 17 MPa across layers"),
|
|
208
|
+
"shear": _est_shear(0.85),
|
|
209
|
+
"tensileZ": (0.85, "estimate", "as tensile"),
|
|
210
|
+
"shearZ": _est_shear(0.85),
|
|
211
|
+
"note": "unsourced. Nylon prints at 260-300 C, far above PLA's 60 C glass transition, and the two are "
|
|
212
|
+
"chemically unlike; Bambu Lab sells a separate breakaway 'Support for PA/PET'. Test coupons.",
|
|
213
|
+
},
|
|
214
|
+
"pa-cf+petg": {
|
|
215
|
+
"process": "fdm", "bond": "poor",
|
|
216
|
+
"tensile": (0.9, "estimate", "no published test found: the default, 5% of PETG's 18 MPa across layers"),
|
|
217
|
+
"shear": _est_shear(0.9),
|
|
218
|
+
"tensileZ": (0.9, "estimate", "as tensile"),
|
|
219
|
+
"shearZ": _est_shear(0.9),
|
|
220
|
+
"note": "unsourced; nylon and copolyester are chemically unlike and carbon fibre reduces the polymer "
|
|
221
|
+
"at the surface. Test coupons.",
|
|
222
|
+
},
|
|
223
|
+
# --------------------------------------------------------- jetting
|
|
224
|
+
"agilus30+vero": {
|
|
225
|
+
"process": "jetting", "bond": "good", "bondSource": "stratasys-overmold",
|
|
226
|
+
"tensile": (1.07, "aberdeen-2024", "flat VeroWhite/Agilus30 boundary in tension (1.074 MPa); it "
|
|
227
|
+
"failed in the Agilus30, so the bond is at least this"),
|
|
228
|
+
"shear": _est_shear(1.07),
|
|
229
|
+
"tensileZ": (1.07, "estimate", "taken equal to the in-plane value: the materials are jetted and "
|
|
230
|
+
"UV-cured together, layer by layer, in both orientations"),
|
|
231
|
+
"shearZ": _est_shear(1.07),
|
|
232
|
+
"note": "co-cured in one build (Stratasys over-mold); the boundary is limited by the soft side. "
|
|
233
|
+
"1.07 MPa is below Agilus30's datasheet 2.4-3.1 MPa: size and print effects, keep the lower.",
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_PROCESS_ALIASES = {"polyjet": "jetting", "material jetting": "jetting", "fff": "fdm", "fdm": "fdm"}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True)
|
|
241
|
+
class InterfaceCard:
|
|
242
|
+
pair: tuple # the two material keys, sorted
|
|
243
|
+
tensile: float # MPa, boundary inside a layer (side by side)
|
|
244
|
+
shear: float # MPa
|
|
245
|
+
bond: str # good | fair | poor | none | unknown
|
|
246
|
+
source: str # the source ids, comma separated ("default" for an unknown pair)
|
|
247
|
+
note: str
|
|
248
|
+
conservative: bool # True: no measurement for this pair (a default or an estimate)
|
|
249
|
+
tensile_z: float | None = None # MPa, boundary between layers (stacked)
|
|
250
|
+
shear_z: float | None = None
|
|
251
|
+
process: str = ""
|
|
252
|
+
|
|
253
|
+
def to_dict(self):
|
|
254
|
+
return {"pair": list(self.pair), "tensile": self.tensile, "shear": self.shear, "bond": self.bond,
|
|
255
|
+
"tensileZ": self.tensile_z, "shearZ": self.shear_z, "source": self.source,
|
|
256
|
+
"note": self.note, "conservative": self.conservative, "process": self.process}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _key(m):
|
|
260
|
+
"""A material (name, alias, '<base>-custom' name or Material) as its
|
|
261
|
+
library key."""
|
|
262
|
+
from .materials import _ALIASES # lazy: materials imports material_library
|
|
263
|
+
name = str(getattr(m, "name", m)).strip().lower()
|
|
264
|
+
while name.endswith("-custom"):
|
|
265
|
+
name = name[: -len("-custom")]
|
|
266
|
+
return _ALIASES.get(name, name)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _material(m, key):
|
|
270
|
+
"""The Material for `m`: its own object if it is one, else the library's."""
|
|
271
|
+
from .materials import MATERIALS
|
|
272
|
+
if hasattr(m, "strength"):
|
|
273
|
+
return m
|
|
274
|
+
if key in MATERIALS:
|
|
275
|
+
return MATERIALS[key]
|
|
276
|
+
raise ValueError(f"unknown material {m!r}; known: {sorted(MATERIALS)}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def interface_names():
|
|
280
|
+
return sorted(INTERFACES)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _pair_key(a, b):
|
|
284
|
+
ka, kb = _key(a), _key(b)
|
|
285
|
+
if ka == kb:
|
|
286
|
+
raise ValueError(f"{ka!r} with itself is not an interface: use the material's own across-layer "
|
|
287
|
+
"strength (strengthZ / process interlayer)")
|
|
288
|
+
return tuple(sorted((ka, kb)))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def interface_strength(a, b, process=None):
|
|
292
|
+
"""The InterfaceCard for materials `a` and `b` (names, aliases or
|
|
293
|
+
Material objects; order does not matter). `process` ("fdm",
|
|
294
|
+
"jetting"...), when given, must match the measured pair's process;
|
|
295
|
+
otherwise, and for a pair with no data, the card is the conservative
|
|
296
|
+
default: DEFAULT_FRACTION of the weaker across-layer strength."""
|
|
297
|
+
pair = _pair_key(a, b)
|
|
298
|
+
proc = _PROCESS_ALIASES.get(str(process).strip().lower(), str(process).strip().lower()) \
|
|
299
|
+
if process is not None else None
|
|
300
|
+
e = INTERFACES.get("+".join(pair))
|
|
301
|
+
if e is not None and (proc is None or proc == e["process"]):
|
|
302
|
+
srcs = []
|
|
303
|
+
for k in ("tensile", "shear", "tensileZ", "shearZ"):
|
|
304
|
+
if k in e and e[k][1] not in srcs:
|
|
305
|
+
srcs.append(e[k][1])
|
|
306
|
+
if e.get("bondSource") and e["bondSource"] not in srcs:
|
|
307
|
+
srcs.append(e["bondSource"])
|
|
308
|
+
return InterfaceCard(pair, float(e["tensile"][0]), float(e["shear"][0]), e["bond"],
|
|
309
|
+
",".join(srcs), e["note"], all(s == "estimate" for s in srcs),
|
|
310
|
+
float(e["tensileZ"][0]) if "tensileZ" in e else None,
|
|
311
|
+
float(e["shearZ"][0]) if "shearZ" in e else None, e["process"])
|
|
312
|
+
weak = min((m.strength_z or m.strength) for m in (_material(a, _key(a)), _material(b, _key(b))))
|
|
313
|
+
t = round(DEFAULT_FRACTION * weak, 3)
|
|
314
|
+
why = (f"no data for {pair[0]}+{pair[1]}" + (f" by {proc}" if e is not None else "")
|
|
315
|
+
+ f": {DEFAULT_FRACTION:.0%} of the weaker across-layer strength ({weak:g} MPa); print coupons")
|
|
316
|
+
return InterfaceCard(pair, t, round(SHEAR_FROM_TENSILE * t, 3), "unknown", "default", why, True,
|
|
317
|
+
t, round(SHEAR_FROM_TENSILE * t, 3), proc or "")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def describe_interface(a, b, process=None):
|
|
321
|
+
"""An interface as JSON for reports and the CLI: each value with its
|
|
322
|
+
source's title and url."""
|
|
323
|
+
card = interface_strength(a, b, process)
|
|
324
|
+
e = INTERFACES.get("+".join(card.pair)) if card.source != "default" else None
|
|
325
|
+
vals = {}
|
|
326
|
+
if e is not None:
|
|
327
|
+
for k in ("tensile", "shear", "tensileZ", "shearZ"):
|
|
328
|
+
if k in e:
|
|
329
|
+
v, src, note = e[k]
|
|
330
|
+
s = SOURCES[src]
|
|
331
|
+
vals[k] = {"value": v, "note": note, "source": s.title, **({"url": s.url} if s.url else {})}
|
|
332
|
+
else:
|
|
333
|
+
s = SOURCES["default"]
|
|
334
|
+
for k, v in (("tensile", card.tensile), ("shear", card.shear), ("tensileZ", card.tensile_z),
|
|
335
|
+
("shearZ", card.shear_z)):
|
|
336
|
+
vals[k] = {"value": v, "note": card.note, "source": s.title}
|
|
337
|
+
bs = SOURCES.get(e.get("bondSource", "")) if e is not None else None
|
|
338
|
+
return {"pair": "+".join(card.pair), "process": card.process, "bond": card.bond, "note": card.note,
|
|
339
|
+
**({"bondSource": {"source": bs.title, "url": bs.url, "note": bs.note}} if bs else {}),
|
|
340
|
+
"conservative": card.conservative, "values": vals, "accessed": ACCESSED}
|
loadpath/interlock.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mechanical interlocks at the interface between two printed materials.
|
|
3
|
+
|
|
4
|
+
A flat butt joint between two materials holds only by adhesion, and some
|
|
5
|
+
pairs barely adhere: PETG/TPU 1.5 MPa against PETG's 47, PLA on PETG almost
|
|
6
|
+
nothing across layers (interface_library.py). Multi-material printers get
|
|
7
|
+
round that by interlocking the materials where they meet: Cura's and Bambu
|
|
8
|
+
Studio's "interlocking beams", after Kuipers et al.'s interlaced
|
|
9
|
+
topologically interlocking lattice (ITIL, Additive Manufacturing 2022,
|
|
10
|
+
doi:10.1016/j.addma.2021.102495).
|
|
11
|
+
|
|
12
|
+
The pattern here is that lattice. In a band of `depth` mm straddling the
|
|
13
|
+
interface, space is cut into beams `beamWidth` wide and `layers` print
|
|
14
|
+
layers tall. Layers alternate between beams along one in-plane axis and
|
|
15
|
+
beams along the other, and the beams alternate material. Each material
|
|
16
|
+
forms a woodpile connected to its own side, and the two woodpiles
|
|
17
|
+
interpenetrate: neither can be pulled out of the other without breaking
|
|
18
|
+
beams. Every beam is a whole number of extrusion widths and layers, so a
|
|
19
|
+
slicer prints it as solid lines.
|
|
20
|
+
|
|
21
|
+
What the interlock is credited with (`interlocked_card`), for a pair whose
|
|
22
|
+
flat bond is S (tensile, and shear the same way):
|
|
23
|
+
|
|
24
|
+
S_eff = min(gain * S, cap)
|
|
25
|
+
|
|
26
|
+
* `gain` is the measured ratio of interlocked to flat strength, scaled by
|
|
27
|
+
depth up to 2 mm. Frenkel et al. 2022 (Biomimetics 7:170,
|
|
28
|
+
doi:10.3390/biomimetics7040170), PETG/TPU on one FDM head: an
|
|
29
|
+
interdigitated interface with 2 mm overlap raised UTS by 202% over the
|
|
30
|
+
butt joint (x3.0). A 2026 PLA/TPU study (Int. J. Adv. Manuf. Technol.,
|
|
31
|
+
doi:10.1007/s00170-026-17902-3): 45 deg interlocking, 2 mm deep, 5.91 MPa,
|
|
32
|
+
against ~1.5 MPa for a flat PLA/TPU butt (interface_library's card), so
|
|
33
|
+
x3.8. The default gain of 2.5 is below both.
|
|
34
|
+
* `cap` is what the lattice itself can carry. In the woodpile, a plane
|
|
35
|
+
across the band in a print layer cuts each material over at least a
|
|
36
|
+
quarter of its area, so in-layer the lattice carries at most a quarter
|
|
37
|
+
of the weaker material's strength. Between two layers, the plane is a
|
|
38
|
+
quarter one material on itself, a quarter the other on itself (at the
|
|
39
|
+
materials' across-layer strengths) and half the bond:
|
|
40
|
+
0.25 * min(S_z) + 0.5 * S_bond,z. Shear caps at 0.6 of those.
|
|
41
|
+
|
|
42
|
+
Verification judges the smooth interface (what the optimizer designed)
|
|
43
|
+
against S_eff, except within `avoid` of the part's outer surface, where
|
|
44
|
+
no beams are put (they would leave slivers, as Cura's boundary avoidance
|
|
45
|
+
knows) and the bond is judged flat. The delivered bodies carry the lattice. A voxel model at
|
|
46
|
+
analysis pitch cannot resolve 0.8 mm beams, so the lattice is credited as
|
|
47
|
+
a homogenized bond and not analysed beam by beam. The default gain sits
|
|
48
|
+
below both sources because of that.
|
|
49
|
+
"""
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
from scipy import ndimage
|
|
56
|
+
|
|
57
|
+
GAIN = 2.5 # interlocked / flat bond strength at full depth (see module docstring)
|
|
58
|
+
FULL_DEPTH = 2.0 # mm: the depth the gains were measured at
|
|
59
|
+
SHEAR_CAP = 0.6
|
|
60
|
+
SOURCES = [
|
|
61
|
+
"Frenkel et al. 2022, Biomimetics 7:170, doi:10.3390/biomimetics7040170 (PETG/TPU, 2 mm overlap, +202% UTS)",
|
|
62
|
+
"Int. J. Adv. Manuf. Technol. 2026, doi:10.1007/s00170-026-17902-3 (PLA/TPU, 45 deg interlock 2 mm, 5.91 MPa)",
|
|
63
|
+
"Kuipers et al. 2022, Additive Manufacturing, doi:10.1016/j.addma.2021.102495 (ITIL lattice)",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Interlock:
|
|
69
|
+
pattern: str = "beams"
|
|
70
|
+
depth: float = 2.0 # mm, total, straddling the interface
|
|
71
|
+
beam_width: float = 0.8 # mm in plane (two 0.4 mm extrusions)
|
|
72
|
+
layer_height: float = 0.4 # mm per beam layer (two 0.2 mm layers)
|
|
73
|
+
gain: float | None = None # None: GAIN scaled by depth
|
|
74
|
+
pairs: list | None = None # ["petg+tpu", ...]; None: every pair
|
|
75
|
+
# mm: no beams this close to the part's outer surface (slivers); the
|
|
76
|
+
# bond there is flat and verification judges it so. None: a beam width
|
|
77
|
+
# plus half the design grid's cell (set by multimat)
|
|
78
|
+
avoid: float | None = None
|
|
79
|
+
|
|
80
|
+
def factor(self) -> float:
|
|
81
|
+
g = GAIN if self.gain is None else float(self.gain)
|
|
82
|
+
if self.gain is not None:
|
|
83
|
+
return g
|
|
84
|
+
return 1.0 + (g - 1.0) * min(1.0, self.depth / FULL_DEPTH)
|
|
85
|
+
|
|
86
|
+
def applies(self, a: str, b: str) -> bool:
|
|
87
|
+
if not self.pairs:
|
|
88
|
+
return True
|
|
89
|
+
key = "+".join(sorted((a, b)))
|
|
90
|
+
return any("+".join(sorted(s.strip() for s in p.split("+"))) == key for p in self.pairs)
|
|
91
|
+
|
|
92
|
+
def to_dict(self):
|
|
93
|
+
return {"pattern": self.pattern, "depth": self.depth, "beamWidth": self.beam_width,
|
|
94
|
+
"layerHeight": self.layer_height, "gain": self.factor(), "pairs": self.pairs}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_interlock(d, process=None) -> Interlock | None:
|
|
98
|
+
"""The `interlock` value of a multiMaterial block: true, false, or
|
|
99
|
+
{depth, beamWidth, layers | layerHeight, gain, pairs}."""
|
|
100
|
+
if d is None or d is False:
|
|
101
|
+
return None
|
|
102
|
+
nozzle, layer = 0.4, 0.2
|
|
103
|
+
if process is not None:
|
|
104
|
+
layer = float(getattr(process, "layer_height", None) or layer)
|
|
105
|
+
nozzle = float(getattr(process, "nozzle", None) or nozzle)
|
|
106
|
+
il = Interlock(beam_width=2 * nozzle, layer_height=2 * layer)
|
|
107
|
+
if d is True:
|
|
108
|
+
return il
|
|
109
|
+
if not isinstance(d, dict):
|
|
110
|
+
raise ValueError("multiMaterial.interlock is true or {depth, beamWidth, layers, gain, pairs}")
|
|
111
|
+
if d.get("pattern", "beams") != "beams":
|
|
112
|
+
raise ValueError("multiMaterial.interlock.pattern: only 'beams' (an interlaced woodpile)")
|
|
113
|
+
if "depth" in d:
|
|
114
|
+
il.depth = float(d["depth"])
|
|
115
|
+
if "beamWidth" in d:
|
|
116
|
+
il.beam_width = float(d["beamWidth"])
|
|
117
|
+
if "layers" in d:
|
|
118
|
+
il.layer_height = int(d["layers"]) * layer
|
|
119
|
+
if "layerHeight" in d:
|
|
120
|
+
il.layer_height = float(d["layerHeight"])
|
|
121
|
+
if "gain" in d:
|
|
122
|
+
il.gain = float(d["gain"])
|
|
123
|
+
if il.gain < 1.0:
|
|
124
|
+
raise ValueError("multiMaterial.interlock.gain is interlocked / flat strength, >= 1")
|
|
125
|
+
if "pairs" in d:
|
|
126
|
+
il.pairs = [str(p) for p in d["pairs"]]
|
|
127
|
+
for k, v in (("depth", il.depth), ("beamWidth", il.beam_width), ("layerHeight", il.layer_height)):
|
|
128
|
+
if v <= 0:
|
|
129
|
+
raise ValueError(f"multiMaterial.interlock.{k} must be > 0 (mm)")
|
|
130
|
+
if il.depth < 2 * il.beam_width:
|
|
131
|
+
raise ValueError(f"multiMaterial.interlock.depth {il.depth} mm holds less than two beams of "
|
|
132
|
+
f"{il.beam_width} mm: nothing interlocks")
|
|
133
|
+
return il
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def interlocked_card(card: dict, a, b, il: Interlock) -> dict:
|
|
137
|
+
"""The bond card of materials a, b (Material) with the interlock credited."""
|
|
138
|
+
g = il.factor()
|
|
139
|
+
weak = min(a.strength, b.strength)
|
|
140
|
+
weak_z = min(a.strength_z or a.strength, b.strength_z or b.strength)
|
|
141
|
+
cap = 0.25 * weak
|
|
142
|
+
cap_z = 0.25 * weak_z + 0.5 * card["tensileZ"]
|
|
143
|
+
out = dict(card)
|
|
144
|
+
out["tensile"] = max(card["tensile"], min(g * card["tensile"], cap))
|
|
145
|
+
out["shear"] = max(card["shear"], min(g * card["shear"], SHEAR_CAP * cap))
|
|
146
|
+
out["tensileZ"] = max(card["tensileZ"], min(g * card["tensileZ"], cap_z))
|
|
147
|
+
out["shearZ"] = max(card["shearZ"], min(g * card["shearZ"], SHEAR_CAP * cap_z))
|
|
148
|
+
out["flat"] = {k: card[k] for k in ("tensile", "shear", "tensileZ", "shearZ")}
|
|
149
|
+
out["interlock"] = {**il.to_dict(), "cap": cap, "capZ": cap_z, "sources": SOURCES}
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def beam_pattern(xyz: np.ndarray, il: Interlock, build_axis: int = 2, phase=None) -> np.ndarray:
|
|
154
|
+
"""True where the first material of the pair goes, for points (..., 3):
|
|
155
|
+
layer k (along the build axis) holds beams along in-plane axis u when
|
|
156
|
+
k is even, along v when odd; beams alternate material across the other
|
|
157
|
+
in-plane axis."""
|
|
158
|
+
u, v = [a for a in range(3) if a != build_axis]
|
|
159
|
+
p = xyz if phase is None else xyz - np.asarray(phase, float)
|
|
160
|
+
k = np.floor(p[..., build_axis] / il.layer_height).astype(np.int64)
|
|
161
|
+
iu = np.floor(p[..., u] / il.beam_width).astype(np.int64)
|
|
162
|
+
iv = np.floor(p[..., v] / il.beam_width).astype(np.int64)
|
|
163
|
+
# even layers: beams run along u, material set by the v index; odd: along v, set by u
|
|
164
|
+
return np.where(k % 2 == 0, iv % 2 == 0, iu % 2 == 0)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def interlocked_indicator(inside: np.ndarray, spacing, origin, il: Interlock, build_axis: int = 2,
|
|
168
|
+
phase=None, mask=None) -> np.ndarray:
|
|
169
|
+
"""A material's region on a fine grid (bool, `inside`) with the beam
|
|
170
|
+
lattice put in a band of il.depth round its boundary. `mask` (bool)
|
|
171
|
+
limits the band (where the other side is a material the interlock
|
|
172
|
+
applies to). Grid point (i, j, k) is at origin + index * spacing."""
|
|
173
|
+
spacing = np.asarray(spacing, float)
|
|
174
|
+
d_in = ndimage.distance_transform_edt(inside, sampling=spacing)
|
|
175
|
+
d_out = ndimage.distance_transform_edt(~inside, sampling=spacing)
|
|
176
|
+
sd = np.where(inside, d_in, -d_out)
|
|
177
|
+
band = np.abs(sd) < 0.5 * il.depth
|
|
178
|
+
if mask is not None:
|
|
179
|
+
band &= mask
|
|
180
|
+
if not band.any():
|
|
181
|
+
return inside
|
|
182
|
+
idx = np.argwhere(band)
|
|
183
|
+
xyz = np.asarray(origin, float) + idx * spacing
|
|
184
|
+
out = inside.copy()
|
|
185
|
+
out[tuple(idx.T)] = beam_pattern(xyz, il, build_axis, phase)
|
|
186
|
+
return out
|