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}