code wiki / (root) / nx_bom.nx

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}