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}