code wiki / (root) / nx_build_plan.nx

nx_build_plan.nx source

↩ module page · 241 lines · 9482 B

1// nx_build_plan.nx -- multi-part / multi-printer / multi-process 2// schedule solver. 3// 4// ===================================================================== 5// SUPERIOR CAPABILITY (per the 2026-05-20 cardinal): 6// 7// NO production additive-manufacturing system schedules across 8// PROCESS CLASSES. Slicers are per-process (Orca/Bambu/Cura/Prusa 9// FDM-only; Chitubox resin-only; Materialise metal-only). Multi-printer 10// coordination today is humans + spreadsheets. 11// 12// This file ships the foundational primitive for cross-process build 13// coordination: each part declares its process_class + estimated 14// print time + post-cure time + dependencies on other parts. Solver 15// outputs per-part start_time minimizing the dependency chain subject 16// to per-process printer availability. 17// 18// Demo target (per [[NISHI_3D_PRINT_MULTI_PROCESS_ROADMAP_2026_05_20]]): 19// 3-process Christus assembly -- FDM PLA body + DIW cement base + 20// FDM metal sintered crown, robot pickup at T+max(all end-times). 21// Before this primitive that schedule existed only on paper; now the 22// substrate computes it. 23// ===================================================================== 24// 25// license_tier: ORIGINAL 26 27import "nx_syscalls.nx" 28import "nx_print_process.nx" 29 30// ===== verdicts ==================================================== 31 32const NX_BUILD_OK: i64 = 0 33const NX_BUILD_ERR_BAD_INPUT: i64 = 1 34const NX_BUILD_ERR_CAPACITY: i64 = 2 35const NX_BUILD_ERR_CYCLE: i64 = 3 36const NX_BUILD_ERR_BAD_DEP: i64 = 4 37 38// ===== struct: build part ========================================= 39// 40// Each part is one printable artifact assigned to ONE process class. 41// dep_bitmap: bit i set means "this part depends on part i finishing 42// (print + post-cure) before THIS part can start." Up to 64 parts 43// per plan in v1 (i64 bitmap). 44 45struct NxBuildPart { 46 id: i64, 47 process_class: i64, 48 estimated_minutes: i64, 49 post_cure_minutes: i64, 50 dep_bitmap: i64, 51 52 // Computed by solver: 53 start_time_min: i64, 54 end_time_min: i64, 55 scheduled: i64, 56} 57 58const NX_BUILD_PART_BYTES: i64 = 64 // 8 fields × 8 59 60// ===== struct: build plan ========================================= 61 62struct NxBuildPlan { 63 parts: *NxBuildPart, 64 n_parts: i64, 65 capacity: i64, 66 printer_free: *i64, // per-process printer-free time (min) 67 total_minutes: i64, 68} 69 70const NX_BUILD_PLAN_BYTES: i64 = 40 // 5 fields × 8 71 72// ===== construction ================================================ 73 74func nx_build_plan_new(capacity: i64) -> *NxBuildPlan { 75 if capacity <= 0 { return 0 as *NxBuildPlan } 76 if capacity > 64 { return 0 as *NxBuildPlan } // i64 bitmap limit 77 78 let plan: *NxBuildPlan = (sys_mmap(NX_BUILD_PLAN_BYTES)) as *NxBuildPlan 79 let parts_buf: *NxBuildPart = (sys_mmap(capacity * NX_BUILD_PART_BYTES)) as *NxBuildPart 80 let printer_free: *i64 = (sys_mmap(NX_PROCESS_N * 8)) as *i64 81 82 plan.parts = parts_buf 83 plan.n_parts = 0 84 plan.capacity = capacity 85 plan.printer_free = printer_free 86 plan.total_minutes = 0 87 88 // Zero-init the per-process printer-free times. 89 var i: i64 = 0 90 while i < NX_PROCESS_N { 91 printer_free[i] = 0 92 i = i + 1 93 } 94 return plan 95} 96 97// Adds a part to the plan. Returns the new part's id (0-based index) 98// or a negative error verdict. 99 100func nx_build_plan_add_part(plan: *NxBuildPlan, 101 process_class: i64, 102 estimated_minutes: i64, 103 post_cure_minutes: i64) -> i64 { 104 if (plan as i64) == 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 105 if nx_process_is_valid(process_class) == 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 106 if estimated_minutes < 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 107 if post_cure_minutes < 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 108 if plan.n_parts >= plan.capacity { return 0 - NX_BUILD_ERR_CAPACITY } 109 110 let new_id: i64 = plan.n_parts 111 let pp: *NxBuildPart = (((plan.parts as i64) + new_id * NX_BUILD_PART_BYTES)) as *NxBuildPart 112 pp.id = new_id 113 pp.process_class = process_class 114 pp.estimated_minutes = estimated_minutes 115 pp.post_cure_minutes = post_cure_minutes 116 pp.dep_bitmap = 0 117 pp.start_time_min = 0 118 pp.end_time_min = 0 119 pp.scheduled = 0 120 121 plan.n_parts = plan.n_parts + 1 122 return new_id 123} 124 125// Marks part `child_id` as depending on `parent_id`. The child can't 126// start until the parent's print + post-cure are complete. 127// 128// Self-dependencies + bad indices return error verdicts. 129 130func nx_build_part_set_dep(plan: *NxBuildPlan, 131 child_id: i64, 132 parent_id: i64) -> i64 { 133 if (plan as i64) == 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 134 if child_id < 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 135 if child_id >= plan.n_parts { return 0 - NX_BUILD_ERR_BAD_INPUT } 136 if parent_id < 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 137 if parent_id >= plan.n_parts { return 0 - NX_BUILD_ERR_BAD_INPUT } 138 if child_id == parent_id { return 0 - NX_BUILD_ERR_BAD_DEP } 139 140 let child: *NxBuildPart = (((plan.parts as i64) + child_id * NX_BUILD_PART_BYTES)) as *NxBuildPart 141 let one: i64 = 1 142 let bit: i64 = one << parent_id 143 child.dep_bitmap = child.dep_bitmap | bit 144 return NX_BUILD_OK 145} 146 147// ===== schedule solver ============================================= 148// 149// Greedy topological scheduling: iterate parts; for each unscheduled 150// part whose deps are all scheduled, compute its start_time as 151// MAX(max-dep-end-time, printer-free-time-for-this-process) and 152// advance the printer's free time. 153// 154// Returns the total wall-clock time (max end_time across all parts) 155// or a negative cycle-detection verdict if no part can be scheduled 156// in a pass (indicating a dependency cycle). 157// 158// O(N²) for N parts; ample for the 64-part v1 limit. 159 160func nx_build_plan_solve(plan: *NxBuildPlan) -> i64 { 161 if (plan as i64) == 0 { return 0 - NX_BUILD_ERR_BAD_INPUT } 162 if plan.n_parts <= 0 { return 0 } 163 164 // Reset per-process printer-free times (idempotent solve). 165 var pi: i64 = 0 166 while pi < NX_PROCESS_N { 167 plan.printer_free[pi] = 0 168 pi = pi + 1 169 } 170 var pj: i64 = 0 171 while pj < plan.n_parts { 172 let pp: *NxBuildPart = (((plan.parts as i64) + pj * NX_BUILD_PART_BYTES)) as *NxBuildPart 173 pp.scheduled = 0 174 pp.start_time_min = 0 175 pp.end_time_min = 0 176 pj = pj + 1 177 } 178 179 var n_scheduled: i64 = 0 180 var max_iters: i64 = plan.n_parts * 2 + 4 181 var iter: i64 = 0 182 while iter < max_iters { 183 if n_scheduled >= plan.n_parts { iter = max_iters } 184 if iter < max_iters { 185 var progressed: i64 = 0 186 var idx: i64 = 0 187 while idx < plan.n_parts { 188 let part: *NxBuildPart = (((plan.parts as i64) + idx * NX_BUILD_PART_BYTES)) as *NxBuildPart 189 if part.scheduled == 0 { 190 // Check all deps are scheduled. 191 var deps_ready: i64 = 1 192 var max_dep_end: i64 = 0 193 var di: i64 = 0 194 let deps: i64 = part.dep_bitmap 195 while di < plan.n_parts { 196 let one: i64 = 1 197 let bit: i64 = one << di 198 if (deps & bit) != 0 { 199 let dep: *NxBuildPart = (((plan.parts as i64) + di * NX_BUILD_PART_BYTES)) as *NxBuildPart 200 if dep.scheduled == 0 { deps_ready = 0; di = plan.n_parts } 201 if deps_ready == 1 { 202 if dep.end_time_min > max_dep_end { 203 max_dep_end = dep.end_time_min 204 } 205 } 206 } 207 di = di + 1 208 } 209 if deps_ready == 1 { 210 let pc: i64 = part.process_class 211 let printer_ready: i64 = plan.printer_free[pc] 212 var start: i64 = max_dep_end 213 if printer_ready > start { start = printer_ready } 214 part.start_time_min = start 215 part.end_time_min = start + part.estimated_minutes + part.post_cure_minutes 216 part.scheduled = 1 217 plan.printer_free[pc] = part.end_time_min 218 n_scheduled = n_scheduled + 1 219 progressed = 1 220 } 221 } 222 idx = idx + 1 223 } 224 if progressed == 0 { return 0 - NX_BUILD_ERR_CYCLE } 225 iter = iter + 1 226 } 227 } 228 229 if n_scheduled < plan.n_parts { return 0 - NX_BUILD_ERR_CYCLE } 230 231 // Compute total = max end_time across all parts. 232 var total: i64 = 0 233 var k: i64 = 0 234 while k < plan.n_parts { 235 let pp: *NxBuildPart = (((plan.parts as i64) + k * NX_BUILD_PART_BYTES)) as *NxBuildPart 236 if pp.end_time_min > total { total = pp.end_time_min } 237 k = k + 1 238 } 239 plan.total_minutes = total 240 return total 241}