code wiki / (root) / nx_pillar_physics.nx

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}