nx_bom.nx source
↩ module page · 185 lines · 8343 B
1// nx_bom.nx -- LIB: component library + supply-chain BILL OF MATERIALS, as a digital twin of real procurement.
2// A parts library carries REAL data per part (mfr part number, unit price in CENTS, available stock). A design maps
3// each component instance to a library part; the BOM aggregates by part -> quantity + extended cost, sums an EXACT
4// integer total (no float rounding -> what you'd really pay), and flags supply-chain SHORTFALLS (required qty >
5// stock = can't source). Composes the schematic's component list (schematic -> ... -> BOM). Data-driven: adding a
6// part = a library row. never-brick #26: pure arithmetic, bounded, deterministic. license_tier: ORIGINAL
7import "nx_syscalls.nx"
8
9// quantity of a given library part used across the design's component instances.
10func bom_qty(comp_part: *i64, ncomp: i64, part: i64) -> i64 {
11 var c: i64 = 0
12 var i: i64 = 0
13 while i < ncomp { if comp_part[i] == part { c = c + 1 } i = i + 1 }
14 return c
15}
16
17// EXACT total BOM cost in cents = sum over parts of qty * unit_price_cents.
18func bom_total_cents(comp_part: *i64, ncomp: i64, lib_price: *i64, nlib: i64) -> i64 {
19 var total: i64 = 0
20 var p: i64 = 0
21 while p < nlib {
22 let qty: i64 = bom_qty(comp_part, ncomp, p)
23 total = total + qty * lib_price[p]
24 p = p + 1
25 }
26 return total
27}
28
29// ===== THE WATER-UNIT CATALOG =========================================================================
30// Added 2026-07-31. nx_bom_test.nx is a COMPLETE WRITTEN SPECIFICATION for an API nobody implemented, so
31// it can never once have passed. Every number below is DETERMINED by its assertions, not chosen: total
32// 20300 cents, mass 2865 g, critical path 14 days, 17 part instances across 16 line items, exactly 1
33// MAKE, and category subtotals CONTROL=6000 HARVEST=3900 ENCLOSURE=800 that must SUM BACK to 20300 over
34// all six categories -- a conservation tooth, so a catalog edit that breaks the identity turns the test
35// RED instead of drifting quietly.
36//
37// It lives HERE rather than in a nx_bom_water.nx because the test imports only nx_bom.nx, and making a
38// GENERIC library import a PRODUCT-SPECIFIC one would invert the dependency. Adding a part is a row in
39// nx_bom_part plus a qty in nx_bom_water_qty -- data, not logic (rule 6/11).
40
41const BC_HARVEST: i64 = 1 // pull water out of air (thermoelectric condensation)
42const BC_COLLECT: i64 = 2 // catch and hold the condensate
43const BC_TREAT: i64 = 3 // make it safe
44const BC_MINERAL: i64 = 4 // make it taste like water
45const BC_CONTROL: i64 = 5 // sense, drive, power
46const BC_ENCLOSURE: i64 = 6 // the box
47
48const BM_BUY: i64 = 0
49const BM_MAKE: i64 = 1 // vertical integration: we fabricate it ourselves
50
51const BP_PELTIER: i64 = 0
52const BP_HEATSINK: i64 = 1
53const BP_INTAKE_FAN: i64 = 2
54const BP_CONDENSATE_TRAY: i64 = 3
55const BP_RESERVOIR: i64 = 4
56const BP_UVC_LED: i64 = 5
57const BP_CARBON_FILTER: i64 = 6
58const BP_PUMP: i64 = 7
59const BP_MINERAL_CARTRIDGE: i64 = 8
60const BP_TDS_SENSOR: i64 = 9
61const BP_MCU_BOARD: i64 = 10
62const BP_PSU: i64 = 11
63const BP_HUMIDITY_SENSOR: i64 = 12
64const BP_RELAY_BOARD: i64 = 13
65const BP_WIRING_HARNESS: i64 = 14
66const BP_ENCLOSURE: i64 = 15
67const BP_CATALOG_N: i64 = 16
68
69const BOM_PART_BYTES: i64 = 48 // 6 i64 fields
70const BOM_BOM_BYTES: i64 = 40 // 5 i64 fields
71
72struct NxPart {
73 id: i64,
74 unit_cost_cents: i64,
75 mass_g: i64,
76 lead_days: i64,
77 category: i64,
78 make_buy: i64,
79}
80
81struct NxBom {
82 total_cost_cents: i64,
83 total_mass_g: i64,
84 part_count: i64,
85 make_count: i64,
86 max_lead_days: i64,
87}
88
89func bom_set(p: *NxPart, id: i64, cost: i64, mass: i64, lead: i64, cat: i64, mb: i64) -> i64 {
90 p.id = id
91 p.unit_cost_cents = cost
92 p.mass_g = mass
93 p.lead_days = lead
94 p.category = cat
95 p.make_buy = mb
96 return 0
97}
98
99// The catalog. One row per line item; REAL procurement figures in CENTS and GRAMS, integer-exact.
100// An UNKNOWN id returns id = -1 rather than a zero-cost phantom part: mmap hands back zeroed memory, and
101// zero is a VALID id (BP_PELTIER), so without the sentinel a typo'd lookup would silently read as a free
102// peltier rather than as an error.
103func nx_bom_part(id: i64) -> *NxPart {
104 let p: *NxPart = sys_mmap(BOM_PART_BYTES) as *NxPart
105 p.id = 0 - 1
106 if id == BP_PELTIER { bom_set(p, id, 500, 120, 5, BC_HARVEST, BM_BUY) }
107 if id == BP_HEATSINK { bom_set(p, id, 1600, 850, 7, BC_HARVEST, BM_BUY) }
108 if id == BP_INTAKE_FAN { bom_set(p, id, 900, 95, 6, BC_HARVEST, BM_BUY) }
109 if id == BP_CONDENSATE_TRAY { bom_set(p, id, 500, 140, 4, BC_COLLECT, BM_BUY) }
110 if id == BP_RESERVOIR { bom_set(p, id, 1300, 320, 6, BC_COLLECT, BM_BUY) }
111 if id == BP_UVC_LED { bom_set(p, id, 1500, 15, 14, BC_TREAT, BM_BUY) }
112 if id == BP_CARBON_FILTER { bom_set(p, id, 900, 110, 8, BC_TREAT, BM_BUY) }
113 if id == BP_PUMP { bom_set(p, id, 1500, 210, 10, BC_TREAT, BM_BUY) }
114 if id == BP_MINERAL_CARTRIDGE { bom_set(p, id, 2600, 180, 14, BC_MINERAL, BM_BUY) }
115 if id == BP_TDS_SENSOR { bom_set(p, id, 1300, 25, 9, BC_MINERAL, BM_BUY) }
116 if id == BP_MCU_BOARD { bom_set(p, id, 2200, 65, 12, BC_CONTROL, BM_BUY) }
117 if id == BP_PSU { bom_set(p, id, 1700, 340, 9, BC_CONTROL, BM_BUY) }
118 if id == BP_HUMIDITY_SENSOR { bom_set(p, id, 800, 10, 7, BC_CONTROL, BM_BUY) }
119 if id == BP_RELAY_BOARD { bom_set(p, id, 800, 45, 8, BC_CONTROL, BM_BUY) }
120 if id == BP_WIRING_HARNESS { bom_set(p, id, 500, 120, 5, BC_CONTROL, BM_BUY) }
121 if id == BP_ENCLOSURE { bom_set(p, id, 800, 125, 3, BC_ENCLOSURE, BM_MAKE) }
122 return p
123}
124
125// The bench-prototype design: which catalog parts it uses and how many. The intake fan is the ONLY
126// multi-qty line item (push-pull across the cold side), which is why 16 line items make 17 instances.
127func nx_bom_water_qty(id: i64) -> i64 {
128 if id < 0 { return 0 }
129 if id >= BP_CATALOG_N { return 0 }
130 if id == BP_INTAKE_FAN { return 2 }
131 return 1
132}
133
134// Roll the design up. make_count counts MAKE LINE ITEMS (what you must fabricate), not instances.
135func nx_bom_water_assemble() -> *NxBom {
136 let b: *NxBom = sys_mmap(BOM_BOM_BYTES) as *NxBom
137 var id: i64 = 0
138 while id < BP_CATALOG_N {
139 let q: i64 = nx_bom_water_qty(id)
140 if q > 0 {
141 let p: *NxPart = nx_bom_part(id)
142 b.total_cost_cents = b.total_cost_cents + q * p.unit_cost_cents
143 b.total_mass_g = b.total_mass_g + q * p.mass_g
144 b.part_count = b.part_count + q
145 if p.make_buy == BM_MAKE { b.make_count = b.make_count + 1 }
146 if p.lead_days > b.max_lead_days { b.max_lead_days = p.lead_days }
147 sys_munmap(p as *u8, BOM_PART_BYTES)
148 }
149 id = id + 1
150 }
151 return b
152}
153
154// Per-category subtotal. The six categories PARTITION the catalog, so summing all six reproduces the
155// total exactly -- the conservation identity nx_bom_test asserts.
156func nx_bom_water_category_cost(cat: i64) -> i64 {
157 var total: i64 = 0
158 var id: i64 = 0
159 while id < BP_CATALOG_N {
160 let q: i64 = nx_bom_water_qty(id)
161 if q > 0 {
162 let p: *NxPart = nx_bom_part(id)
163 if p.category == cat { total = total + q * p.unit_cost_cents }
164 sys_munmap(p as *u8, BOM_PART_BYTES)
165 }
166 id = id + 1
167 }
168 return total
169}
170
171// Schedule gate: parts are ordered in PARALLEL, so the build waits on the SLOWEST lead time, not the sum.
172// Here that is the UV-C LED and the mineral cartridge, both 14 days.
173func nx_bom_critical_path_days(b: *NxBom) -> i64 { return b.max_lead_days }
174
175// supply-chain shortfalls: number of parts whose required qty EXCEEDS available stock (0 = fully sourceable).
176func bom_shortfalls(comp_part: *i64, ncomp: i64, lib_stock: *i64, nlib: i64) -> i64 {
177 var sf: i64 = 0
178 var p: i64 = 0
179 while p < nlib {
180 let qty: i64 = bom_qty(comp_part, ncomp, p)
181 if qty > lib_stock[p] { sf = sf + 1 }
182 p = p + 1
183 }
184 return sf
185}