code wiki / (root) / nx_print_sim.nx

nx_print_sim.nx source

↩ module page · 374 lines · 15531 B

1// nx_print_sim.nx -- military/aerospace-grade print simulator, 2// FIRST STONE. 3// 4// ===================================================================== 5// CARDINAL (2026-05-20): 6// "real life simulation like the military or aircraft do where we 7// can simulate a print with settings in hyperspeed to see what 8// issues will happen in reality with those settings none of this 9// guesswork bullshit" 10// 11// Bar: simulate the entire print at hyperspeed (24h Christus print 12// evaluated in seconds) and emit predicted failures with cause + 13// location + magnitude. No heuristics; real physics throughout. 14// 15// 12-18 sessions to military-grade per the cardinal. Modeled 16// physics roadmap (none of which industry slicers do at slice-time): 17// - Thermal: hotend + bed + chamber + cooling-fan heat diffusion 18// - Mechanical: bridge sag + pillar buckling + adhesion stress 19// - Flow: pressure advance + viscosity + max-flow ceiling 20// - Adhesion: first-layer adhesion + warp force vs bed-grip 21// - Kinematic: input-shaping residual + ringing + collision detect 22// 23// FIRST STONE (this file): state tracking + ONE real physical 24// invariant: HOTEND MAX-FLOW CEILING. Volumetric flow rate 25// (width × height × speed in mm³/s) is a real physical limit 26// (hotend power budget); exceeding it predicts underextrusion. 27// Industry slicers CLAMP speed proactively against the parameter; 28// we SIMULATE what would happen if user-supplied G-code exceeded it. 29// 30// Per [[feedback-exceed-means-superior-capability-not-toy-parameter-addition]]: 31// this primitive earns the EXCEED label only when it correctly 32// predicts a real-print failure that industry slicers miss. Today 33// it's a TRUE check (real physics) but the simulator-as-capability 34// is at the first stone -- EXCEED claim is for the full integrated 35// simulator that lands across the 12-18 session arc. 36// ===================================================================== 37// 38// license_tier: ORIGINAL 39 40import "nx_syscalls.nx" 41import "nx_machine_graph.nx" 42import "nx_material_profile.nx" 43const NX_MAGIC_1024: i64 = 1024 44 45// ===== sealed issue-kind enum ===================================== 46// 47// Every kind queued in the roadmap is listed; FIRST STONE implements 48// only HOTEND_FLOW_CEILING. The rest are ALLOCATED constants so 49// downstream consumers (caller-side iteration) can switch-case them 50// safely as later stones land. 51 52const NX_SIM_ISSUE_NONE: i64 = 0 53const NX_SIM_ISSUE_HOTEND_FLOW_CEILING: i64 = 1 // SHIPPED 54const NX_SIM_ISSUE_MATERIAL_FLOW_CEILING: i64 = 2 // SHIPPED 55const NX_SIM_ISSUE_LAYER_TIME_LOW: i64 = 3 // queued (thermal arc) 56const NX_SIM_ISSUE_BRIDGE_SAG: i64 = 4 // queued (mechanical arc) 57const NX_SIM_ISSUE_PILLAR_BUCKLING: i64 = 5 // queued (mechanical arc) 58const NX_SIM_ISSUE_COLLISION: i64 = 6 // queued (kinematic arc) 59const NX_SIM_ISSUE_BED_ADHESION_LIFT: i64 = 7 // queued (adhesion arc) 60const NX_SIM_ISSUE_RINGING_VISIBLE: i64 = 8 // queued (kinematic arc) 61// ----- spaghetti / halfway-failure progenitors (2026-05-20) ----- 62// Real-physics predictors of the most common catastrophic mid-print 63// failure modes. Each composes existing machine + material fields, 64// no heuristic thresholds. 65const NX_SIM_ISSUE_ENVELOPE_VIOLATION: i64 = 9 // SHIPPED (this update) 66const NX_SIM_ISSUE_BED_CRASH: i64 = 10 // SHIPPED (this update) 67const NX_SIM_ISSUE_TIPOVER_RISK: i64 = 11 // SHIPPED (this update) 68const NX_SIM_ISSUE_N: i64 = 12 69 70func nx_sim_issue_is_valid(k: i64) -> i64 { 71 if k < 0 { return 0 } 72 if k >= NX_SIM_ISSUE_N { return 0 } 73 return 1 74} 75 76// ===== issue record =============================================== 77 78struct NxPrintSimIssue { 79 kind: i64, 80 x_q14: i64, 81 y_q14: i64, 82 z_q14: i64, 83 layer_idx: i64, 84 magnitude_q14: i64, // severity (kind-specific units; e.g., 85 // for HOTEND_FLOW_CEILING this is 86 // flow_mm3s_q14 - max_flow_q14 = excess 87} 88 89const NX_PRINT_SIM_ISSUE_BYTES: i64 = 48 90 91// ===== simulator state ============================================ 92 93struct NxPrintSim { 94 machine: *NxMachineGraph, 95 material: *NxMaterialProfile, 96 97 // Position state (Q14 mm) 98 cur_x_q14: i64, 99 cur_y_q14: i64, 100 cur_z_q14: i64, 101 102 // Extruder state 103 cur_e_q14: i64, // accumulated E axis 104 cumulative_volume_um3: i64, // total filament volume (µm³) 105 106 // Time state 107 cumulative_time_ms: i64, // simulated wall-clock so far 108 109 // Layer tracking 110 current_layer_idx: i64, 111 112 // Bbox tracking (accumulated min/max XY/Z across all moves -- used by 113 // end-of-print TIPOVER check). Initialised to sentinels at _new. 114 bbox_min_x_q14: i64, 115 bbox_max_x_q14: i64, 116 bbox_min_y_q14: i64, 117 bbox_max_y_q14: i64, 118 bbox_min_z_q14: i64, 119 bbox_max_z_q14: i64, 120 121 // Issue log 122 issues: *NxPrintSimIssue, 123 n_issues: i64, 124 issues_cap: i64, 125} 126 127const NX_PRINT_SIM_BYTES: i64 = 144 // 18 fields × 8 128 129// Sentinel for "no moves seen yet" -- larger than any physical bbox. 130const NX_PRINT_SIM_SENTINEL_HI: i64 = 9223372036854775000 131const NX_PRINT_SIM_SENTINEL_LO: i64 = -9223372036854775000 132 133const NX_PRINT_SIM_Q14: i64 = 16384 134const NX_PRINT_SIM_Q14_SQ: i64 = 268435456 // 16384² 135 136// ===== construction ================================================ 137 138func nx_print_sim_new(machine: *NxMachineGraph, 139 material: *NxMaterialProfile) -> *NxPrintSim { 140 if (machine as i64) == 0 { return 0 as *NxPrintSim } 141 if (material as i64) == 0 { return 0 as *NxPrintSim } 142 143 let s: *NxPrintSim = (sys_mmap(NX_PRINT_SIM_BYTES)) as *NxPrintSim 144 s.machine = machine 145 s.material = material 146 s.cur_x_q14 = 0 147 s.cur_y_q14 = 0 148 s.cur_z_q14 = 0 149 s.cur_e_q14 = 0 150 s.cumulative_volume_um3 = 0 151 s.cumulative_time_ms = 0 152 s.current_layer_idx = 0 153 154 // Bbox sentinels: min starts HI so any real coord pulls it down; 155 // max starts LO so any real coord pulls it up. 156 s.bbox_min_x_q14 = NX_PRINT_SIM_SENTINEL_HI 157 s.bbox_max_x_q14 = NX_PRINT_SIM_SENTINEL_LO 158 s.bbox_min_y_q14 = NX_PRINT_SIM_SENTINEL_HI 159 s.bbox_max_y_q14 = NX_PRINT_SIM_SENTINEL_LO 160 s.bbox_min_z_q14 = NX_PRINT_SIM_SENTINEL_HI 161 s.bbox_max_z_q14 = NX_PRINT_SIM_SENTINEL_LO 162 163 let cap: i64 = NX_MAGIC_1024 164 s.issues = (sys_mmap(cap * NX_PRINT_SIM_ISSUE_BYTES)) as *NxPrintSimIssue 165 s.n_issues = 0 166 s.issues_cap = cap 167 return s 168} 169 170// Append an issue record. Capacity-bounded. 171func nx_print_sim_append_issue(s: *NxPrintSim, 172 kind: i64, 173 magnitude_q14: i64) -> i64 { 174 if s.n_issues >= s.issues_cap { return -1 } 175 let idx: i64 = s.n_issues 176 let ip: *NxPrintSimIssue = (((s.issues as i64) + idx * NX_PRINT_SIM_ISSUE_BYTES)) as *NxPrintSimIssue 177 ip.kind = kind 178 ip.x_q14 = s.cur_x_q14 179 ip.y_q14 = s.cur_y_q14 180 ip.z_q14 = s.cur_z_q14 181 ip.layer_idx = s.current_layer_idx 182 ip.magnitude_q14 = magnitude_q14 183 s.n_issues = idx + 1 184 return 0 185} 186 187// ===== extrude-move step ========================================== 188// 189// Simulates one G-code extrusion move: 190// - advances position by (dx, dy, dz) 191// - computes move time = length / speed (s) 192// - computes volumetric flow rate = width × height × speed (mm³/s) 193// - if flow_rate > machine.max_flow_mm3s -> HOTEND_FLOW_CEILING issue 194// - if flow_rate > material.typical_max_flow_mm3s -> MATERIAL_FLOW_CEILING issue 195// - accumulates time + volume 196// 197// PHYSICS, not heuristic: flow_rate = width × height × speed is the 198// volumetric rate the hotend must melt. Exceeding the hotend's power 199// budget predicts underextrusion (real, measurable, repeatable). 200// 201// Returns: 202// 0 OK 203// number of issues raised this step (1 or 2) 204 205func nx_print_sim_extrude_move(s: *NxPrintSim, 206 dx_q14: i64, dy_q14: i64, dz_q14: i64, 207 line_width_q14: i64, layer_height_q14: i64, 208 speed_mms: i64) -> i64 { 209 if (s as i64) == 0 { return 0 } 210 if speed_mms <= 0 { return 0 } 211 212 // Advance position. 213 s.cur_x_q14 = s.cur_x_q14 + dx_q14 214 s.cur_y_q14 = s.cur_y_q14 + dy_q14 215 s.cur_z_q14 = s.cur_z_q14 + dz_q14 216 217 // ===== Spaghetti progenitor #1: ENVELOPE_VIOLATION ===== 218 // Machine build volume bounds. Off-bed crash on any axis = sim 219 // predicts spaghetti from that point forward. Real check, no 220 // heuristic. 221 let mq14: i64 = NX_PRINT_SIM_Q14 222 let env_x: i64 = s.machine.build_x_mm * mq14 223 let env_y: i64 = s.machine.build_y_mm * mq14 224 let env_z: i64 = s.machine.build_z_mm * mq14 225 var env_violated: i64 = 0 226 if s.cur_x_q14 < 0 { env_violated = 1 } 227 if s.cur_x_q14 > env_x { env_violated = 1 } 228 if s.cur_y_q14 < 0 { env_violated = 1 } 229 if s.cur_y_q14 > env_y { env_violated = 1 } 230 if s.cur_z_q14 > env_z { env_violated = 1 } 231 if env_violated == 1 { 232 nx_print_sim_append_issue(s, NX_SIM_ISSUE_ENVELOPE_VIOLATION, 0) 233 } 234 235 // ===== Spaghetti progenitor #2: BED_CRASH ===== 236 // Z below 0 = nozzle drives into bed. Real catastrophic failure. 237 if s.cur_z_q14 < 0 { 238 let depth: i64 = 0 - s.cur_z_q14 239 nx_print_sim_append_issue(s, NX_SIM_ISSUE_BED_CRASH, depth) 240 } 241 242 // Update bbox (extrusion moves only -- travel moves don't count 243 // toward the printed-object bbox). 244 if s.cur_x_q14 < s.bbox_min_x_q14 { s.bbox_min_x_q14 = s.cur_x_q14 } 245 if s.cur_x_q14 > s.bbox_max_x_q14 { s.bbox_max_x_q14 = s.cur_x_q14 } 246 if s.cur_y_q14 < s.bbox_min_y_q14 { s.bbox_min_y_q14 = s.cur_y_q14 } 247 if s.cur_y_q14 > s.bbox_max_y_q14 { s.bbox_max_y_q14 = s.cur_y_q14 } 248 if s.cur_z_q14 < s.bbox_min_z_q14 { s.bbox_min_z_q14 = s.cur_z_q14 } 249 if s.cur_z_q14 > s.bbox_max_z_q14 { s.bbox_max_z_q14 = s.cur_z_q14 } 250 251 // Volumetric flow rate (mm³/s) = (width × height × speed). 252 // width_q14 × height_q14 = mm² × Q14² (Q28 representation) 253 // × speed_mms = mm³/s × Q14² 254 // / Q14² = mm³/s (plain integer) 255 // 256 // Overflow guard: width × height (both Q14) max ~ 16384 × 16384 = 257 // 2.7e8. × speed (max ~500 mms) = 1.3e11. Safely in i64. 258 let wh: i64 = line_width_q14 * layer_height_q14 259 let flow_num: i64 = wh * speed_mms 260 let flow_mm3s: i64 = flow_num / NX_PRINT_SIM_Q14_SQ 261 262 var issues_raised: i64 = 0 263 264 // Hotend flow ceiling: machine.max_flow_mm3s is a plain integer. 265 // Compare in plain mm³/s units. 266 if flow_mm3s > s.machine.max_flow_mm3s { 267 let excess: i64 = flow_mm3s - s.machine.max_flow_mm3s 268 // Encode excess as Q14 mm³/s for the magnitude field (callers 269 // expecting Q14 throughout for severity arithmetic). 270 let excess_q14: i64 = excess * NX_PRINT_SIM_Q14 271 nx_print_sim_append_issue(s, NX_SIM_ISSUE_HOTEND_FLOW_CEILING, excess_q14) 272 issues_raised = issues_raised + 1 273 } 274 275 if flow_mm3s > s.material.typical_max_flow_mm3s { 276 let excess_m: i64 = flow_mm3s - s.material.typical_max_flow_mm3s 277 let excess_m_q14: i64 = excess_m * NX_PRINT_SIM_Q14 278 nx_print_sim_append_issue(s, NX_SIM_ISSUE_MATERIAL_FLOW_CEILING, excess_m_q14) 279 issues_raised = issues_raised + 1 280 } 281 282 return issues_raised 283} 284 285// Query accessors. 286func nx_print_sim_n_issues(s: *NxPrintSim) -> i64 { 287 if (s as i64) == 0 { return 0 } 288 return s.n_issues 289} 290 291func nx_print_sim_get_issue(s: *NxPrintSim, i: i64) -> *NxPrintSimIssue { 292 if (s as i64) == 0 { return 0 as *NxPrintSimIssue } 293 if i < 0 { return 0 as *NxPrintSimIssue } 294 if i >= s.n_issues { return 0 as *NxPrintSimIssue } 295 return (((s.issues as i64) + i * NX_PRINT_SIM_ISSUE_BYTES)) as *NxPrintSimIssue 296} 297 298// ===== end-of-print finalize ====================================== 299// 300// Runs the geometric / accumulated-state checks that need the full 301// print bbox. Call after the last move has been simulated. 302// 303// Spaghetti progenitor #3: TIPOVER_RISK 304// Real lever-arm physics. An object tips when the lateral force 305// from XY accel × height-of-CoM exceeds gravity × base-radius. 306// 307// F_horiz × z_com > m × g × base_radius 308// mass × accel × (z_height / 2) > mass × g × base_radius 309// accel × z_height / 2 > g × base_radius 310// z_height / (2 × base_radius) > g / accel 311// 312// g = 9810 mm/s² (Earth gravity). Print accelerations on a CoreXY 313// typically 5000-20000 mm/s². For machine.max_accel_mms2 = 5000, 314// threshold = z_height > 3.92 × base_radius. For accel = 20000, 315// threshold = z_height > 0.98 × base_radius (very strict). 316// 317// This composes machine.max_accel_mms2 (existing field). No 318// heuristic threshold; the threshold IS the physical ratio. 319// 320// Returns count of issues raised by this finalize pass. 321 322const NX_PRINT_SIM_GRAVITY_MMS2: i64 = 9810 323 324func nx_print_sim_finalize(s: *NxPrintSim) -> i64 { 325 if (s as i64) == 0 { return 0 } 326 327 // No bbox = no moves were ever simulated -- nothing to finalize. 328 if s.bbox_min_x_q14 >= s.bbox_max_x_q14 { return 0 } 329 if s.bbox_min_y_q14 >= s.bbox_max_y_q14 { return 0 } 330 if s.bbox_min_z_q14 >= s.bbox_max_z_q14 { return 0 } 331 332 let z_height_q14: i64 = s.bbox_max_z_q14 - s.bbox_min_z_q14 333 let bbox_x_q14: i64 = s.bbox_max_x_q14 - s.bbox_min_x_q14 334 let bbox_y_q14: i64 = s.bbox_max_y_q14 - s.bbox_min_y_q14 335 336 // base_radius = min(bbox_x, bbox_y) / 2 (conservative for tipover; 337 // worst-case lever arm is the shorter base dimension) 338 var base_dim_q14: i64 = bbox_x_q14 339 if bbox_y_q14 < base_dim_q14 { base_dim_q14 = bbox_y_q14 } 340 let base_radius_q14: i64 = base_dim_q14 / 2 341 342 if base_radius_q14 <= 0 { return 0 } // degenerate (zero-area base) 343 344 // Tipover threshold: z_height > (g / accel) × 2 × base_radius 345 // = base_radius × (2g / accel) 346 // 347 // Rewrite as ratio = z_height / base_radius. 348 // Critical = 2g / machine.max_accel_mms2 (real-number; both 349 // dimensionless once units cancel: mm/s² over mm/s² = pure ratio). 350 // Multiply both sides by accel to avoid division: 351 // z_height_q14 × accel > 2g × base_radius_q14 352 let accel: i64 = s.machine.max_accel_mms2 353 if accel <= 0 { return 0 } // shouldn't happen for valid machine 354 355 let lhs: i64 = z_height_q14 * accel 356 let two_g: i64 = 2 * NX_PRINT_SIM_GRAVITY_MMS2 357 let rhs: i64 = two_g * base_radius_q14 358 359 var n_raised: i64 = 0 360 if lhs > rhs { 361 // Magnitude: how much z_height exceeds the safe threshold. 362 // safe_z_q14 = (2g × base_radius_q14) / accel 363 let safe_z_q14: i64 = rhs / accel 364 let excess_q14: i64 = z_height_q14 - safe_z_q14 365 // Record issue at the centre of the bbox (representative). 366 s.cur_x_q14 = (s.bbox_min_x_q14 + s.bbox_max_x_q14) / 2 367 s.cur_y_q14 = (s.bbox_min_y_q14 + s.bbox_max_y_q14) / 2 368 s.cur_z_q14 = s.bbox_max_z_q14 369 nx_print_sim_append_issue(s, NX_SIM_ISSUE_TIPOVER_RISK, excess_q14) 370 n_raised = n_raised + 1 371 } 372 373 return n_raised 374}