nx_pillar_physics.nx source
↩ module page · 210 lines · 9593 B
1// nx_pillar_physics.nx -- bits-up cantilever-moment + load-proportional
2// pillar footprint sizing for FDM support generation.
3//
4// =====================================================================
5// EXCEED axis (research baseline)
6// =====================================================================
7//
8// As of 2026-05, NO production slicer sizes support pillars by load.
9// All four major systems use CONSTANT pillar diameter:
10//
11// OrcaSlicer 2.3.2: `tree_support_branch_diameter` USER-SET CONSTANT
12// Bambu Studio: `tree_support_branch_distance` USER-SET CONSTANT
13// Cura 5.x: `support_tree_branch_diameter` USER-SET CONSTANT
14// PrusaSlicer 2.7: `support_pillar_diameter` USER-SET CONSTANT
15//
16// Academic SOTA (Topology-Aware Support Generation, SIGGRAPH 2023)
17// uses FEM stress analysis but is OFFLINE (minutes per model) and
18// non-incremental. No production system does load-aware sizing at
19// slice-time.
20//
21// This file ships a Q14 fixed-point load-proportional sizing primitive
22// that runs in O(1) per overhang vertex. Industry uses constant
23// diameter; we scale with computed moment. That is the EXCEED axis.
24//
25// v2.1 (active, this file): unit-exact bending mechanics via cube-root
26// section modulus. For a square pillar of edge s under bending moment
27// M, the maximum bending stress is σ = M × (s/2) / (s⁴/12) = 6M/s³.
28// Setting σ ≤ σ_yield gives s ≥ ∛(6M / σ_yield). This is the textbook
29// formula NO production slicer implements (none read material yield at
30// slice-time). Ships bits-up via nx_icbrt + nx_icbrt_q14.
31//
32// v2.0 (sqrt-proportional, kept as nx_pillar_min_footprint_v20 for
33// reference/benchmarking) was monotone but not unit-exact -- safe at
34// the calibration anchor, less conservative at heavy loads. v2.1's
35// cube-root scaling grows ~30% slower than v2.0's sqrt at the heavy
36// end, matching real bending mechanics.
37//
38// =====================================================================
39//
40// license_tier: ORIGINAL
41
42import "nx_syscalls.nx"
43import "nx_isqrt.nx"
44import "nx_icbrt.nx"
45import "nx_material_profile.nx"
46const NX_MAGIC_13743895348: i64 = 13743895348
47const NX_MAGIC_9223372036854775000: i64 = 9223372036854775000
48
49// ===== calibration constants =======================================
50//
51// SAFETY_Q14 is calibrated so:
52// 1 g cantilever × 1 mm arm × PLA yield (30 MPa @ 50°C) → 0.6 mm
53// 10 g × 10 mm × PLA → 6.3 mm (above MIN, below MAX)
54// 1 g × 1 mm × PEEK yield (95 MPa @ 200°C) → 0.36 mm (clamped to MIN)
55//
56// MIN_FOOT_Q14: industry-baseline pillar diameter (Orca/Bambu default ≈ 2 mm).
57// MAX_FOOT_Q14: runaway cap; loads above this saturate the footprint.
58//
59// Calibration cross-checks documented in NISHI_3D_PRINT_PHYSICS_EXCEED.md.
60
61const NX_MAT_PILLAR_SAFETY_Q14: i64 = 196608 // 12.0 in Q14
62const NX_MAT_PILLAR_MIN_FOOT_Q14: i64 = 32768 // 2.0 mm in Q14
63const NX_MAT_PILLAR_MAX_FOOT_Q14: i64 = 131072 // 8.0 mm in Q14
64
65const NX_PILLAR_PHYS_Q14: i64 = 16384
66
67// ===== cantilever moment ==========================================
68//
69// Inputs (Q14):
70// mass_g_q14: mass of cantilever material above the pillar, grams
71// arm_mm_q14: horizontal distance from pillar centerline to the
72// cantilever load centroid, mm
73//
74// Output (Q14): bending moment magnitude, g·mm.
75//
76// This is the LOCAL load attributable to ONE pillar. Caller is
77// responsible for slicing the overhang into per-pillar contributions
78// (typically: overhang_area_near_pillar × layer_thickness × density
79// × n_layers_above). This primitive is the math leaf.
80//
81// Returns 0 for nonpositive inputs (well-defined: no load → no moment).
82// Inputs > ~700 g·mm in physical units are clamped by the i64 product
83// guard (1000 g × 1000 mm would push the intermediate over 2^63).
84
85func nx_cantilever_moment(mass_g_q14: i64, arm_mm_q14: i64) -> i64 {
86 if mass_g_q14 <= 0 { return 0 }
87 if arm_mm_q14 <= 0 { return 0 }
88
89 // Overflow guard: max safe Q14*Q14 product is < 2^63.
90 // 2^63 / Q14² = 2^63 / 2^28 = 2^35 ≈ 3.4e10
91 // So mass_g (real) × arm_mm (real) must be < ~3.4e10 (in g·mm).
92 // Cap inputs at 1e9 g·mm physical = 1e9 × 16384 = 1.6e13 in Q14.
93 let safe_limit: i64 = NX_MAGIC_13743895348 // 8.4e9 g·mm in Q14 (safe headroom)
94 var m_clamped: i64 = mass_g_q14
95 var a_clamped: i64 = arm_mm_q14
96 if m_clamped > safe_limit { m_clamped = safe_limit }
97 if a_clamped > safe_limit { a_clamped = safe_limit }
98
99 // Q14 multiply: (m_q14 × a_q14) / 16384 = (m × a)_q14
100 let prod: i64 = m_clamped * a_clamped
101 let moment_q14: i64 = prod / NX_PILLAR_PHYS_Q14
102 return moment_q14
103}
104
105// ===== load-proportional footprint sizing =========================
106//
107// Inputs (Q14):
108// moment_q14: cantilever moment from nx_cantilever_moment (g·mm)
109// yield_mpa_q14: material yield stress at PRINT-CHAMBER temperature
110// (read from NxMaterialProfile.tensile_yield_mpa_q14;
111// NOT room-temperature yield -- prints at chamber temp)
112// safety_q14: calibration factor; typically NX_MAT_PILLAR_SAFETY_Q14
113//
114// Output (Q14): square pillar edge length, mm, clamped to
115// [NX_MAT_PILLAR_MIN_FOOT_Q14, NX_MAT_PILLAR_MAX_FOOT_Q14].
116//
117// Industry comparison: all four major slicers (Orca/Bambu/Cura/Prusa)
118// emit pillars at the user-set constant diameter regardless of moment
119// OR material. This primitive sizes pillars proportional to
120// ∛(load/yield) per textbook bending mechanics for a square cross-section
121// (section modulus Z = s³/6, σ_max = 6M/s³, set ≤ σ_yield).
122//
123// v2.1 mechanics (this function): cube-root section modulus, unit-exact.
124// v2.0 mechanics (nx_pillar_min_footprint_v20 below): sqrt-proportional,
125// monotone-only. Kept for benchmark/research; new code should use v2.1.
126
127func nx_pillar_min_footprint(moment_q14: i64,
128 yield_mpa_q14: i64,
129 safety_q14: i64) -> i64 {
130 if moment_q14 <= 0 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
131 if yield_mpa_q14 <= 0 { return NX_MAT_PILLAR_MAX_FOOT_Q14 }
132 if safety_q14 <= 0 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
133
134 // s³ = 6 × safety × M / σ_yield (real units, mm³)
135 // s_q14 = ∛((6 × safety × M / σ)_q14) using nx_icbrt_q14
136 //
137 // Build R_q14 = (safety_q14 × M_q14) / σ_q14 (Q14 of safety×M/σ).
138 // Then s_q14 = nx_icbrt_q14(6 × R_q14).
139
140 let prod_limit: i64 = NX_MAGIC_9223372036854775000 // ~2^63 - epsilon
141 let max_safe_m: i64 = prod_limit / safety_q14
142 var m_clamped: i64 = moment_q14
143 if m_clamped > max_safe_m { m_clamped = max_safe_m }
144
145 let prod_sm: i64 = safety_q14 * m_clamped
146 let r_q14: i64 = prod_sm / yield_mpa_q14
147
148 // 6 × R_q14 must not overflow before nx_icbrt_q14's internal × 2^28.
149 // nx_icbrt_q14 caps at NX_ICBRT_Q14_LIMIT internally; here we just
150 // protect the 6× multiplication.
151 let max_r_for_six: i64 = prod_limit / 6
152 var r_for_six: i64 = r_q14
153 if r_for_six > max_r_for_six { r_for_six = max_r_for_six }
154 let six_r: i64 = 6 * r_for_six
155
156 let edge_q14: i64 = nx_icbrt_q14(six_r)
157
158 // Clamp into [MIN_FOOT, MAX_FOOT]
159 if edge_q14 < NX_MAT_PILLAR_MIN_FOOT_Q14 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
160 if edge_q14 > NX_MAT_PILLAR_MAX_FOOT_Q14 { return NX_MAT_PILLAR_MAX_FOOT_Q14 }
161 return edge_q14
162}
163
164// ===== v2.0 legacy (kept for benchmark / research) ================
165//
166// Sqrt-proportional sizing. Monotone in load and 1/yield like v2.1
167// but NOT unit-exact. Retained so research can compare v2.0 vs v2.1
168// on identical inputs. New code should call nx_pillar_min_footprint
169// (the v2.1 cube-root path).
170
171func nx_pillar_min_footprint_v20(moment_q14: i64,
172 yield_mpa_q14: i64,
173 safety_q14: i64) -> i64 {
174 if moment_q14 <= 0 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
175 if yield_mpa_q14 <= 0 { return NX_MAT_PILLAR_MAX_FOOT_Q14 }
176 if safety_q14 <= 0 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
177
178 let prod_limit: i64 = NX_MAGIC_9223372036854775000
179 let max_safe_a: i64 = prod_limit / safety_q14
180 var m_clamped: i64 = moment_q14
181 if m_clamped > max_safe_a { m_clamped = max_safe_a }
182
183 let prod_sm: i64 = safety_q14 * m_clamped
184 let required_area_q14: i64 = prod_sm / yield_mpa_q14
185
186 let max_safe_area: i64 = prod_limit / NX_PILLAR_PHYS_Q14
187 var area_for_sqrt: i64 = required_area_q14
188 if area_for_sqrt > max_safe_area { area_for_sqrt = max_safe_area }
189 let shifted_area: i64 = area_for_sqrt * NX_PILLAR_PHYS_Q14
190 let edge_q14: i64 = nx_isqrt(shifted_area)
191
192 if edge_q14 < NX_MAT_PILLAR_MIN_FOOT_Q14 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
193 if edge_q14 > NX_MAT_PILLAR_MAX_FOOT_Q14 { return NX_MAT_PILLAR_MAX_FOOT_Q14 }
194 return edge_q14
195}
196
197// ===== convenience: pillar footprint for a material+load directly =====
198//
199// Composes nx_cantilever_moment + nx_pillar_min_footprint reading
200// the material's at-temp yield stress straight from the profile.
201
202func nx_pillar_footprint_for_load(mass_g_q14: i64,
203 arm_mm_q14: i64,
204 material: *NxMaterialProfile) -> i64 {
205 if (material as i64) == 0 { return NX_MAT_PILLAR_MIN_FOOT_Q14 }
206 let moment_q14: i64 = nx_cantilever_moment(mass_g_q14, arm_mm_q14)
207 return nx_pillar_min_footprint(moment_q14,
208 material.tensile_yield_mpa_q14,
209 NX_MAT_PILLAR_SAFETY_Q14)
210}