diff --git a/artifacts/printing/rear-posts-2026-10-05/placements.json b/artifacts/printing/rear-posts-2026-10-05/placements.json new file mode 100644 index 00000000000..dc8ff85f117 --- /dev/null +++ b/artifacts/printing/rear-posts-2026-10-05/placements.json @@ -0,0 +1,125 @@ +{ + "name": "rear-and-upper-posts", + "items": [ + { + "id": "M140", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 10, + 8 + ] + }, + { + "id": "G140", + "rotation_degrees": [ + 90, + 90, + 0 + ], + "minimum_xy_mm": [ + 10, + 72 + ] + }, + { + "id": "CU-1-1", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 10, + 110 + ] + }, + { + "id": "CU-11", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 45, + 110 + ] + }, + { + "id": "CU1-1", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 80, + 110 + ] + }, + { + "id": "CU11", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 115, + 110 + ] + }, + { + "id": "CP-1-1", + "rotation_degrees": [ + 0, + 90, + 0 + ], + "minimum_xy_mm": [ + 15, + 150 + ] + }, + { + "id": "CP-11", + "rotation_degrees": [ + 0, + 90, + 0 + ], + "minimum_xy_mm": [ + 50, + 150 + ] + }, + { + "id": "CP1-1", + "rotation_degrees": [ + 0, + 90, + 0 + ], + "minimum_xy_mm": [ + 85, + 150 + ] + }, + { + "id": "CP11", + "rotation_degrees": [ + 0, + 90, + 0 + ], + "minimum_xy_mm": [ + 120, + 150 + ] + } + ] +} diff --git a/artifacts/printing/rear-posts-2026-10-05/readback.json b/artifacts/printing/rear-posts-2026-10-05/readback.json new file mode 100644 index 00000000000..733e4e137af --- /dev/null +++ b/artifacts/printing/rear-posts-2026-10-05/readback.json @@ -0,0 +1,76 @@ +[ + { + "plate": "rear-and-upper-posts", + "objects": [ + "M140", + "G140", + "CU-1-1", + "CU-11", + "CU1-1", + "CU11", + "CP-1-1", + "CP-11", + "CP1-1", + "CP11" + ], + "sha256": "6c7fbb7afd5e6c9aaecc252c693df416e0cd170e8523e667e3a2cd49d0c9a5d1", + "seconds": 56110, + "grams": 200.4, + "support_fraction": 0.0431805612335687, + "support_mm_by_object": { + "CU11": 174.20300999999913, + "CU-11": 174.20300999999915, + "G140": 783.3089899999517, + "M140": 1413.358740000025, + "CU-1-1": 181.0683000000004, + "CU1-1": 174.20300999999915, + "CP-1-1": 0.0, + "CP-11": 0.0, + "CP1-1": 0.0, + "CP11": 0.0 + }, + "extrusion_mm_by_feature": { + "Support": 2507.9353999998425, + "Outer wall": 15872.752379999927, + "Inner wall": 37541.26568999349, + "Bottom surface": 185.691060000001, + "Brim": 222.78522999999848, + "Gap infill": 905.6723700000108, + "Internal solid infill": 2837.5871899997996, + "Sparse infill": 5518.038489999481, + "Internal Bridge": 468.8338699999985, + "Top surface": 474.9304900000076, + "Support interface": 392.40965999999634, + "Overhang wall": 109.81214000000061, + "Bridge": 130.12676999999977 + }, + "path_bounds": { + "project": "rear-and-upper-posts.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 3.472, + 20.516, + 174.681, + 158.517 + ], + "arc_moves_checked": 49053, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + } +] diff --git a/artifacts/printing/rear-posts-2026-10-05/start-receipt.json b/artifacts/printing/rear-posts-2026-10-05/start-receipt.json new file mode 100644 index 00000000000..10a6de4fb81 --- /dev/null +++ b/artifacts/printing/rear-posts-2026-10-05/start-receipt.json @@ -0,0 +1,26 @@ +{ + "observed_at_utc": "2026-10-05T00:50:44Z", + "printer": "printer-02", + "project_sha256": "6c7fbb7afd5e6c9aaecc252c693df416e0cd170e8523e667e3a2cd49d0c9a5d1", + "remote_name": "gunbc-6c7fbb7afd5e6c9aaecc252c693df416e0cd170e8523e667e3a2cd49d0c9a5d1.3mf", + "members": [ + "M140", + "G140", + "CU-1-1", + "CU-11", + "CU1-1", + "CU11", + "CP-1-1", + "CP-11", + "CP1-1", + "CP11" + ], + "standing": "running-confirmed", + "publisher": "acknowledgement-timeout", + "reconciliation": "first read-only observation matched exact file and RUNNING", + "remaining_minutes": 935, + "print_error": 0, + "start_republished": false, + "operator_token_deleted": true, + "inspection": "pending-completion-and-physical-inspection" +} diff --git a/artifacts/printing/socket-redesign-2026-10-04/placement-candidates.json b/artifacts/printing/socket-redesign-2026-10-04/placement-candidates.json new file mode 100644 index 00000000000..7762c8a92dc --- /dev/null +++ b/artifacts/printing/socket-redesign-2026-10-04/placement-candidates.json @@ -0,0 +1,272 @@ +[ + { + "name": "flat-p1", + "items": [ + { + "id": "P1", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 5, + 5 + ] + } + ] + }, + { + "name": "flat-p2", + "items": [ + { + "id": "P2", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 5, + 5 + ] + } + ] + }, + { + "name": "upright-batch", + "items": [ + { + "id": "P1", + "rotation_degrees": [ + 0, + -90, + 0 + ], + "minimum_xy_mm": [ + 8, + 12 + ] + }, + { + "id": "P2", + "rotation_degrees": [ + 0, + 90, + 0 + ], + "minimum_xy_mm": [ + 8, + 52 + ] + }, + { + "id": "S1", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 12, + 95 + ] + }, + { + "id": "S2", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 55, + 95 + ] + } + ] + }, + { + "name": "rear-edge-batch", + "items": [ + { + "id": "P1", + "rotation_degrees": [ + 0, + 0, + 0 + ], + "minimum_xy_mm": [ + 8, + 12 + ] + }, + { + "id": "P2", + "rotation_degrees": [ + 0, + 0, + 0 + ], + "minimum_xy_mm": [ + 8, + 52 + ] + }, + { + "id": "S1", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 12, + 95 + ] + }, + { + "id": "S2", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 55, + 95 + ] + } + ] + }, + { + "name": "overnight-batch", + "items": [ + { + "id": "P1", + "rotation_degrees": [ + 0, + 0, + 0 + ], + "minimum_xy_mm": [ + 8, + 12 + ] + }, + { + "id": "P2", + "rotation_degrees": [ + 0, + 0, + 0 + ], + "minimum_xy_mm": [ + 8, + 52 + ] + }, + { + "id": "S1", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 12, + 95 + ] + }, + { + "id": "S2", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 55, + 95 + ] + }, + { + "id": "K3", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 100, + 100 + ] + }, + { + "id": "K4", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 125, + 100 + ] + }, + { + "id": "K6", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 100, + 130 + ] + }, + { + "id": "K7", + "rotation_degrees": [ + -90, + 0, + 0 + ], + "minimum_xy_mm": [ + 125, + 130 + ] + } + ] + }, + { + "name": "sockets", + "items": [ + { + "id": "S1", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 20, + 20 + ] + }, + { + "id": "S2", + "rotation_degrees": [ + 90, + 0, + 0 + ], + "minimum_xy_mm": [ + 65, + 20 + ] + } + ] + } +] diff --git a/artifacts/printing/socket-redesign-2026-10-04/preparation-manifest.json b/artifacts/printing/socket-redesign-2026-10-04/preparation-manifest.json new file mode 100644 index 00000000000..b1154dff12d --- /dev/null +++ b/artifacts/printing/socket-redesign-2026-10-04/preparation-manifest.json @@ -0,0 +1,68 @@ +{ + "source_commit": "1a2191a7201c03d9b88f40acfc0f1ad3dcf5584f", + "standing": "unpowered fit prototype; orientation comparison, not a structural release", + "inventory": { + "replacements": [ + "P1", + "P2" + ], + "new": [ + "S1", + "S2", + "K3", + "K4", + "K6", + "K7" + ], + "preserve_existing": [ + "P3", + "P4", + "E1", + "E2", + "B140L", + "B140R" + ], + "retired": [ + "K1", + "K2" + ] + }, + "files": { + "assembly.json": "8537a8bdda3ea0cc156db61d3fca1b21e93008f60a9b685ed9f1e2542e201591", + "prepare.py": "106688bae02c2ccb68dbf6558915384b638fc62608e326ec4d7d3b6f6643a72e", + "supported.json": "d679c046df9ef9e17352b962a6397807ef9e5772a72759a8628f0b05bab8afef", + "unsupported.json": "94272b7beeb91f867d48f68119626e8ecce0f7ca6e82feeccc04a10ce9549fdb", + "upright.json": "019635af53af3ba1ab8f43fedb5b1578cb05f1112cc85ce6ff1ea0b4cdf4a468", + "placement-candidates.json": "fda2d953c12e7364a48def6b48de291a8166e5432a52f4925626c1f0dca2d727", + "readback.py": "c960932d5d87f076c13bc9b27f8ce008393f111e71e9026f6283055f47df508b", + "witness-results.txt": "9887b52fbfe18ca3f595549060d36037649c8a338c8fb7fa0338f05c99980436" + }, + "intended_plate_members": [ + "P1", + "P2", + "S1", + "S2", + "K3", + "K4", + "K6", + "K7" + ], + "selection_policy": "Prefer one overnight bed-clear cycle for compatible unpowered prototype parts; retain flat alternatives with lower total material and machine time.", + "selected_project_sha256": "ebd858a69bb1fdf8c4395d0e8fc5d6ffe0ad94d3da84ad236e56064226eb6f6d", + "validation": { + "witnesses": [ + "PASS rear_bank_has_bed_sized_interfaces_and_separate_socket_retention", + "PASS posts_fit_bed_and_invalid_height_cannot_produce_solids", + "PASS middle_pop_needs_support_and_top_pop_does_not_borrow_it", + "PASS rear_panels_have_no_geometry_below_the_rail_feet" + ], + "cad": "34 valid positive solids; printed solids connected; socket/tray and socket/arm intersections zero; candidate objects within bed and mutually disjoint", + "slice": "One plate; eight named objects; unit rigid transforms; PLA AMS slot 1; A1 mini 0.4; 220/65 C; four walls and 20 percent gyroid; G-code checksum and deposited XY bounds passed; sockets/pins have zero attributed support extrusion", + "unqualified": [ + "loaded strength in upright orientation", + "socket bearing and creep", + "physical bolt engagement", + "bed adhesion of full-height panels" + ] + } +} diff --git a/artifacts/printing/socket-redesign-2026-10-04/readback.json b/artifacts/printing/socket-redesign-2026-10-04/readback.json new file mode 100644 index 00000000000..b4ff9aa8af1 --- /dev/null +++ b/artifacts/printing/socket-redesign-2026-10-04/readback.json @@ -0,0 +1,345 @@ +[ + { + "plate": "flat-p1", + "objects": [ + "P1" + ], + "sha256": "8b2d06ff1c62551e620625320e9ca7f0f9b2dec46f66cd73cc299f8ba9a19436", + "seconds": 13468, + "grams": 110.47, + "support_fraction": 0.19202319438111473, + "support_mm_by_object": { + "P1": 7107.817209999873 + }, + "extrusion_mm_by_feature": { + "Brim": 39.25636000000001, + "Support": 4384.926979999898, + "Outer wall": 1989.7836599999985, + "Inner wall": 6362.920300000025, + "Bottom surface": 120.63829000000104, + "Internal solid infill": 9105.659139999412, + "Sparse infill": 7664.336570002776, + "Support interface": 2722.8902299999686, + "Overhang wall": 156.43353000000027, + "Bridge": 1173.5397399999993, + "Internal Bridge": 1848.4630599999405, + "Top surface": 1446.5618300000563 + }, + "path_bounds": { + "project": "flat-p1.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 6.565, + 3.688, + 173.474, + 176.312 + ], + "arc_moves_checked": 113578, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + }, + { + "plate": "flat-p2", + "objects": [ + "P2" + ], + "sha256": "f5c8e44f96494e80625f209ad4a17b22a8144289931260d13537fda22276f043", + "seconds": 12788, + "grams": 104.79, + "support_fraction": 0.18899801892542564, + "support_mm_by_object": { + "P2": 6635.636689999975 + }, + "extrusion_mm_by_feature": { + "Brim": 40.33870000000001, + "Support": 4195.138669999891, + "Outer wall": 1989.7582500000083, + "Inner wall": 6370.295520000039, + "Bottom surface": 120.69251000000102, + "Internal solid infill": 8520.413849999228, + "Sparse infill": 7096.911670002082, + "Support interface": 2440.4980199999513, + "Overhang wall": 162.90333000000032, + "Bridge": 1092.3245399999942, + "Internal Bridge": 1723.4529800000203, + "Top surface": 1356.8288100000264 + }, + "path_bounds": { + "project": "flat-p2.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 16.526, + 3.688, + 161.927, + 176.312 + ], + "arc_moves_checked": 105165, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + }, + { + "plate": "overnight-batch", + "objects": [ + "P1", + "P2", + "S1", + "S2", + "K3", + "K4", + "K6", + "K7" + ], + "sha256": "ebd858a69bb1fdf8c4395d0e8fc5d6ffe0ad94d3da84ad236e56064226eb6f6d", + "seconds": 65334, + "grams": 256.63, + "support_fraction": 0.09327656553947446, + "support_mm_by_object": { + "P1": 3990.363999999155, + "P2": 4033.2482200002623, + "S1": 0.0, + "S2": 0.0, + "K3": 0.0, + "K4": 0.0, + "K6": 0.0, + "K7": 0.0 + }, + "extrusion_mm_by_feature": { + "Brim": 436.2654700000004, + "Support": 7615.634810000071, + "Outer wall": 14461.548310000138, + "Inner wall": 42476.19716999722, + "Gap infill": 444.90495999999405, + "Bottom surface": 266.87240000000037, + "Internal solid infill": 4036.333590001026, + "Sparse infill": 14838.138619997639, + "Internal Bridge": 502.29543, + "Top surface": 451.0465200000047, + "Support interface": 407.9774099999989, + "Overhang wall": 33.281879999999916, + "Bridge": 49.096600000000144 + }, + "path_bounds": { + "project": "overnight-batch.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 27.807, + 6.569, + 152.197, + 175.306 + ], + "arc_moves_checked": 160304, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + }, + { + "plate": "rear-edge-batch", + "objects": [ + "P1", + "P2", + "S1", + "S2" + ], + "sha256": "e9f22e9a526bc51c4ff6ecf7753169c8b22117b0e31c7e1fc436522ba1cb4eeb", + "seconds": 64077, + "grams": 253.21, + "support_fraction": 0.09453809237306848, + "support_mm_by_object": { + "P1": 3990.363999999155, + "P2": 4033.250040000262 + }, + "extrusion_mm_by_feature": { + "Brim": 342.6094499999995, + "Outer wall": 14233.400170000155, + "Inner wall": 41977.64666999682, + "Bottom surface": 244.9387599999979, + "Support": 7615.636620000069, + "Gap infill": 444.90497999999405, + "Internal solid infill": 3854.6179600009586, + "Sparse infill": 14789.359429997676, + "Internal Bridge": 468.96364999999844, + "Top surface": 409.3238600000029, + "Support interface": 407.97741999999886, + "Overhang wall": 33.281879999999916, + "Bridge": 49.09654000000015 + }, + "path_bounds": { + "project": "rear-edge-batch.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 46.001, + 6.569, + 133.999, + 175.307 + ], + "arc_moves_checked": 157463, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + }, + { + "plate": "sockets", + "objects": [ + "S1", + "S2" + ], + "sha256": "653ab3fd3904b9c42e4a0cfdc30e81ce31c087821ede4c0def7a1ff18badd1bf", + "seconds": 1710, + "grams": 11.53, + "support_fraction": 0.0, + "support_mm_by_object": {}, + "extrusion_mm_by_feature": { + "Brim": 36.37027999999997, + "Outer wall": 575.5053600000034, + "Inner wall": 1304.2074999999932, + "Bottom surface": 138.73486000000022, + "Internal solid infill": 1009.3784700000017, + "Sparse infill": 150.20226000000105, + "Internal Bridge": 199.76742999999996, + "Gap infill": 251.9059999999992, + "Top surface": 176.11693999999747, + "Support": 0.0, + "Support interface": 0.0 + }, + "path_bounds": { + "project": "sockets.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 60.3, + 67.3, + 119.7, + 112.7 + ], + "arc_moves_checked": 2335, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + } + ] + }, + { + "plate": "upright-batch", + "objects": [ + "P1", + "P2", + "S1", + "S2" + ], + "sha256": "f9cb4e6b02b47fec3755fcaceed85bcfb6c0a45afbd5b108a718df37b64de9b4", + "seconds": 60950, + "grams": 271.17, + "support_fraction": 0.18648133614009466, + "support_mm_by_object": { + "P1": 8557.300879998935, + "P2": 8392.887619998504 + }, + "extrusion_mm_by_feature": { + "Brim": 233.8546600000002, + "Outer wall": 12509.640480000498, + "Inner wall": 38252.24960999682, + "Bottom surface": 138.76768000000007, + "Support": 14621.922180001462, + "Gap infill": 473.72311999999897, + "Internal solid infill": 4802.47321000023, + "Support interface": 2328.2663199999815, + "Sparse infill": 15013.75974000074, + "Overhang wall": 201.28986, + "Bridge": 442.95680999999814, + "Internal Bridge": 997.9948699999939, + "Top surface": 877.9269200000089 + }, + "path_bounds": { + "project": "upright-batch.3mf", + "extrusion_centerline_bounds_xy_mm": [ + 14.444, + 18.259, + 161.653, + 162.541 + ], + "arc_moves_checked": 168099, + "edge_allowance_mm": 0.25 + }, + "warnings": [ + { + "msg": "bed_temperature_too_high_than_filament", + "level": "3", + "error_code": "1000C001" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "2", + "error_code": "10018003" + }, + { + "msg": "not_support_traditional_timelapse", + "level": "3", + "error_code": "1000C003" + } + ] + } +] diff --git a/artifacts/printing/socket-redesign-2026-10-04/start-receipt.json b/artifacts/printing/socket-redesign-2026-10-04/start-receipt.json new file mode 100644 index 00000000000..b86abc0aed6 --- /dev/null +++ b/artifacts/printing/socket-redesign-2026-10-04/start-receipt.json @@ -0,0 +1,23 @@ +{ + "observed_at_utc": "2026-10-05T00:34:03Z", + "printer": "printer-01", + "project_sha256": "ebd858a69bb1fdf8c4395d0e8fc5d6ffe0ad94d3da84ad236e56064226eb6f6d", + "remote_name": "gunbc-ebd858a69bb1fdf8c4395d0e8fc5d6ffe0ad94d3da84ad236e56064226eb6f6d.3mf", + "members": [ + "P1", + "P2", + "S1", + "S2", + "K3", + "K4", + "K6", + "K7" + ], + "standing": "running-confirmed", + "publisher": "acknowledgement-timeout-after-20-seconds", + "reconciliation": "first read-only observation matched exact file and RUNNING", + "remaining_minutes": 1088, + "start_republished": false, + "operator_token_deleted": true, + "inspection": "pending-completion-and-physical-inspection" +} diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index a8fe0d9da2a..5bd0d4fb757 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -1,6 +1,10 @@ module extdeps.boards.asrock_rack -import extdeps.boards.types { BaseboardCatalogRow, DimmPopulation } +import extdeps.boards.types { + BaseboardCatalogRow, DimmPopulation, + BaseboardPhysicalProfile, + PowerInputPopulationStanding, PowerInputMinimumOneStated, +} import extdeps.vendor.asrock_rack { asrock_rack } import extdeps.bmc.webui.nbd_proxy { BmcwebNbdProxyEndpointVariant, VmSlotIndexEndpoint, NbdSlotEndpoint } import extdeps.bmc.types { @@ -31,8 +35,11 @@ import extdeps.firmware.types { InstantFlashRom, firmware_semantic_version, } -import std.measure { Micrometer, micrometer, ByteSize, MegatransfersPerSecond, byte_size, celsius, megatransfers_per_second, percent, positive_celsius_delta, positive_measure_count } -import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, FactCitation } +import std.measure { + Micrometer, micrometer, ByteSize, MegatransfersPerSecond, byte_size, celsius, megatransfers_per_second, percent, positive_celsius_delta, positive_measure_count, + Watt, watt, Millimeter, millimeter, Celsius, Ampere, Volt, volt, +} +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, FactCitation, CitedFigureStanding, TranscribedUncited } import std.coordinate_frame { CoordinateFrameIdentity, coordinate_frame_identity } import std.affine_space { point2 } import std.orthogonal_geometry { AxisAlignedRectangle2, OrthogonalConstruction, axis_aligned_rectangle2 } @@ -701,8 +708,11 @@ data asrock_altrad8ud_standoff_figure_authority: ExternalAuthority = ExternalAut // WHAT IS KNOWN about a standard location on this specific board. Every arm names its evidence // route, so the grade is readable from the constructor rather than from prose beside it. +// Image-position estimates (used by Jade review) do not establish a location; unlike +// a correspondence to exact standard coordinates, they cannot admit a standoff. type BoardLocationStanding = HoleCorrespondenceInferred + | HolePositionEstimatedFromImage | HoleAbsenceExplicitlyDirected | HoleNotObservedInDrawing | HolePhysicallyConfirmedPresent @@ -723,6 +733,7 @@ type StandoffPlacementDecision // physically-confirmed arms below are unpopulated today and are the shape the measurement lands in. fn standoff_placement(standing: BoardLocationStanding) -> StandoffPlacementDecision { match standing { + HolePositionEstimatedFromImage => StandoffProhibited { cause: NoAdmittedBoardHole } HoleCorrespondenceInferred => StandoffAdmitted HolePhysicallyConfirmedPresent => StandoffAdmitted HoleAbsenceExplicitlyDirected => StandoffProhibited { cause: ProductSaysRemove } @@ -897,3 +908,195 @@ data asrock_altrad8ud_open_mounting_unknowns: List = [ MountingHoleGroundBondUnknown, StandoffHeightAndPadUnmeasured, ] + + +// ============================================================================================= +// THE VENDOR MANUAL, READ FIRST-PARTY ON 2026-10-04 FROM AN OPERATOR-SUPPLIED COPY. +// +// extdeps_external_authority_anchor has named download.asrock.com/Manual/ALTRAD8UD-1L2T.pdf since +// this module was written, and until today every row below this line that cited it was either a +// relayed reading (the power-interface figures came from a routed review, as the former gigabyte +// co-location recorded) or a reading of the geometry pages only. The copy read today is Version +// 1.20, published Jul. 2025, 95 pages. Every row in this section names the manual section it was +// read from, so a later revision of the manual can be diffed against the row rather than against +// a memory of it. +// ============================================================================================= +type HeldManualRevision { + version_label: NonEmptyStr + published_label: NonEmptyStr + page_count: Nat + authority: ExternalAuthority +} + +data asrock_altrad8ud_manual_held: HeldManualRevision = HeldManualRevision { + version_label: "1.20", + published_label: "Jul. 2025", + page_count: 95, + authority: extdeps_external_authority_anchor, +} + +// ---- 1.2 Specifications, the comparison profile. Moved here from extdeps.boards.gigabyte, where +// it was co-located for a two-board comparison and carried a routed-review reading. The manual +// confirms that reading on every field it covers: one 4-pin ATX PSU signal connector with a +// 24-pin adapter cable, three 8-pin ATX 12V connectors supporting 12 V DC-in, no 24-pin socket. +data asrock_altrad8ud_1l2t_physical_profile: BaseboardPhysicalProfile = BaseboardPhysicalProfile { + model_label: "ASRock Rack ALTRAD8UD-1L2T", + socket_count: 1, + max_tdp_per_socket: watt(count: 250), + form_factor_label: "Deep Micro-ATX", + width: millimeter(count: 244), + depth: millimeter(count: 267), + dimm_slots: 8, + max_data_rate: megatransfers_per_second(count: 3200), + atx_24pin_count: 0, + eps_8pin_count: 3, + onboard_10gbe_ports: 2, + dedicated_management_ports: 1, + pcie_slots: 4, +} + +// max_tdp_per_socket is NOT in the manual. Section 1.2 names the processor family and socket and +// states no socket power; the 250 W figure arrived with the relayed profile and is kept as the +// value the comparison witness already consumes, graded below so no reader takes it for a manual +// row. pcie_slots is 4 per section 2.5 (PCIE4..PCIE7), correcting the relayed 3. +data asrock_altrad8ud_max_tdp_standing: CitedFigureStanding = TranscribedUncited { + read_obligation: "Section 1.2 of manual v1.20 states no socket power limit. Ground 250 W against the ASRock Rack product page or the Ampere Altra Max datasheet's socket TDP ceiling, or delete the field from the comparison.", +} + +// ---- 2.8 ATX PSU / DC-IN power connections, the section this program was blocked on. +// +// Verbatim, section 2.8: "This motherboard supports both +12V DC and ATX power input." The table +// then marks the 12V 8pin connector O under DC-IN and O under ATX PSU, and the ATX 4pin X under +// DC-IN and O under ATX PSU "(with the bundled ATX 24pin-to-4pin converter cable)". +// +// Verbatim, section 2.7 (ATX 12V Power Connectors): "This motherboard provides three 8-pin ATX +// 12V power connectors. Under the lowest power supply, user can freely use ATX12V1, ATX12V2 or +// ATX12V3 connector." +// +// Verbatim, section 2.7 (Micro-Fit ATX 4Pin Power Connector): "The motherboard provides one 4-pin +// power/signal connector which is a required input for ATX power source. ... For DC-IN 12V +// application, it is not necessary to use this ATX 4-PIN power connector." +type BoardPowerInputMode = DcIn12V | AtxPsu + +type PowerInputRequirement + = InputRequired + | InputRequiredViaBundledConverter + | InputNotUsed + +type BoardPowerConnectorKind = Atx12V8Pin | MicroFitAtx4PinSignal + +// The 2.8 table as a total function: every (mode, connector) cell is one arm, so a fourth cell +// cannot appear without a decision. +fn asrock_altrad8ud_power_input_requirement(mode: BoardPowerInputMode, kind: BoardPowerConnectorKind) -> PowerInputRequirement { + match mode { + DcIn12V => match kind { Atx12V8Pin => InputRequired MicroFitAtx4PinSignal => InputNotUsed } + AtxPsu => match kind { Atx12V8Pin => InputRequired MicroFitAtx4PinSignal => InputRequiredViaBundledConverter } + } +} + +// Pinout of each 8-pin ATX 12V connector, section 2.7 figure: pins 1-4 GND, pins 5-8 12V. Four +// 12 V contacts per connector is the fact a current-per-connector derivation needs. +data asrock_altrad8ud_atx12v_ground_pins: List = [1, 2, 3, 4] +data asrock_altrad8ud_atx12v_twelve_volt_pins: List = [5, 6, 7, 8] +data asrock_altrad8ud_atx12v_nominal_input: Volt = volt(count: 12) + +// Pinout of ATX4PIN1, section 2.7 figure: pin 1 ATX_PWROK, pin 2 GND, pin 3 ATX_+5VSB, pin 4 PSON#. +type MicroFit4PinSignal = AtxPwrOk | SignalGround | Atx5Vsb | PsOnActiveLow +fn asrock_altrad8ud_atx4pin_signal(pin: Nat) -> MicroFit4PinSignal? { + if pin == 1 { Present { value: AtxPwrOk } } + else { if pin == 2 { Present { value: SignalGround } } + else { if pin == 3 { Present { value: Atx5Vsb } } + else { if pin == 4 { Present { value: PsOnActiveLow } } else { none } } } } +} + +// The population standing, upgraded from Unresolved by the quote above and no further: one input +// suffices "under the lowest power supply", and the manual states no rule for higher power. What +// a given processor needs above one is a derivation from the connector's rated current, owned by +// the consumer (product.printed_chassis.node_power_feed), never a number authored here. +data asrock_altrad8ud_required_input_population: PowerInputPopulationStanding = PowerInputMinimumOneStated { + available_inputs: 3, + stated_condition: "Under the lowest power supply, user can freely use ATX12V1, ATX12V2 or ATX12V3 connector. (manual v1.20 section 2.7)", +} + +// STANDBY UNDER DC-IN IS AN INFERENCE, typed as one. The 4-pin carries ATX_+5VSB as an INPUT in +// ATX mode and is not used in DC-IN mode; section 1.2 lists a Standby Power LED (5VSB) and section +// 1.5 names SB_PWR1 "STB PWR Ready"; section 3.3.8 reads a 5VSB rail. So under DC-IN the board must +// derive its standby rail from the 12 V input on-board. The manual never says so in those words. +data asrock_altrad8ud_dc_in_standby_standing: CitedFigureStanding = TranscribedUncited { + read_obligation: "Confirm on a held board under 12 V DC-IN with ATX4PIN1 unconnected that SB_PWR1 lights and the BMC boots; that observation is the receipt, recorded in the fleet layer.", +} + +// Package contents, section 1.1: the ATX 4P-to-24P cable is 8 cm, so an ATX-mode supply must sit +// within that reach of ATX4PIN1 or the converter is extended. DC-IN mode has no such tether. +data asrock_altrad8ud_bundled_converter_cable_length: Millimeter = millimeter(count: 80) + +// ---- 2.7 System fan connectors: five 6-pin headers FAN1..FAN5 (layout items 17-21). Pinout from +// the figure, pin 1 first: GND, FAN_VOLTAGE, FAN_SPEED_SENSOR1, FAN_SPEED_CONTROL, +// FAN_SPEED_SENSOR2, NC. A 4-pin PWM fan occupies pins 1-4. Header current limit is NOT stated. +data asrock_altrad8ud_system_fan_header_count: Nat = 5 +data asrock_altrad8ud_fan_header_pin_count: Nat = 6 +data asrock_altrad8ud_fan_header_current_limit_standing: CitedFigureStanding = TranscribedUncited { + read_obligation: "Manual v1.20 states no per-header current limit for FAN1-FAN5. Ground against an ASRock Rack support statement before loading one header with a daisy-chained fan bank.", +} + +// ---- 1.6 Rear I/O, left to right as the panel figure numbers them. +type RearIoPort + = RearVga + | RearLan2TenGbe + | RearLan1TenGbeSharedNic + | RearUsb3Pair12 + | RearLan3OneGbe + | RearUsb3Pair34 + | RearIpmiLan + | RearUidSwitch +data asrock_altrad8ud_rear_io_ports: List = [RearVga, RearLan2TenGbe, RearLan1TenGbeSharedNic, RearUsb3Pair12, RearLan3OneGbe, RearUsb3Pair34, RearIpmiLan, RearUidSwitch] + +// Which rear ports carry an Ethernet cable in service. The 1L2T has two 10GbE (LAN1 shared with +// the BMC NC-SI path), one 1GbE and one dedicated IPMI port; how many a deployment plugs is a +// fleet decision, so the row carries the available count and nothing about the plugged count. +data asrock_altrad8ud_rear_rj45_count: Nat = 4 + +// ---- 1.4 Layout: WHICH EDGE each service connector sits on, read from the to-scale layout figure +// the standoff correspondence above already relies on. The board frame is x along the rear I/O +// edge and y the depth from that edge (asrock_altrad8ud_orientation). The figure shows the rear +// I/O at the left and the three ATX12V connectors, ATX4PIN1 and FAN1-FAN5 at the RIGHT edge, i.e. +// the edge FARTHEST from rear I/O. That is the fact the cassette harness inherits: in a cassette +// whose rear I/O faces the rear fan bank, the board's power and fan headers are at the FRONT. +type BoardEdge = RearIoEdge | FrontEdge | SideEdgeNearDimmBankA | SideEdgeNearPcieSlots + +type LayoutConnectorEdge { + designator: NonEmptyStr + layout_item: Nat + edge: BoardEdge +} + +data asrock_altrad8ud_service_connector_edges: List = [ + LayoutConnectorEdge { designator: "ATX4PIN1", layout_item: 6, edge: FrontEdge }, + LayoutConnectorEdge { designator: "ATX12V1", layout_item: 7, edge: FrontEdge }, + LayoutConnectorEdge { designator: "PSU_SMB1", layout_item: 8, edge: FrontEdge }, + LayoutConnectorEdge { designator: "ATX12V2", layout_item: 13, edge: FrontEdge }, + LayoutConnectorEdge { designator: "ATX12V3", layout_item: 15, edge: FrontEdge }, + LayoutConnectorEdge { designator: "FAN1", layout_item: 17, edge: FrontEdge }, + LayoutConnectorEdge { designator: "FAN2", layout_item: 18, edge: FrontEdge }, + LayoutConnectorEdge { designator: "FAN3", layout_item: 19, edge: FrontEdge }, + LayoutConnectorEdge { designator: "FAN4", layout_item: 20, edge: FrontEdge }, + LayoutConnectorEdge { designator: "FAN5", layout_item: 21, edge: FrontEdge }, + LayoutConnectorEdge { designator: "PANEL1", layout_item: 25, edge: SideEdgeNearPcieSlots }, + LayoutConnectorEdge { designator: "AUX_PANEL1", layout_item: 26, edge: SideEdgeNearPcieSlots }, +] + +fn asrock_altrad8ud_connector_edge(designator: NonEmptyStr) -> BoardEdge? { + fold(asrock_altrad8ud_service_connector_edges, init: none, f: (acc, row) => + if row.designator == designator { Present { value: row.edge } } else { acc }) +} + +// ---- 1.2 Environment: operation 10-35 C, non-operation -40-70 C. The ambient ceiling is an +// input to the exhaust-temperature derivation, not a chassis target. +data asrock_altrad8ud_operating_ambient_min: Celsius = celsius(count: 10) +data asrock_altrad8ud_operating_ambient_max: Celsius = celsius(count: 35) + +// ---- 2.7 PSU SMBus header PSU_SMB1 (5-pin: ALERT, SMBCLK, +3V, SMBDATA, GND), "PSU SMBus +// monitors the status of the power supply, fan and system temperature." One board can host a +// PMBus supply; a supply shared by several boards can be monitored by at most one of them through +// this header, which is a station design fact rather than a board one. +data asrock_altrad8ud_psu_smbus_header_pin_count: Nat = 5 diff --git a/dag/extdeps/boards/gigabyte.dag b/dag/extdeps/boards/gigabyte.dag index c67f0371a8d..5670be68a37 100644 --- a/dag/extdeps/boards/gigabyte.dag +++ b/dag/extdeps/boards/gigabyte.dag @@ -8,7 +8,12 @@ import std.measure { SquareMillimeter, square_millimeter, square_millimeter_count, MegatransfersPerSecond, megatransfers_per_second, } -import extdeps.boards.types { BaseboardCatalogRow } +import extdeps.boards.types { + BaseboardCatalogRow, + BaseboardPhysicalProfile, + board_profile_power_headers_match, board_profile_area, + board_profile_sockets_per_area_permille, board_profile_max_socket_power, +} import extdeps.vendor.gigabyte { gigabyte } import std.decl_ref { DeclarationRef, WholeDeclaration } @@ -64,23 +69,7 @@ data gigabyte_mp72_hb0_bmc_firmware_standing_note: NonEmptyStr = "BMC firmware f // Vendor-page facts, independently verified 2026-08-11 against the Gigabyte product page and // reseller listings rather than accepted from a routed review that asserted them. Every field is a // READING, never a measurement: no MP72-HB0 has been held, powered, or measured by this operator, -// and none is cited by any PhysicalAsset. -type BaseboardPhysicalProfile { - model_label: NonEmptyStr - socket_count: Nat - max_tdp_per_socket: Watt - form_factor_label: NonEmptyStr - width: Millimeter - depth: Millimeter - dimm_slots: Nat - max_data_rate: MegatransfersPerSecond - atx_24pin_count: Nat - eps_8pin_count: Nat - onboard_10gbe_ports: Nat - dedicated_management_ports: Nat - pcie_slots: Nat -} - +// and none is cited by any PhysicalAsset. The profile SHAPE now lives in extdeps.boards.types. data gigabyte_mp72_hb0_physical_profile: BaseboardPhysicalProfile = BaseboardPhysicalProfile { model_label: "Gigabyte MP72-HB0 (rev. 3.0)", socket_count: 2, @@ -97,72 +86,10 @@ data gigabyte_mp72_hb0_physical_profile: BaseboardPhysicalProfile = BaseboardPhy pcie_slots: 4, } -// CORRECTED 2026-08-11 after a routed review challenged the previous row, which said -// atx_24pin_count 1 / eps_8pin_count 2. That was FALSE: asserted by this session with confidence, -// carried as SEED DEBT 3 "pending pin-verification", and never closed before a witness was built on -// it. The corrected reading: THREE 8-pin ATX12V inputs (ATX12V1/2/3) plus a 4-pin Micro-Fit signal -// connector (ATX4PIN1), NO 24-pin socket, a bundled 24-pin-to-4-pin converter for ATX-supply -// operation, and direct +12 V DC input in which the 4-pin signal path is unused. -// -// PROVENANCE CORRECTION, 2026-08-11, and it matters more than the numbers. An earlier version of -// this comment and its commit message said this was "verified against the vendor manual". It was -// not: every fetch of download.asrock.com, asrockrack.com and a mirror was refused by this -// environment's egress policy, so the manual has never been opened here. The correction came from a -// routed review — a real source, and NOT a vendor document. -// -// Writing "verified against the vendor manual" over a reviewer's reading is the SAME defect one -// level up: the first mistake invented a fact, this one invented its provenance. A wrong number -// gets caught when someone opens the manual; a correct number wearing a manual's authority is never -// re-checked, because the row says the checking already happened. The standing below records what -// actually happened. -data asrock_altrad8ud_1l2t_physical_profile: BaseboardPhysicalProfile = BaseboardPhysicalProfile { - model_label: "ASRock Rack ALTRAD8UD-1L2T", - socket_count: 1, - max_tdp_per_socket: watt(count: 250), - form_factor_label: "deep micro-ATX", - width: millimeter(count: 244), - depth: millimeter(count: 267), - dimm_slots: 8, - max_data_rate: megatransfers_per_second(count: 3200), - atx_24pin_count: 0, - eps_8pin_count: 3, - onboard_10gbe_ports: 2, - dedicated_management_ports: 1, - pcie_slots: 3, -} - -data asrock_altrad8ud_power_interface_note: NonEmptyStr = "Three 8-pin ATX12V inputs plus one 4-pin Micro-Fit signal connector; no 24-pin socket. ATX mode uses the bundled 24-pin-to-4-pin converter. Direct +12 V DC input is also supported and does not use the 4-pin signal path." - -// This URI is where the answer LIVES, not a document that has been read. Previously named -// asrock_altrad8ud_manual_authority, which asserted the opposite: an ExternalAuthority reads as a -// citation, and citing an unopened PDF manufactures confidence the row has not earned. Renamed to -// say what it is — an unread target — so that when someone fetches it, upgrading the standing is a -// deliberate edit rather than a silent no-op against a line that already claimed it. -data asrock_altrad8ud_manual_unread_target: ExternalAuthority = ExternalAuthority { - uri: Uri { - scheme: Https - locator: "download.asrock.com/Manual/ALTRAD8UD-1L2T.pdf" - } -} - -data asrock_altrad8ud_power_interface_provenance_note: NonEmptyStr = "The power-interface reading above came from a routed review, not from the vendor manual. The manual is unread: fetches of download.asrock.com and asrockrack.com were refused by this environment's egress policy. Treat the figures as a strong lead pending a first-party read, and do not describe them anywhere as manual-verified." - -type PowerInputPopulationStanding - = PowerInputPopulationSpecified { required_inputs: Nat } - | PowerInputPopulationUnresolved { available_inputs: Nat } - -// UNRESOLVED and load-bearing. The board offers three 8-pin inputs; how many must be POPULATED for -// a given processor and workload is not established by anything read so far. A cable diagram must -// not draw one, two or three connected inputs until a vendor statement or a fixture measurement -// closes this — drawing a plausible number is exactly how the previous false inventory reached a -// witness. -data asrock_altrad8ud_required_input_population: PowerInputPopulationStanding = PowerInputPopulationUnresolved { available_inputs: 3 } - -// The ASRock profile above is authored in THIS module rather than beside the ASRock catalog row, -// against the usual placement rule: it exists only so the two boards can be compared by one shape -// in one place. If a second consumer needs it, move it to extdeps.boards.asrock_rack and leave a -// reference — do not copy it. -data asrock_profile_placement_note: NonEmptyStr = "Co-located for comparison only; migrate to extdeps.boards.asrock_rack on second consumer." +// THE ASROCK ROW MOVED TO extdeps.boards.asrock_rack on 2026-10-04, the day a second consumer +// arrived and the day the vendor manual was finally read first-party. The correction history that +// stood here (a false 24-pin reading, then a provenance that claimed a manual nobody had opened) is +// carried with the row in its own module, where the manual read now grounds it. type BoardProcurementStanding = CatalogReadOnly @@ -205,19 +132,3 @@ data gigabyte_mp72_hb0_purchase_gate: List = [ // prices near USD 1445 that this session has not verified. Authoring either here would launder a // hypothesis into the catalog. The gate row above says what must be read first. data gigabyte_mp72_hb0_landed_cost_standing_note: NonEmptyStr = "Landed cost unmodelled: no confirmed tariff classification, no verified quote, no freight or brokerage figure." - -fn board_profile_power_headers_match(a: BaseboardPhysicalProfile, b: BaseboardPhysicalProfile) -> Bool { - a.atx_24pin_count == b.atx_24pin_count && a.eps_8pin_count == b.eps_8pin_count -} - -fn board_profile_area(p: BaseboardPhysicalProfile) -> SquareMillimeter { - square_millimeter(count: millimeter_count(m: p.width) * millimeter_count(m: p.depth)) -} - -fn board_profile_sockets_per_area_permille(p: BaseboardPhysicalProfile) -> Nat { - p.socket_count * 1000000 / square_millimeter_count(m: board_profile_area(p: p)) -} - -fn board_profile_max_socket_power(p: BaseboardPhysicalProfile) -> Watt { - watt(count: p.socket_count * watt_count(w: p.max_tdp_per_socket)) -} diff --git a/dag/extdeps/boards/types.dag b/dag/extdeps/boards/types.dag index cb83042e15e..0a6b8277990 100644 --- a/dag/extdeps/boards/types.dag +++ b/dag/extdeps/boards/types.dag @@ -1,6 +1,13 @@ module extdeps.boards.types import std.types { Bool, Int, List, NonEmptyStr } +import std.nat { Nat } +import std.measure { + Watt, watt, watt_count, + Millimeter, millimeter_count, + SquareMillimeter, square_millimeter, square_millimeter_count, + MegatransfersPerSecond, +} import extdeps.bmc.endpoint { BmcFirmwareFamily, BmcEndpoint } import extdeps.vendor { Vendor } import extdeps.hardware { Hardware } @@ -87,3 +94,51 @@ fn admit_population( PopulationUnsupported { slots: candidate } } } + +// A BOARD'S PHYSICAL PROFILE, the one shape two boards are compared by. Authored in +// extdeps.boards.gigabyte while it had one consumer; moved here on its second (the printed-chassis +// power feed reads the ASRock row), exactly as the co-location note there said to do. Every field +// is a READING of a vendor surface, never a measurement of a held board. +type BaseboardPhysicalProfile { + model_label: NonEmptyStr + socket_count: Nat + max_tdp_per_socket: Watt + form_factor_label: NonEmptyStr + width: Millimeter + depth: Millimeter + dimm_slots: Nat + max_data_rate: MegatransfersPerSecond + atx_24pin_count: Nat + eps_8pin_count: Nat + onboard_10gbe_ports: Nat + dedicated_management_ports: Nat + pcie_slots: Nat +} + +fn board_profile_power_headers_match(a: BaseboardPhysicalProfile, b: BaseboardPhysicalProfile) -> Bool { + a.atx_24pin_count == b.atx_24pin_count && a.eps_8pin_count == b.eps_8pin_count +} + +fn board_profile_area(p: BaseboardPhysicalProfile) -> SquareMillimeter { + square_millimeter(count: millimeter_count(m: p.width) * millimeter_count(m: p.depth)) +} + +fn board_profile_sockets_per_area_permille(p: BaseboardPhysicalProfile) -> Nat { + p.socket_count * 1000000 / square_millimeter_count(m: board_profile_area(p: p)) +} + +fn board_profile_max_socket_power(p: BaseboardPhysicalProfile) -> Watt { + watt(count: p.socket_count * watt_count(w: p.max_tdp_per_socket)) +} + +// HOW MANY 12 V INPUTS A BOARD NEEDS POPULATED, graded by what the vendor actually said. +// +// Three arms because the vendor manual answers three different questions. Specified: the manual +// states a number for the configuration. MinimumOneStated: the manual says ANY ONE input suffices +// under its lowest-power condition and says nothing about the rule above it, so one is a floor +// and the ceiling is the consumer's derivation from connector ratings, never a vendor number. +// Unresolved: nothing read says anything, and a cable diagram may not draw a count. +type PowerInputPopulationStanding + = PowerInputPopulationSpecified { required_inputs: Nat } + | PowerInputMinimumOneStated { available_inputs: Nat, stated_condition: NonEmptyStr } + | PowerInputPopulationUnresolved { available_inputs: Nat } diff --git a/dag/extdeps/connector/molex_mini_fit_jr.dag b/dag/extdeps/connector/molex_mini_fit_jr.dag new file mode 100644 index 00000000000..123fffd8b11 --- /dev/null +++ b/dag/extdeps/connector/molex_mini_fit_jr.dag @@ -0,0 +1,76 @@ +module extdeps.connector.molex_mini_fit_jr + +import std.types { NonEmptyStr, List } +import std.nat { Nat } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.measure { Ampere, ampere, Micrometer, micrometer } +import extdeps.vendor.molex { molex } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, CitedToAuthority, TranscribedUncited } +import extdeps.uri { Uri, Https } + +// THE 4.2 MM PITCH POWER CONNECTOR FAMILY an ATX12V 8-pin input is built on. +// +// The ASRock manual names an 8-pin ATX12V input but not its manufacturer or +// header part. These family test ratings are screening candidates; they do not +// establish the installed header, terminal, wire or PCB current capability. +// Standard and HCS terminal figures are not interchangeable. The standard row +// uses the primary 8-circuit / 18 AWG table; the HCS row remains unverified. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.molex.com/en-us/products/connectors/wire-to-board-connectors/mini-fit-jr-connectors" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.connector.molex_mini_fit_jr", + decl_name: "mini_fit_jr_family", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +type MiniFitJrTerminalRating { + terminal_series_label: NonEmptyStr + wire_gauge_awg: Nat + rated_current_per_circuit: Ampere + standing: CitedFigureStanding +} + +type MiniFitJrFamily { + family_label: NonEmptyStr + pitch: Micrometer + ratings: List +} + +data mini_fit_jr_current_table_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"www.molex.com/content/dam/molex/molex-dot-com/products/automated/en-us/productspecificationpdf/555/5556/PS-5556-001-001.pdf"}} +// Rev H2 (2026-06-22), section 4.3: 7-10 circuits, 18 AWG, brass 7 A; phosphor bronze 6 A. +// Use the lower of those two test figures for screening; actual installed terminal/header remains unidentified. +data mini_fit_jr_standard_terminal_rating: MiniFitJrTerminalRating = MiniFitJrTerminalRating { + terminal_series_label: "5556/5558 8-circuit 18 AWG screening: lower material rating", + wire_gauge_awg: 18, + rated_current_per_circuit: ampere(count: 6), + standing: CitedToAuthority {authority:mini_fit_jr_current_table_authority}, +} + +data mini_fit_jr_hcs_terminal_rating: MiniFitJrTerminalRating = MiniFitJrTerminalRating { + terminal_series_label: "45750 high-current terminal", + wire_gauge_awg: 16, + rated_current_per_circuit: ampere(count: 13), + standing: TranscribedUncited { + read_obligation: "Open Molex PS-45750-001 and confirm 13 A max per circuit with 45750 terminals crimped to 16 AWG, plus its derating table; relayed from distributor datasheet summaries on 2026-10-04.", + }, +} + +data mini_fit_jr_family: MiniFitJrFamily = MiniFitJrFamily { + family_label: "Molex Mini-Fit Jr.", + pitch: micrometer(count: 4200), + ratings: [mini_fit_jr_standard_terminal_rating, mini_fit_jr_hcs_terminal_rating], +} + +data mini_fit_jr_vendor_note: NonEmptyStr = "Family owner extdeps.vendor.molex; header part on the ALTRAD8UD-1L2T unread." diff --git a/dag/extdeps/cooling/dynatron.dag b/dag/extdeps/cooling/dynatron.dag index 35df435c133..55e6238bbbb 100644 --- a/dag/extdeps/cooling/dynatron.dag +++ b/dag/extdeps/cooling/dynatron.dag @@ -1,7 +1,8 @@ module extdeps.cooling.dynatron import std.types { NonEmptyStr, List } -import std.measure { watt, rack_unit } +import std.nat { Nat } +import std.measure { Micrometer, micrometer, watt, rack_unit, Gram, gram, Percent, percent, RevolutionsPerMinute, rpm, MillilitrePerSecond, millilitre_per_second } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.cooling.types { CpuCoolerCatalogRow } import extdeps.cpu.types { @@ -9,7 +10,7 @@ import extdeps.cpu.types { import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.vendor.dynatron { dynatron } import extdeps.vendor.ampere { ampere_vendor } -import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, CitedToAuthority, TranscribedUncited } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -40,3 +41,38 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope { first_citation: extdeps_external_authority_anchor, further_citations: [] } + +// Manufacturer active W1 envelope; installation pose belongs to the product assembly. +data dynatron_w1_active_length: Micrometer = micrometer(count: 141100) +data dynatron_w1_active_width: Micrometer = micrometer(count: 84000) +data dynatron_w1_active_height: Micrometer = micrometer(count: 72000) + +// --------------------------------------------------------------------------------------------- +// MASS AND AIRFLOW: manufacturer page directly read 2026-10-04. +// +// Mass is the figure the stack-mechanics roster sums: 612 +/- 10 g active (fan fitted), 512 +/- 10 g +// passive. Airflow is the cooler's own 60 mm fan at three duty points, carried at mL/s by floor +// conversion of the listed CFM (1 CFM = 471.947 mL/s): 11.91 -> 5621, 28.38 -> 13393, 58.31 -> +// 27519. The cooler's fan is a second air mover in the cassette beside the rear bank; what it +// contributes to EXHAUST flow depends on ducting the chassis has not qualified, so the exhaust +// derivation reads the rear bank only and names this row as the unmodelled addition. +// --------------------------------------------------------------------------------------------- +data dynatron_w1_mass_active: Gram = gram(count: 612) +data dynatron_w1_mass_passive: Gram = gram(count: 512) +data dynatron_w1_mass_standing: CitedFigureStanding = CitedToAuthority {authority:extdeps_external_authority_anchor} + +type CoolerFanDutyPoint { + duty: Percent + speed: RevolutionsPerMinute + airflow: MillilitrePerSecond +} + +data dynatron_w1_fan_duty_points: List = [ + CoolerFanDutyPoint { duty: percent(20), speed: rpm(2300), airflow: millilitre_per_second(count: 5621) }, + CoolerFanDutyPoint { duty: percent(50), speed: rpm(5400), airflow: millilitre_per_second(count: 13393) }, + CoolerFanDutyPoint { duty: percent(100), speed: rpm(11000), airflow: millilitre_per_second(count: 27519) }, +] +data dynatron_w1_fan_duty_points_standing: CitedFigureStanding = CitedToAuthority {authority:extdeps_external_authority_anchor} + +// First-party page read 2026-10-04; mass tolerance is not zero. +data dynatron_w1_mass_tolerance: Gram = gram(count:10) diff --git a/dag/extdeps/cooling/noctua_ampere.dag b/dag/extdeps/cooling/noctua_ampere.dag new file mode 100644 index 00000000000..a73b8cd29d8 --- /dev/null +++ b/dag/extdeps/cooling/noctua_ampere.dag @@ -0,0 +1,12 @@ +module extdeps.cooling.noctua_ampere +import std.types {NonEmptyStr} +import std.measure {Micrometer,micrometer,Gram,gram} +import std.decl_ref {DeclarationRef,WholeDeclaration} +import extdeps.external_authority {ExternalAuthority,ExternalModelScope,ExternalSubjectRef} +import extdeps.uri {Uri,Https} +data extdeps_external_authority_anchor:ExternalAuthority=ExternalAuthority {uri:Uri {scheme:Https,locator:"www.noctua.at/en/products/nh-u14s-amp-4926/specifications"}} +type DualFanCoolerEnvelope {model:NonEmptyStr,height:Micrometer,width:Micrometer,depth:Micrometer,mass:Gram} +// Includes BOTH supplied fans. Not shipping weight or heatsink-only dimensions. +// Vendor lists 5U compatibility and discontinued standing; no 4U fit assertion. +data nh_u14s_amp_4926:DualFanCoolerEnvelope=DualFanCoolerEnvelope {model:"NH-U14S AMP-4926",height:micrometer(count:165000),width:micrometer(count:150000),depth:micrometer(count:111000),mass:gram(count:1136)} +data extdeps_model_scope:ExternalModelScope=ExternalModelScope {subject:ExternalSubjectRef {declaration:DeclarationRef {module_path:"extdeps.cooling.noctua_ampere",decl_name:"nh_u14s_amp_4926",field:WholeDeclaration}},first_citation:extdeps_external_authority_anchor,further_citations:[]} diff --git a/dag/extdeps/cpu/ampere.dag b/dag/extdeps/cpu/ampere.dag index 02f3dcbec08..61a1e7173bd 100644 --- a/dag/extdeps/cpu/ampere.dag +++ b/dag/extdeps/cpu/ampere.dag @@ -1,9 +1,9 @@ module extdeps.cpu.ampere import std.types { String, List } -import std.measure { ByteSize, byte_size, celsius, hardware_thread_count, hertz, watt } +import std.measure { Gram, gram, ByteSize, byte_size, celsius, hardware_thread_count, hertz, watt } import extdeps.external_authority { ExternalAuthority, FactCitation } -import std.decl_ref { decl_field_ref } +import std.decl_ref { decl_field_ref, decl_ref } import extdeps.uri { Uri, Https } import extdeps.publication { HeldSpecificationSupersessionStanding, NewerRevisionPubliclyServed, HeldRevisionIsCurrentPublicCopy } import extdeps.toolchain.types { Aarch64 } @@ -353,3 +353,12 @@ data ampere_cpu_catalog_rows: List = [ altra_q6430_catalog, altra_q8030_catalog, ] + +// Processor Mass Specification, section 4, p.17 of Altra Max Rev A1 issues +// 1.15/1.25/1.30. Typical package mass, not a guaranteed tolerance interval. +// The non-Max Altra's 112 g must not be substituted for this installed family. +data ampere_altra_max_typical_package_mass: Gram = gram(count:114) +data ampere_altra_max_package_mass_citation: FactCitation = FactCitation { + authority: ampere_altra_max_rev_a1_datasheet_authority, + fact: decl_ref(module_path:"extdeps.cpu.ampere",decl_name:"ampere_altra_max_typical_package_mass"), +} diff --git a/dag/extdeps/fans/arctic.dag b/dag/extdeps/fans/arctic.dag index d028bc5fc18..7179b1bf399 100644 --- a/dag/extdeps/fans/arctic.dag +++ b/dag/extdeps/fans/arctic.dag @@ -1,19 +1,20 @@ module extdeps.fans.arctic import std.types { List } -import std.measure { micrometer, millimeter, percent, rpm } +import std.measure {Celsius,celsius} +import std.measure { Micrometer, micrometer, millimeter, percent, rpm } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.fans.types { FanMountingPattern, fan_mounting_pattern, - ConventionUnverified, + ConventionUnverified, ReadFromVendorDrawing, FanCatalogRow, FanSpeedRangePublished, FanStopsBelowPwm, FluidDynamicBearing, } import extdeps.vendor.arctic { arctic } -import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, CitedToAuthority, TranscribedUncited } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -106,3 +107,57 @@ data arctic_p8_mounting_pattern: FanMountingPattern = fan_mounting_pattern( convention: "80 mm axial fan square mounting pattern, unverified against a held part", }, ) + +// Published axial frame depth, separate from the conventional mounting-hole pattern. +data arctic_p8_frame_depth: Micrometer = micrometer(count: 25000) + +// --------------------------------------------------------------------------------------------- +// THE FIGURES A CHASSIS MODEL CONSUMES BEYOND THE SPEED RANGE, each with its own standing. +// +// Airflow, mass, current, cable length and ambient range cite the primary PDF +// directly read 2026-10-04. Maximum free-air flow is not installed airflow. +data arctic_p8_rated_power: Milliwatt = milliwatt(count: 1080) +data arctic_p8_rated_power_standing: CitedFigureStanding = CitedToAuthority { authority: extdeps_external_authority_anchor } + +data arctic_p8_spec_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"www.arctic.de/media/84/14/68/1697784882/Spec_Sheet_P8_PWM_PST_EN.pdf"}} +// First-party PDF read 2026-10-04: 39.76 m3/h, not reseller 40.3. Maximum free-air flow. +data arctic_p8_operating_ambient_max: Celsius = celsius(count:40) +data arctic_p8_published_airflow: MillilitrePerSecond = millilitre_per_second(count: 11044) +data arctic_p8_published_airflow_standing: CitedFigureStanding = CitedToAuthority {authority:arctic_p8_spec_authority} + +data arctic_p8_mass: Gram = gram(count: 81) +data arctic_p8_mass_standing: CitedFigureStanding = CitedToAuthority {authority:arctic_p8_spec_authority} + +data arctic_p8_rated_current: Milliampere = milliampere(count: 90) +data arctic_p8_rated_current_standing: CitedFigureStanding = CitedToAuthority {authority:arctic_p8_spec_authority} + +data arctic_p8_cable_length: Millimeter = millimeter(400) +data arctic_p8_cable_length_standing: CitedFigureStanding = CitedToAuthority {authority:arctic_p8_spec_authority} + +// PWM SHARING TECHNOLOGY: the "PST" in the model name. Each fan carries a pass-through 4-pin +// socket, so a bank of P8 PWM PST fans chains fan-to-fan and reaches the board on ONE header +// cable. The chassis harness census counts the bank as one header cable plus (n - 1) short links +// because of this row. The maximum chain length ARCTIC supports is not stated on the listings +// read; the header's own current limit is the board's unresolved row, not this one. +type PwmSharingChain + = PstPassThrough { max_chained_standing: CitedFigureStanding } + | NoPassThrough + +data arctic_p8_pwm_sharing: PwmSharingChain = PstPassThrough { + max_chained_standing: TranscribedUncited { + read_obligation: "Read ARCTIC's PST statement for the maximum number of fans per chain; the model name and the listings establish the pass-through exists, not its limit.", + }, +} + +// Publisher mounting drawings visually read on 2026-10-04. Nominal dimensions; +// drawing tolerance is +/- 0.3 mm on these frame edges and pitches. Printed fit is separate. +data arctic_p14_mounting_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"support.arctic.de/products/p14-pwm-pst/techdocs/140mm_fan-Mounting_hole_pattern.pdf"}} +data arctic_p14_mounting_pattern: FanMountingPattern = fan_mounting_pattern(frame_edge:micrometer(count:140000),hole_pitch:micrometer(count:125000),hole_clearance_diameter:micrometer(count:4300),evidence:ReadFromVendorDrawing {authority:arctic_p14_mounting_authority}) +data arctic_p12_mounting_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"support.arctic.de/products/p12-pwm-pst/techdocs/120mm_fan-Mounting_hole_pattern.pdf"}} +data arctic_p12_mounting_pattern: FanMountingPattern = fan_mounting_pattern(frame_edge:micrometer(count:120000),hole_pitch:micrometer(count:105000),hole_clearance_diameter:micrometer(count:4300),evidence:ReadFromVendorDrawing {authority:arctic_p12_mounting_authority}) +data arctic_f9_mounting_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"support.arctic.de/products/f9-pwm-pst/techdocs/92mm_fan-Mounting_hole_pattern.pdf"}} +data arctic_f9_mounting_pattern: FanMountingPattern = fan_mounting_pattern(frame_edge:micrometer(count:92000),hole_pitch:micrometer(count:82500),hole_clearance_diameter:micrometer(count:4300),evidence:ReadFromVendorDrawing {authority:arctic_f9_mounting_authority}) + +// P14 PWM PST sheet, technical specifications p.1, directly read 2026-10-04. +data arctic_p14_spec_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"www.arctic.de/media/1d/98/04/1693306037/Spec_Sheet_P14_PWM_PST_EN.pdf"}} +data arctic_p14_frame_depth: Micrometer = micrometer(count:27000) diff --git a/dag/extdeps/fans/types.dag b/dag/extdeps/fans/types.dag index c3b4549c3df..7805c33535d 100644 --- a/dag/extdeps/fans/types.dag +++ b/dag/extdeps/fans/types.dag @@ -299,6 +299,7 @@ type FanMountingPattern sole_constructor { // before anything is designed around it. type FanMechanicalEvidence = ConventionUnverified { convention: NonEmptyStr } + | ReadFromVendorDrawing { authority: ExternalAuthority } | RelayedFromVendorDocument { document: NonEmptyStr } | MeasuredFromHeldPart { measured_by: NonEmptyStr } @@ -322,6 +323,7 @@ fn fan_mounting_pattern( fn fan_mounting_pattern_is_verified(p: FanMountingPattern) -> Bool { match p.evidence { MeasuredFromHeldPart { measured_by: _ } => true + ReadFromVendorDrawing { authority: _ } => false RelayedFromVendorDocument { document: _ } => false ConventionUnverified { convention: _ } => false } diff --git a/dag/extdeps/physics/dry_air.dag b/dag/extdeps/physics/dry_air.dag new file mode 100644 index 00000000000..596897500ae --- /dev/null +++ b/dag/extdeps/physics/dry_air.dag @@ -0,0 +1,56 @@ +module extdeps.physics.dry_air + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.measure { + Celsius, celsius, + GramPerCubicMetre, gram_per_cubic_metre, + JoulePerKilogramKelvin, joule_per_kilogram_kelvin, +} +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, TranscribedUncited } +import extdeps.uri { Uri, Https } + +// DRY AIR AT ONE ATMOSPHERE: the two figures an exhaust-temperature-rise derivation needs, at the +// reference temperature the table states. Density falls about 3 percent from 20 C to 30 C and cp +// is flat to a tenth of a percent over the whole chassis range, so one row at 20 C serves a +// derivation whose other inputs are far coarser; a consumer wanting the 35 C row reads it from the +// cited table rather than scaling this one. +// +// Relayed on 2026-10-04 through search summaries of the Penn State ME 420 properties table +// (density 1.204 kg/m3, cp 1007 J/kg K at 20 C) and standard references stating cp 1005 J/kg K at +// 25 C. The row carries 1005 and names the 1007 reading in its obligation. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.me.psu.edu/cimbala/me420/Links/Table_A_9_CC_Properties_of_Air.pdf" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.physics.dry_air", + decl_name: "dry_air_at_20c", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +type DryAirProperties { + reference_temperature: Celsius + density: GramPerCubicMetre + specific_heat_cp: JoulePerKilogramKelvin + standing: CitedFigureStanding +} + +data dry_air_at_20c: DryAirProperties = DryAirProperties { + reference_temperature: celsius(count: 20), + density: gram_per_cubic_metre(count: 1204), + specific_heat_cp: joule_per_kilogram_kelvin(count: 1005), + standing: TranscribedUncited { + read_obligation: "Open Table A-9 (properties of air at 1 atm) and confirm 1.204 kg/m3 and cp at 20 C; the table reads 1007 J/kg K where this row carries 1005, a 0.2 percent difference below every other input's grain.", + }, +} diff --git a/dag/extdeps/portwell/aptsa_627211_b4.dag b/dag/extdeps/portwell/aptsa_627211_b4.dag new file mode 100644 index 00000000000..a961a697ce5 --- /dev/null +++ b/dag/extdeps/portwell/aptsa_627211_b4.dag @@ -0,0 +1,25 @@ +module extdeps.portwell.aptsa_627211_b4 +import std.types {Int} +import std.measure {Micrometer,micrometer} +import std.decl_ref {DeclarationRef,WholeDeclaration} +import extdeps.external_authority {ExternalAuthority,ExternalModelScope,ExternalSubjectRef,CitedFigureStanding,CitedToAuthority} +import extdeps.uri {Uri,Https} + +data extdeps_external_authority_anchor:ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"portwell.com/pdf/server/APTSA-627211-B4.pdf"}} +// Portwell's March 2022 datasheet p1. The ordering of the two board dimensions is +// not axis-labelled; the product must justify its across/depth assignment. Keep +// the printed inch equivalents because they disagree with the metric figures. +type CollinsDatasheetMechanicals { + first_board_dimension:Micrometer,second_board_dimension:Micrometer, + first_board_dimension_hundredth_inches:Int,second_board_dimension_tenth_inches:Int, + chassis_width:Micrometer,standing:CitedFigureStanding +} +data collins_datasheet_mechanicals:CollinsDatasheetMechanicals = CollinsDatasheetMechanicals { + first_board_dimension:micrometer(count:512000),second_board_dimension:micrometer(count:424200), + first_board_dimension_hundredth_inches:2013,second_board_dimension_tenth_inches:167, + chassis_width:micrometer(count:448000),standing:CitedToAuthority {authority:extdeps_external_authority_anchor} +} +data extdeps_model_scope:ExternalModelScope = ExternalModelScope { + subject:ExternalSubjectRef {declaration:DeclarationRef {module_path:"extdeps.portwell.aptsa_627211_b4",decl_name:"collins_datasheet_mechanicals",field:WholeDeclaration}}, + first_citation:extdeps_external_authority_anchor,further_citations:[] +} diff --git a/dag/extdeps/power/fsp_crps.dag b/dag/extdeps/power/fsp_crps.dag new file mode 100644 index 00000000000..1219da742f1 --- /dev/null +++ b/dag/extdeps/power/fsp_crps.dag @@ -0,0 +1,36 @@ +module extdeps.power.fsp_crps +import std.types {List,NonEmptyStr} +import std.measure {Volt,volt,Milliampere,milliampere,Millimeter,millimeter} +import std.decl_ref {DeclarationRef,WholeDeclaration} +import extdeps.external_authority {ExternalAuthority,ExternalModelScope,ExternalSubjectRef} +import extdeps.uri {Uri,Https} + +data extdeps_external_authority_anchor:ExternalAuthority=ExternalAuthority {uri:Uri {scheme:Https,locator:"www.fsp-group.com/download/pro/FSP800-50FS_Datasheet.pdf"}} +data fsp1200_50fs_authority:ExternalAuthority=ExternalAuthority {uri:Uri {scheme:Https,locator:"www.fsp-group.com/download/pro/FSP1200-50FS_Datasheet.pdf"}} +data fsp_catalog_authority:ExternalAuthority=ExternalAuthority {uri:Uri {scheme:Https,locator:"www.fsp-group.com/download/catalog/IPCPSU.pdf"}} +data extdeps_model_scope:ExternalModelScope=ExternalModelScope {subject:ExternalSubjectRef {declaration:DeclarationRef {module_path:"extdeps.power.fsp_crps",decl_name:"fsp_server_power_references",field:WholeDeclaration}},first_citation:extdeps_external_authority_anchor,further_citations:[fsp1200_50fs_authority,fsp_catalog_authority]} + +type MainRailRating {input_min:Volt,input_max:Volt,current:Milliampere} +type ServerPowerReference { + model:NonEmptyStr,main_voltage:Volt,main_ratings:List, + reservation_length:Millimeter,reservation_width:Millimeter,reservation_height:Millimeter, + authority:ExternalAuthority, +} +// Complete redundant-assembly references, not a claim that any loose CRPS module +// mates with either assembly. May 2020 datasheets pp.1-2 and 2025-26 catalog. +// Reserve the larger catalog envelope (265 x 76 x 84 mm); older sheets give +// 250 x 76 x 83.8. Drawing/variant reconciliation is required before mount CAD. +// Standby and other rails are NOT added to the main-12V budget. +data fsp800_50fs_reference:ServerPowerReference=ServerPowerReference { + model:"FSP800-50FS",main_voltage:volt(count:12), + main_ratings:[MainRailRating {input_min:volt(count:100),input_max:volt(count:240),current:milliampere(count:65000)}], + reservation_length:millimeter(count:265),reservation_width:millimeter(count:76),reservation_height:millimeter(count:84),authority:extdeps_external_authority_anchor, +} +// Manufacturer lists 97 A at 180-264 Vac and 80.5 A at 100-127 Vac. +// The gap between these ranges is deliberately not interpolated. +data fsp1200_50fs_reference:ServerPowerReference=ServerPowerReference { + model:"FSP1200-50FS",main_voltage:volt(count:12), + main_ratings:[MainRailRating {input_min:volt(count:100),input_max:volt(count:127),current:milliampere(count:80500)},MainRailRating {input_min:volt(count:180),input_max:volt(count:264),current:milliampere(count:97000)}], + reservation_length:millimeter(count:265),reservation_width:millimeter(count:76),reservation_height:millimeter(count:84),authority:fsp1200_50fs_authority, +} +data fsp_server_power_references:List = [fsp800_50fs_reference,fsp1200_50fs_reference] diff --git a/dag/extdeps/printing/bambu_lab_filament.dag b/dag/extdeps/printing/bambu_lab_filament.dag new file mode 100644 index 00000000000..65dcd1df75a --- /dev/null +++ b/dag/extdeps/printing/bambu_lab_filament.dag @@ -0,0 +1,87 @@ +module extdeps.printing.bambu_lab_filament + +import std.types { List, NonEmptyStr, Bool } +import std.nat { Nat } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.measure { + Celsius, celsius, celsius_count, + Megapascal, megapascal, megapascal_count, + KilogramPerCubicMetre, kilogram_per_cubic_metre, +} +import extdeps.printing.fdm { FilamentTechnicalDataSheetRow, FilamentMaterial, Pla, Petg, filament_material_eq } +import extdeps.vendor.bambu_lab { bambu_lab } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, CitedToAuthority, TranscribedUncited } +import extdeps.uri { Uri, Https } + +// ONE VENDOR'S FILAMENT PRODUCTS, each with the figures its technical data sheet publishes. +// +// extdeps.printing.fdm deliberately carries no thermal or mechanical property on a material CLASS: +// a heat-deflection temperature is a fact about one formulation from one supplier, cited to that +// supplier's sheet. This module is where such sheets live for Bambu Lab's own filaments, which are +// what the two A1 minis in the fleet run. A row here is the thing the exhaust-thermal and +// stack-mechanics models consult; a class-level nominal would be the fabricated figure fdm refuses. +// +// Vendor PDFs read directly on 2026-10-04. Neither row supplies a printed-joint +// compressive allowable or creep qualification; bending strength cannot replace it. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "store.bblcdn.com/3a230e260a3a47c2b0db0156e07eef91.pdf" + } +} + +data bambu_filament_guide_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "cdn1.bambulab.com/filament/a5e185aed14e6/filament-guide-en-4.pdf" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.printing.bambu_lab_filament", + decl_name: "bambu_filament_sheets", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [bambu_filament_guide_authority] +} + +// The sheet's own fields, at the sheet's own grain. HDT is the ISO 75 figure at 0.45 MPa, which is +// the low-stress condition the sheets state; a load-bearing part at higher stress deflects at a +// LOWER temperature than this, so a consumer must not read it as a service ceiling under load. +// Shared property shape lives in the vendor-neutral FDM module. + +data bambu_pla_basic_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"store.bblcdn.com/s7/default/b189de92249a4b9ebed28b8ea1f080f0/Bambu_PLA_Basic_Technical_Data_Sheet.pdf"}} + +data bambu_pla_basic_tds: FilamentTechnicalDataSheetRow = FilamentTechnicalDataSheetRow { + product_label: "Bambu PLA Basic", + material: Pla, + density: kilogram_per_cubic_metre(count: 1240), + heat_deflection_at_0_45_mpa: celsius(count: 57), + tensile_strength: megapascal(count: 35), + bending_strength: megapascal(count: 76), + bending_modulus: megapascal(count: 2750), + standing: CitedToAuthority {authority:bambu_pla_basic_authority}, +} + +data bambu_petg_hf_tds: FilamentTechnicalDataSheetRow = FilamentTechnicalDataSheetRow { + product_label: "Bambu PETG HF", + material: Petg, + density: kilogram_per_cubic_metre(count: 1280), + heat_deflection_at_0_45_mpa: celsius(count: 69), + tensile_strength: megapascal(count: 34), + bending_strength: megapascal(count: 64), + bending_modulus: megapascal(count: 2050), + standing: CitedToAuthority {authority:extdeps_external_authority_anchor}, +} + +data bambu_filament_sheets: List = [bambu_pla_basic_tds, bambu_petg_hf_tds] + +// The sheet uncertainty the rows above drop: the tensile and bending figures carry +/- bands on the +// sheet. A consumer that wants a conservative limit subtracts the band it declares; the rows carry +// the central value the sheet prints, and this note says the band exists. +data bambu_tds_tolerance_band_note: NonEmptyStr = "Sheet values carry +/- bands (PLA Basic bending 76 +/- 5 MPa, modulus 2750 +/- 160 MPa; PETG HF bending 64 +/- 3 MPa, modulus 2050 +/- 120 MPa). Rows carry the central value; a consumer declares its own margin." diff --git a/dag/extdeps/printing/fdm.dag b/dag/extdeps/printing/fdm.dag index 57ae871733e..e7bd9b8f58b 100644 --- a/dag/extdeps/printing/fdm.dag +++ b/dag/extdeps/printing/fdm.dag @@ -4,7 +4,8 @@ import std.types { Bool, List, NonEmptyStr } import std.nat { Nat } import std.measure { Millimeter, millimeter_count, Micrometer, micrometer_count, Celsius, celsius_count } -import extdeps.external_authority { ExternalAuthority } +import extdeps.external_authority { ExternalAuthority, CitedFigureStanding } +import std.measure {Megapascal,KilogramPerCubicMetre} import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -328,3 +329,15 @@ fn material_suitability(rows: List, material: FilamentMa SuitabilityResolved { suitability: scan.value } } } + +// Manufacturer-specific typical test values; HDT is not a continuous service rating. +type FilamentTechnicalDataSheetRow { + product_label: NonEmptyStr + material: FilamentMaterial + density: KilogramPerCubicMetre + heat_deflection_at_0_45_mpa: Celsius + tensile_strength: Megapascal + bending_strength: Megapascal + bending_modulus: Megapascal + standing: CitedFigureStanding +} diff --git a/dag/extdeps/printing/sunlu_filament.dag b/dag/extdeps/printing/sunlu_filament.dag new file mode 100644 index 00000000000..f4924a7d7bb --- /dev/null +++ b/dag/extdeps/printing/sunlu_filament.dag @@ -0,0 +1,16 @@ +module extdeps.printing.sunlu_filament +import std.types {NonEmptyStr} +import std.decl_ref {DeclarationRef,WholeDeclaration} +import std.measure {celsius,celsius_delta,megapascal,kilogram_per_cubic_metre,CelsiusDelta} +import extdeps.printing.fdm {FilamentTechnicalDataSheetRow,Petg} +import extdeps.external_authority {ExternalAuthority,ExternalModelScope,ExternalSubjectRef,CitedToAuthority} +import extdeps.uri {Uri,Https} +data extdeps_external_authority_anchor:ExternalAuthority=ExternalAuthority {uri:Uri {scheme:Https,locator:"media.sunlu.com/prod/20260330/ccf53a55-5b58-49fa-b1d9-08f94c0a4b28.pdf?filename=TDS"}} +data extdeps_model_scope:ExternalModelScope=ExternalModelScope {subject:ExternalSubjectRef {declaration:DeclarationRef {module_path:"extdeps.printing.sunlu_filament",decl_name:"sunlu_high_speed_matte_petg",field:WholeDeclaration}},first_citation:extdeps_external_authority_anchor,further_citations:[]} +// SL-TE-WI-062 A/1, directly read 2026-10-04, pp.1 and 3. Typical XY specimen +// values at 160 mm/s, 255 C, 100% infill and 90-degree raster. Not a structural allowable. +// User's Amazon description is a likely match, not verified lot/SKU identity. +data sunlu_high_speed_matte_petg:FilamentTechnicalDataSheetRow=FilamentTechnicalDataSheetRow { + product_label:"SUNLU High Speed Matte PETG (candidate)",material:Petg,density:kilogram_per_cubic_metre(count:1280),heat_deflection_at_0_45_mpa:celsius(count:70),tensile_strength:megapascal(count:46),bending_strength:megapascal(count:75),bending_modulus:megapascal(count:2650),standing:CitedToAuthority {authority:extdeps_external_authority_anchor}} +data sunlu_hdt_tolerance:CelsiusDelta=celsius_delta(count:2) +data sunlu_specimen_caveat:NonEmptyStr="HDT 70 +/- 2 C at 0.45 MPa on XY test specimens; no long-duration creep or printed corner compressive allowable is supplied. Exact purchased SKU/lot must match." diff --git a/dag/extdeps/standards/atx12v_psdg.dag b/dag/extdeps/standards/atx12v_psdg.dag new file mode 100644 index 00000000000..0a492b0fe10 --- /dev/null +++ b/dag/extdeps/standards/atx12v_psdg.dag @@ -0,0 +1,51 @@ +module extdeps.standards.atx12v_psdg + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.measure { Millivolt, millivolt } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, TranscribedUncited } +import extdeps.uri { Uri, Https } + +// THE ATX12V POWER SUPPLY DESIGN GUIDE'S +12 V REGULATION BAND, which is the voltage a board built +// to that guide expects at its 12 V input. The board manual states no input tolerance of its own; +// its 8-pin inputs are ATX12V connectors and this guide is the authority such a board is designed +// against. A DC-IN feed that lands below the band's floor at the connector under load has failed +// the feed, which is what the voltage-drop derivation checks. +// +// +12 V regulation is +/-5 percent of nominal (11.40 V to 12.60 V) in the guide's DC output +// regulation table. Relayed through search summaries on 2026-10-04 and graded so. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "xDevs.com/doc/xDevs.com/Neutron/pdf/ATX12V_PSDG_2_2_public_br2.pdf" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.standards.atx12v_psdg", + decl_name: "atx12v_twelve_volt_regulation", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +type DcOutputRegulationBand { + nominal: Millivolt + minimum: Millivolt + maximum: Millivolt + standing: CitedFigureStanding +} + +data atx12v_twelve_volt_regulation: DcOutputRegulationBand = DcOutputRegulationBand { + nominal: millivolt(count: 12000), + minimum: millivolt(count: 11400), + maximum: millivolt(count: 12600), + standing: TranscribedUncited { + read_obligation: "Open ATX12V Power Supply Design Guide v2.2, DC Output Voltage Regulation table, and confirm +12 V at +/-5 percent (11.40 V min, 12.60 V max); relayed from search summaries that state positive rails are within 5 percent.", + }, +} diff --git a/dag/extdeps/standards/awg.dag b/dag/extdeps/standards/awg.dag new file mode 100644 index 00000000000..56fe090c3ba --- /dev/null +++ b/dag/extdeps/standards/awg.dag @@ -0,0 +1,60 @@ +module extdeps.standards.awg + +import std.types { List, NonEmptyStr } +import std.nat { Nat } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.measure { MicroohmPerMetre, microohm_per_metre } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, TranscribedUncited } +import extdeps.uri { Uri, Https } +import v2.std.optional { Present, Absent } + +// AMERICAN WIRE GAUGE: solid copper conductor resistance per metre at 20 C, the figure a 12 V +// branch's voltage-drop derivation multiplies by its length. +// +// The gauge is defined by ASTM B258 and the resistance figures every table prints derive from it +// with annealed copper's resistivity. The rows below are the rounded values relayed through search +// summaries of published tables (misumi, hyperphysics) on 2026-10-04, not a first-party read of +// the standard, and are graded so. Stranded conductors run a few percent higher; the derivation +// that consumes these must say which it assumed. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "hyperphysics.gsu.edu/hbase/Tables/wirega.html" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.standards.awg", + decl_name: "awg_copper_resistance_rows", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +type AwgCopperResistanceRow { + gauge: Nat + resistance_per_metre: MicroohmPerMetre +} + +data awg_copper_resistance_rows: List = [ + AwgCopperResistanceRow { gauge: 10, resistance_per_metre: microohm_per_metre(count: 3280) }, + AwgCopperResistanceRow { gauge: 12, resistance_per_metre: microohm_per_metre(count: 5210) }, + AwgCopperResistanceRow { gauge: 14, resistance_per_metre: microohm_per_metre(count: 8280) }, + AwgCopperResistanceRow { gauge: 16, resistance_per_metre: microohm_per_metre(count: 13200) }, + AwgCopperResistanceRow { gauge: 18, resistance_per_metre: microohm_per_metre(count: 20900) }, +] + +data awg_copper_resistance_standing: CitedFigureStanding = TranscribedUncited { + read_obligation: "Read ASTM B258 / NEC Chapter 9 Table 8 and replace the rounded relayed values (3.28, 5.21, 8.28, 13.2, 20.9 ohm/km for 10-18 AWG solid copper at 20 C) with the table's own.", +} + +// A gauge the table does not carry is a refusal, never a nearest neighbour. +fn awg_copper_resistance(gauge: Nat) -> MicroohmPerMetre? { + fold(awg_copper_resistance_rows, init: none, f: (acc, row) => + if row.gauge == gauge { Present { value: row.resistance_per_metre } } else { acc }) +} diff --git a/dag/extdeps/storage/samsung.dag b/dag/extdeps/storage/samsung.dag index 8bd3ed3ebc6..bd22b24d889 100644 --- a/dag/extdeps/storage/samsung.dag +++ b/dag/extdeps/storage/samsung.dag @@ -1,6 +1,6 @@ module extdeps.storage.samsung -import std.measure { ByteSize, byte_size, bandwidth } +import std.measure { ByteSize, byte_size, bandwidth, Gram, gram } import std.types { NonEmptyStr } import extdeps.storage.types { NvmeDeviceCatalogRow, PcieLink, PcieGeneration, NvmeFormFactor } import extdeps.vendor.samsung { samsung } @@ -47,3 +47,9 @@ data samsung_970_evo_500gb_catalog: NvmeDeviceCatalogRow = samsung_970_evo( part_number: "MZ-V7E500BW", capacity_bytes: byte_size(500107862016), ) + +// Primary Samsung product brochures, directly read 2026-10-04. Maximum, not nominal. +data samsung_970_evo_mass_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"download.semiconductor.samsung.com/resources/brochure/SAMSUNG_SSD_970_Brochure_Web.pdf"}} +data samsung_970_evo_plus_mass_authority: ExternalAuthority = ExternalAuthority {uri:Uri {scheme:Https,locator:"semiconductor.samsung.com/resources/brochure/SSD_970_PRO_EVO_Plus_Brochure_210712.pdf"}} +data samsung_970_evo_family_max_mass: Gram = gram(count:8) +data samsung_970_evo_plus_family_max_mass: Gram = gram(count:8) diff --git a/dag/extdeps/vendor/bambu_lab.dag b/dag/extdeps/vendor/bambu_lab.dag new file mode 100644 index 00000000000..a21c31bd30f --- /dev/null +++ b/dag/extdeps/vendor/bambu_lab.dag @@ -0,0 +1,32 @@ +module extdeps.vendor.bambu_lab + +import extdeps.vendor { Vendor } +import extdeps.hardware { Hardware } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "bambulab.com" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.vendor.bambu_lab", + decl_name: "bambu_lab", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +// The filament and printer vendor. "Bambu Lab" is the brand the data sheets print; the legal +// entity (Shenzhen Tuozhu Technology Co., Ltd. appears on regulatory copy) was not read first-party +// this session, so this row carries the trading name under the same declared mismatch +// extdeps.vendor.arctic records, awaiting the VendorLegalEntity | VendorTradingNameOnly split. +data bambu_lab: Vendor = Vendor { legal_name: "Bambu Lab" } diff --git a/dag/extdeps/vendor/molex.dag b/dag/extdeps/vendor/molex.dag new file mode 100644 index 00000000000..35729ab25e1 --- /dev/null +++ b/dag/extdeps/vendor/molex.dag @@ -0,0 +1,31 @@ +module extdeps.vendor.molex + +import extdeps.vendor { Vendor } +import extdeps.hardware { Hardware } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.molex.com" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.vendor.molex", + decl_name: "molex", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +// The Mini-Fit Jr. connector family owner. Legal entity name as printed on Molex product +// specifications; not read first-party this session (vendor site refused by egress), relayed +// from distributor datasheet copies. +data molex: Vendor = Vendor { legal_name: "Molex, LLC" } diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index 2c91f6f05fe..75f567bf1bd 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -213,6 +213,14 @@ fn scope_placement_refused_paths(added: List, carriers: List, ma } data scope_carrier_paths: List = [ + "dag/extdeps/vendor/bambu_lab.dag", + "dag/extdeps/vendor/molex.dag", + "dag/extdeps/printing/bambu_lab_filament.dag", + "dag/extdeps/printing/sunlu_filament.dag", + "dag/extdeps/connector/molex_mini_fit_jr.dag", + "dag/extdeps/standards/awg.dag", + "dag/extdeps/standards/atx12v_psdg.dag", + "dag/extdeps/physics/dry_air.dag", "dag/extdeps/apple/property_list.dag", "dag/extdeps/apple/info_plist.dag", "dag/extdeps/apple/build_settings.dag", @@ -265,6 +273,9 @@ data scope_carrier_paths: List = [ "dag/extdeps/storage/te_m2_ngff_connector.dag", "dag/extdeps/memory/te_ddr4_dimm_socket.dag", "dag/extdeps/land_pattern/types.dag", + "dag/extdeps/power/fsp_crps.dag", + "dag/extdeps/portwell/aptsa_627211_b4.dag", + "dag/extdeps/cooling/noctua_ampere.dag", "dag/extdeps/power/ti_tps3808.dag", "dag/extdeps/electronic_component/types.dag", "dag/extdeps/electronic_component/infineon_ipb017n10n5lf.dag", diff --git a/dag/gunbc/product/printed_chassis/assembly_geometry.dag b/dag/gunbc/product/printed_chassis/assembly_geometry.dag new file mode 100644 index 00000000000..1a24595e187 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/assembly_geometry.dag @@ -0,0 +1,49 @@ +module product.printed_chassis.assembly_geometry + +import std.types { Int, String, List, Bool } +import std.measure { Micrometer, micrometer } +import std.affine_space { Point3, point3 } +import std.coordinate_frame { coordinate_frame_identity } +import std.euclidean_geometry { Axis3 } +import product.printed_chassis.coupon { PlateDimensions, plate_dimensions } + +// Review geometry for the first assembled cassette. It is NOT AdmittedRealizationV0 and +// cannot enter the coupon's fabrication admission through a cast. The generic CAD adapter +// must validate positive primitives, solid connectivity, output extents and A1-mini fit. +// This explicit review rung replaces the former Three.js-only geometric authority. +// Coordinates use existing affine points; quantum is one micrometre, X across rear I/O, +// Y up, Z from rear toward the handle. Part-local frame 5100; assembly frame 5101. +type Primitive = Block { size: PlateDimensions, center: Point3 } + | RoundPrism { diameter: Micrometer, length: Micrometer, axis: Axis3, center: Point3 } +type PartKind = Printed | Purchased | Metal | Clearance +// Orientation of the part's assembly-local coordinates when laid on a print bed. +type PrintPlane = LayOnXZ | LayOnXY | LayOnYZ +// Scope prevents rack power equipment being cloned into each moving cassette. +type PartScope = Cassette | FixedFrame + +type AssemblyPart { + id: String, name: String, kind: PartKind, scope: PartScope, + adds: List, cuts: List, at: Point3, explode: Point3, + print_plane: PrintPlane, notes: String, +} +type InterfaceStatus = Proposed | OriginalPlateFitOnly | Unresolved +type FastenerKind = M3PanelClamp | M4FanThroughBolt | M4SocketThroughBolt | PrintedRetainingPin | BoardSupportPending | ProtectedDcBranch | BondingHardwarePending + +type MechanicalInterface { + id: String, members: List, fastener: FastenerKind, quantity: Int, + status: InterfaceStatus, requirement: String, +} +fn local(x: Int, y: Int, z: Int) -> Point3 { point3(frame: coordinate_frame_identity(occurrence: 5100), x: x, y: y, z: z) } +fn placed(x: Int, y: Int, z: Int) -> Point3 { point3(frame: coordinate_frame_identity(occurrence: 5101), x: x, y: y, z: z) } +fn block(w: Int, h: Int, d: Int, x: Int, y: Int, z: Int) -> Primitive { + Block { size: plate_dimensions(size_x: micrometer(count: w), size_y: micrometer(count: h), size_z: micrometer(count: d)), center: local(x: x,y: y,z: z) } +} +fn round(diameter: Int, length: Int, axis: Axis3, x: Int, y: Int, z: Int) -> Primitive { + RoundPrism { diameter: micrometer(count: diameter), length: micrometer(count: length), axis: axis, center: local(x: x,y: y,z: z) } +} +fn part(id: String, name: String, kind: PartKind, scope: PartScope, adds: List, cuts: List, x: Int,y: Int,z: Int, ex: Int,ey: Int,ez: Int, plane: PrintPlane, notes: String) -> AssemblyPart { + AssemblyPart { id: id,name: name,kind: kind,scope: scope,adds: adds,cuts: cuts,at: placed(x: x,y: y,z: z),explode: placed(x: ex,y: ey,z: ez),print_plane: plane,notes: notes } +} +fn mechanical_interface(id: String,members: List,fastener: FastenerKind,quantity: Int,status: InterfaceStatus,requirement: String) -> MechanicalInterface { + MechanicalInterface { id: id,members: members,fastener: fastener,quantity: quantity,status: status,requirement: requirement } +} diff --git a/dag/gunbc/product/printed_chassis/assembly_review_checks.dag b/dag/gunbc/product/printed_chassis/assembly_review_checks.dag new file mode 100644 index 00000000000..d3bd7bf3ef3 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/assembly_review_checks.dag @@ -0,0 +1,82 @@ +module product.printed_chassis.assembly_review_checks +import std.types {Bool,Int,List,Map,String} +import v2.std.algebra {any,length} +import v2.std.collection {empty_map,map_insert,map_lookup} +import std.measure {micrometer_count,millimeter_count} +import std.affine_space {Point3,signed_frame_component_count} +import std.euclidean_geometry {Axis3,Axis3X,Axis3Y,Axis3Z} +import product.printed_chassis.assembly_geometry {AssemblyPart,Primitive,Block,RoundPrism,Printed,Purchased,Metal,Clearance,PrintPlane,LayOnXY,LayOnXZ,LayOnYZ,MechanicalInterface} +import extdeps.printing.bambu_lab_a1_mini {a1_mini_build_envelope} +import extdeps.printing.fdm {build_envelope_x,build_envelope_y,build_envelope_z} + +// Conservative positive-geometry bounds, at half-micrometre precision. Cuts cannot enlarge them. +// These are source-model checks. Kernel validity/connectivity and load behavior remain separate. +type Bounds {lo_x:Int,lo_y:Int,lo_z:Int,hi_x:Int,hi_y:Int,hi_z:Int} +fn component(p:Point3,a:Axis3) -> Int {match a {Axis3X=>signed_frame_component_count(c:p.x) Axis3Y=>signed_frame_component_count(c:p.y) Axis3Z=>signed_frame_component_count(c:p.z)}} +fn dimension(p:Primitive,a:Axis3) -> Int { + match p {Block {size:s,center:_}=>match a {Axis3X=>micrometer_count(m:s.size_x) Axis3Y=>micrometer_count(m:s.size_y) Axis3Z=>micrometer_count(m:s.size_z)} + RoundPrism {diameter:d,length:l,axis:axis,center:_}=>if axis_same(a:a,b:axis) {micrometer_count(m:l)} else {micrometer_count(m:d)}} +} +fn axis_same(a:Axis3,b:Axis3) -> Bool {match a {Axis3X=>match b {Axis3X=>true Axis3Y=>false Axis3Z=>false} Axis3Y=>match b {Axis3Y=>true Axis3X=>false Axis3Z=>false} Axis3Z=>match b {Axis3Z=>true Axis3X=>false Axis3Y=>false}}} +fn center(p:Primitive) -> Point3 {match p {Block {size:_,center:c}=>c RoundPrism {diameter:_,length:_,axis:_,center:c}=>c}} +fn smaller(a:Int,b:Int) -> Int {if a Int {if a>b {a} else {b}} +fn primitive_bounds(p:Primitive) -> Bounds { + let c=center(p:p) + let x=2*component(p:c,a:Axis3X) + let y=2*component(p:c,a:Axis3Y) + let z=2*component(p:c,a:Axis3Z) + let w=dimension(p:p,a:Axis3X) + let h=dimension(p:p,a:Axis3Y) + let d=dimension(p:p,a:Axis3Z) + Bounds {lo_x:x-w,lo_y:y-h,lo_z:z-d,hi_x:x+w,hi_y:y+h,hi_z:z+d} +} +fn joined(a:Bounds,b:Bounds) -> Bounds {Bounds {lo_x:smaller(a:a.lo_x,b:b.lo_x),lo_y:smaller(a:a.lo_y,b:b.lo_y),lo_z:smaller(a:a.lo_z,b:b.lo_z),hi_x:larger(a:a.hi_x,b:b.hi_x),hi_y:larger(a:a.hi_y,b:b.hi_y),hi_z:larger(a:a.hi_z,b:b.hi_z)}} +fn primitive_positive(p:Primitive) -> Bool {all([Axis3X,Axis3Y,Axis3Z],a=>dimension(p:p,a:a)>0)} +fn within_bed(p:AssemblyPart) -> Bool { + match p.adds.first() {Absent=>false Present {value:first}=>{ + let b=fold(p.adds,init:primitive_bounds(p:first),f:fn(acc,q){joined(a:acc,b:primitive_bounds(p:q))}) + let x=b.hi_x-b.lo_x + let y=b.hi_y-b.lo_y + let z=b.hi_z-b.lo_z + let bx=2*1000*millimeter_count(m:build_envelope_x(e:a1_mini_build_envelope)) + let by=2*1000*millimeter_count(m:build_envelope_y(e:a1_mini_build_envelope)) + let bz=2*1000*millimeter_count(m:build_envelope_z(e:a1_mini_build_envelope)) + match p.print_plane {LayOnXY=>x<=bx&&y<=by&&z<=bz LayOnXZ=>x<=bx&&z<=by&&y<=bz LayOnYZ=>z<=bx&&y<=by&&x<=bz} + }} +} +// Build world-space bore lines once. A line ignores only the axial component, +// exactly as the coaxial relation does; diameter and different-owner checks remain. +// Persistent-map insertion copies its small accumulator; the bounded cassette has +// tens of lines, rather than a corpus-sized map. No global linear-cost claim is made. +type BoreLine {axis:Axis3,u:Int,v:Int} +type Bore {owner:String,line:BoreLine,diameter:Int} +fn bores(p:AssemblyPart,primitives:List) -> List { + flat_map(primitives,q=>match q {Block {size:_,center:_}=>[] RoundPrism {diameter:d,length:_,axis:a,center:c}=>{ + let diameter=micrometer_count(m:d) + if diameter>5000 {[]} else { + let x=signed_frame_component_count(c:c.x)+signed_frame_component_count(c:p.at.x) + let y=signed_frame_component_count(c:c.y)+signed_frame_component_count(c:p.at.y) + let z=signed_frame_component_count(c:c.z)+signed_frame_component_count(c:p.at.z) + let line=match a {Axis3X=>BoreLine {axis:Axis3X,u:y,v:z} Axis3Y=>BoreLine {axis:Axis3Y,u:x,v:z} Axis3Z=>BoreLine {axis:Axis3Z,u:x,v:y}} + [Bore {owner:p.id,line:line,diameter:diameter}] + }}}) +} +fn bore_index(parts:List) -> Map> { + fold(flat_map(parts,p=>bores(p:p,primitives:concat(p.adds,p.cuts))),init:empty_map(),f:fn(index,b){ + let previous=match map_lookup(m:index,key:b.line) {Absent=>[] Present {value:rows}=>rows} + map_insert(m:index,key:b.line,value:concat([b],previous)) + }) +} +fn bore_has_mate(b:Bore,index:Map>) -> Bool { + match map_lookup(m:index,key:b.line) {Absent=>false Present {value:rows}=>any(rows,q=>q.owner!=b.owner&&q.diameter<=b.diameter&&b.diameter-q.diameter<=600)} +} +type PartIds {seen:Map,unique:Bool} +fn review_model_checks(parts:List,interfaces:List) -> Bool { + let ids=fold(parts,init:PartIds {seen:empty_map(),unique:true},f:fn(acc,p){ + let fresh=match map_lookup(m:acc.seen,key:p.id) {Absent=>true Present {value:_}=>false} + PartIds {seen:map_insert(m:acc.seen,key:p.id,value:true),unique:acc.unique&&fresh} + }) + let index=bore_index(parts:parts) + length(parts)>0&&ids.unique&&all(interfaces,i=>all(i.members,id=>match map_lookup(m:ids.seen,key:id) {Absent=>false Present {value:_}=>true}))&&all(parts,p=>length(p.adds)>0&&all(concat(p.adds,p.cuts),q=>primitive_positive(p:q))&&match p.kind {Printed=>within_bed(p:p)&&all(bores(p:p,primitives:p.cuts),b=>bore_has_mate(b:b,index:index)) Purchased=>true Metal=>true Clearance=>true}) +} diff --git a/dag/gunbc/product/printed_chassis/bed_partition.dag b/dag/gunbc/product/printed_chassis/bed_partition.dag new file mode 100644 index 00000000000..5a5a1547b70 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/bed_partition.dag @@ -0,0 +1,63 @@ +module product.printed_chassis.bed_partition +import std.types {Int,List,Bool} +import std.nat {nat_range_inclusive} + +// Axis-aligned panel allocation under declared feature envelopes. Protrusions +// belong to a particular side, not a blanket deduction repeated on every cell. +// Brim and bed-edge clearance are independent, per-side process allowances. +type AxisPrintAllowance { + bed_um:Int,brim_each_side_um:Int,edge_clearance_each_side_um:Int, + outer_low_um:Int,outer_high_um:Int,internal_low_um:Int,internal_high_um:Int +} +type AxisCell { + start_um:Int,end_um:Int,low_overhang_um:Int,high_overhang_um:Int, + occupied_with_process_um:Int +} +type AxisAllocation = AxisAllocated {cells:List} | AxisAllocationInvalid | AxisAllocationNoFit +fn lesser(a:Int,b:Int) -> Int {if a Int {if n>0 {n} else {0}} +fn process_allowance(a:AxisPrintAllowance) -> Int {2*(a.brim_each_side_um+a.edge_clearance_each_side_um)} +fn covered(n:Int,first:Int,inner:Int,last:Int,limit:Int) -> Int { + if n==1 {lesser(a:first,b:limit)} else {lesser(a:first,b:limit)+lesser(a:last,b:limit)+(n-2)*lesser(a:inner,b:limit)} +} +// Find the smallest balanced cell width that can cover the span, saturating +// narrower edge cells first. The interval strictly shrinks: logarithmic search. +fn balanced_limit(span:Int,n:Int,first:Int,inner:Int,last:Int,low:Int,high:Int) -> Int { + if low>=high {low} else { + let mid=low+(high-low)/2 + if covered(n:n,first:first,inner:inner,last:last,limit:mid)>=span { + balanced_limit(span:span,n:n,first:first,inner:inner,last:last,low:low,high:mid) + } else {balanced_limit(span:span,n:n,first:first,inner:inner,last:last,low:mid+1,high:high)} + } +} +fn allocate_cells(span:Int,n:Int,first:Int,inner:Int,last:Int,a:AxisPrintAllowance) -> AxisAllocation { + let limit=balanced_limit(span:span,n:n,first:first,inner:inner,last:last,low:1,high:a.bed_um) + let excess=covered(n:n,first:first,inner:inner,last:last,limit:limit)-span + let cells=map(nat_range_inclusive(lo:0,hi:n-1),i=>{ + let capacity=if i==0 {first} else {if i==n-1 {last} else {inner}} + let eligible_before=(if i>0 && first>=limit {1} else {0})+(if inner>=limit {positive(n:i-1)} else {0}) + let before=if i==0 {0} else {lesser(a:first,b:limit)+(i-1)*lesser(a:inner,b:limit)-lesser(a:excess,b:eligible_before)} + let width=lesser(a:capacity,b:limit)-(if capacity>=limit && eligible_beforec.end_um<=c.start_um || c.occupied_with_process_um>a.bed_um) {AxisAllocationNoFit} else {AxisAllocated {cells:cells}} +} +// For n>=2, capacity is first + last + (n-2)*inner. This derives the minimum +// count without enumerating arbitrary cut grids. Balanced cuts then preserve +// exact coverage, including integer-micrometre remainder, with no zero-width cell. +fn allocate_axis(span_um:Int,allowance:AxisPrintAllowance) -> AxisAllocation { + let a=allowance + if span_um<=0 || a.bed_um<=0 || any([a.brim_each_side_um,a.edge_clearance_each_side_um,a.outer_low_um,a.outer_high_um,a.internal_low_um,a.internal_high_um],v=>v<0) {return AxisAllocationInvalid} + let usable=a.bed_um-process_allowance(a:a) + let single=usable-a.outer_low_um-a.outer_high_um + if span_um<=single {return allocate_cells(span:span_um,n:1,first:single,inner:single,last:single,a:a)} + let first=usable-a.outer_low_um-a.internal_high_um + let last=usable-a.internal_low_um-a.outer_high_um + let inner=usable-a.internal_low_um-a.internal_high_um + let extra=positive(n:span_um-first-last) + if first<=0 || last<=0 || (extra>0 && inner<=0) {return AxisAllocationNoFit} + let n=if extra==0 {2} else {2+(extra+inner-1)/inner} + allocate_cells(span:span_um,n:n,first:first,inner:inner,last:last,a:a) +} diff --git a/dag/gunbc/product/printed_chassis/board_mechanical_reference.dag b/dag/gunbc/product/printed_chassis/board_mechanical_reference.dag new file mode 100644 index 00000000000..3f32bf97e59 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/board_mechanical_reference.dag @@ -0,0 +1,105 @@ +module product.printed_chassis.board_mechanical_reference +import std.types {Int,List,String,NonEmptyStr} +import std.measure {Micrometer,micrometer_count} +import extdeps.external_authority {ExternalAuthority,CitedFigureStanding,CitedToAuthority,TranscribedUncited} +import extdeps.uri {uri_wire} +import extdeps.ocp.mt_jade.platform {mt_jade_motherboard_mechanical} +import extdeps.ocp.mt_jade.subject {mt_jade_subject_authority_anchor} +import extdeps.boards.asrock_rack {BoardLocationStanding,HolePositionEstimatedFromImage,BoardStandardLocationCorrespondence,standoff_placement,StandoffAdmitted,StandoffProhibited} +import extdeps.standards.micro_atx {micro_atx_mounting_locations,MountingLocationId,LocB,LocC,LocF,LocH,LocJ,LocL,LocM,LocR,LocS} +import gunbc.specification_citation_read_provenance {SpecificationCitationReadReceipt,mt_jade_mirror_citation_read_receipt} +import extdeps.languages.json.emit {JsonValue,json_object,json_kv,json_array,json_string,json_int,json_null,json_bool} + +// A repository observation, not an OCP-authored coordinate map. Dimensions, +// citation and read receipt are joined from their existing authorities. +type CandidateMountPixel {id:String,x:Int,y:Int} +type RasterDatum = FrontLeftBoundingBoxXRightDepthRear {} +type ImageMountCoverage = VisibleCandidatesIncomplete {} +type ImageMountAccuracy = AccuracyUnestablished {} +type RasterCalibration { + left_px:Int,right_px:Int,front_px:Int,rear_px:Int, + page:Int,figure:Int,pixels_per_point:Int,datum:RasterDatum, + width:Micrometer,depth:Micrometer +} +type ImageMountReading { + calibration:RasterCalibration,candidates:List, + source:ExternalAuthority,source_read:SpecificationCitationReadReceipt, + standing:BoardLocationStanding,coverage:ImageMountCoverage,accuracy:ImageMountAccuracy +} +type BoardMountingReference + = StandardLocationReference {locations:List} + | ImageLocationReference {reading:ImageMountReading} + | MountingReferencePending {read_obligation:NonEmptyStr} + +data jade_candidate_mount_pixels:List = [ + CandidateMountPixel {id:"J01",x:496,y:604},CandidateMountPixel {id:"J02",x:702,y:604}, + CandidateMountPixel {id:"J03",x:405,y:829},CandidateMountPixel {id:"J04",x:821,y:831}, + CandidateMountPixel {id:"J05",x:490,y:859},CandidateMountPixel {id:"J06",x:663,y:859}, + CandidateMountPixel {id:"J07",x:749,y:869},CandidateMountPixel {id:"J08",x:540,y:882}, + CandidateMountPixel {id:"J09",x:499,y:904},CandidateMountPixel {id:"J10",x:478,y:1050}, + CandidateMountPixel {id:"J11",x:498,y:1047},CandidateMountPixel {id:"J12",x:565,y:1062}, + CandidateMountPixel {id:"J13",x:739,y:1047}, +] + +data jade_raster_calibration:RasterCalibration = RasterCalibration { + left_px:394,right_px:830,front_px:594,rear_px:1083, + page:9,figure:1,pixels_per_point:2,datum:FrontLeftBoundingBoxXRightDepthRear {}, + width:mt_jade_motherboard_mechanical.outline_width,depth:mt_jade_motherboard_mechanical.outline_depth +} +data jade_image_mount_reading:ImageMountReading = ImageMountReading { + calibration:jade_raster_calibration,candidates:jade_candidate_mount_pixels, + source:mt_jade_subject_authority_anchor,source_read:mt_jade_mirror_citation_read_receipt, + standing:HolePositionEstimatedFromImage,coverage:VisibleCandidatesIncomplete {},accuracy:AccuracyUnestablished {} +} +// Scaling is a display estimate only. Calibration is fixed by the observation; +// passing this coordinate never discharges standoff_placement's standing check. +fn image_from_left_um(c:RasterCalibration,p:CandidateMountPixel) -> Int {(p.x-c.left_px)*micrometer_count(m:c.width)/(c.right_px-c.left_px)} +fn image_from_front_um(c:RasterCalibration,p:CandidateMountPixel) -> Int {(p.y-c.front_px)*micrometer_count(m:c.depth)/(c.rear_px-c.front_px)} +fn jade_from_left_um(p:CandidateMountPixel) -> Int {image_from_left_um(c:jade_raster_calibration,p:p)} +fn jade_from_front_um(p:CandidateMountPixel) -> Int {image_from_front_um(c:jade_raster_calibration,p:p)} +fn cited_figure_json(standing:CitedFigureStanding) -> JsonValue { + match standing { + CitedToAuthority {authority:a}=>json_object(members:[json_kv(key:"source",value:json_string(s:uri_wire(uri:a.uri) as String)),json_kv(key:"standing",value:json_string(s:"cited"))]) + TranscribedUncited {read_obligation:o}=>json_object(members:[json_kv(key:"standing",value:json_string(s:"transcribed; read required")),json_kv(key:"read_obligation",value:json_string(s:o as String))]) + } +} +fn placement_admitted(standing:BoardLocationStanding) -> Bool { + match standoff_placement(standing:standing) {StandoffAdmitted=>true StandoffProhibited {cause:_}=>false} +} +fn standard_location_label(id:MountingLocationId) -> String { + match id {LocB=>"B" LocC=>"C" LocF=>"F" LocH=>"H" LocJ=>"J" LocL=>"L" LocM=>"M" LocR=>"R" LocS=>"S"} +} +fn mounting_reference_json(reference:BoardMountingReference) -> JsonValue { + match reference { + MountingReferencePending {read_obligation:o}=>json_object(members:[json_kv(key:"kind",value:json_string(s:"pending")),json_kv(key:"read_obligation",value:json_string(s:o as String))]) + StandardLocationReference {locations:rows}=>json_object(members:[ + json_kv(key:"kind",value:json_string(s:"standard-location-correspondence")), + json_kv(key:"locations",value:json_array(elements:map(rows,r=>json_object(members:[ + json_kv(key:"location",value:json_string(s:standard_location_label(id:r.location))), + json_kv(key:"standoff_admitted",value:json_bool(b:placement_admitted(standing:r.standing))), + json_kv(key:"standard_coordinates",value:json_array(elements:map(filter(micro_atx_mounting_locations,m=>m.id==r.location),m=>json_object(members:[json_kv(key:"along_rear_edge_um",value:json_int(n:micrometer_count(m:m.along_rear_edge))),json_kv(key:"depth_from_rear_edge_um",value:json_int(n:micrometer_count(m:m.depth_from_rear_edge)))])))) + ]))))]) + ImageLocationReference {reading:r}=>json_object(members:[ + json_kv(key:"kind",value:json_string(s:"image-derived-candidates")), + json_kv(key:"source",value:json_string(s:uri_wire(uri:r.source.uri) as String)), + json_kv(key:"source_read_subject",value:json_string(s:concat(r.source_read.subject.module_path,concat(".",r.source_read.subject.decl_name)))), + json_kv(key:"page",value:json_int(n:r.calibration.page)),json_kv(key:"figure",value:json_int(n:r.calibration.figure)), + json_kv(key:"pixels_per_point",value:json_int(n:r.calibration.pixels_per_point)), + json_kv(key:"raster_bounds",value:json_array(elements:map([r.calibration.left_px,r.calibration.right_px,r.calibration.front_px,r.calibration.rear_px],n=>json_int(n:n)))), + json_kv(key:"datum",value:json_string(s:match r.calibration.datum {FrontLeftBoundingBoxXRightDepthRear {}=>"Front-left bounding-box corner; X right, depth rearward. Bounding box is not routed PCB outline."})), + json_kv(key:"coverage",value:json_string(s:match r.coverage {VisibleCandidatesIncomplete {}=>"visible candidates only; incomplete"})), + json_kv(key:"coordinate_table_markdown",value:json_string(s:image_candidate_table_markdown(reading:r))), + json_kv(key:"accuracy_bound_um",value:match r.accuracy {AccuracyUnestablished {}=>json_null()}), + json_kv(key:"candidates",value:json_array(elements:map(r.candidates,p=>json_object(members:[ + json_kv(key:"id",value:json_string(s:p.id)),json_kv(key:"from_left_um",value:json_int(n:image_from_left_um(c:r.calibration,p:p))), + json_kv(key:"from_front_um",value:json_int(n:image_from_front_um(c:r.calibration,p:p))), + json_kv(key:"diameter_um",value:json_null()),json_kv(key:"standoff_admitted",value:json_bool(b:placement_admitted(standing:r.standing))) + ])))) + ]) + } +} +// The documentation table is a projection of this function, not a second set of +// coordinates. The review emitter publishes the same values as JSON above. +fn image_candidate_table_markdown(reading:ImageMountReading) -> String { + concat("| Candidate | From left, um | From front, um |\n| --- | ---: | ---: |\n",join(map(reading.candidates,p=>join(["| ",p.id," | ",to_string(image_from_left_um(c:reading.calibration,p:p))," | ",to_string(image_from_front_um(c:reading.calibration,p:p))," |\n"],"")),"")) +} diff --git a/dag/gunbc/product/printed_chassis/cadquery_realization.dag b/dag/gunbc/product/printed_chassis/cadquery_realization.dag index 3764ec5ef22..0c6db6f2de2 100644 --- a/dag/gunbc/product/printed_chassis/cadquery_realization.dag +++ b/dag/gunbc/product/printed_chassis/cadquery_realization.dag @@ -1,4 +1,7 @@ module product.printed_chassis.cadquery_realization +import product.printed_chassis.assembly_geometry {Primitive,Block,RoundPrism,AssemblyPart} +import product.printed_chassis.assembly_review_checks {primitive_positive,component} +import std.euclidean_geometry {Axis3X,Axis3Y,Axis3Z} import std.types { Bool, Int, List, NonEmptyStr, String } import std.nat { Nat } import std.measure { Micrometer, micrometer, micrometer_count } @@ -541,3 +544,45 @@ fn plate_part_from_standoff_gauge(g: StandoffGaugeGeometry) -> AdmittedPlatePart holes: standoff_gauge_holes(g: g), } } + +// Review CSG shares this module's exact dimension renderer, but deliberately produces a +// distinct program carrier. This is no AdmittedRealizationV0 or fabrication capability. +type ReviewCsgAction = BeginReviewSolid | UnionReviewSolid | CutReviewSolid +type ReviewCadQueryProgram sole_constructor {source:NonEmptyStr} +type ReviewCadQueryEmission = ReviewCadQueryEmitted {program:ReviewCadQueryProgram} | ReviewCadQueryRefused {reason:String} +fn review_program_source(p:ReviewCadQueryProgram) -> String {p.source as String} +fn review_dimension(prefix:String,n:Int) -> SourceSubstitution {SourceSubstitution {prefix:prefix,rendering:micrometre_millimetre_wire(m:micrometer(count:n))}} +fn review_primitive_line(p:Primitive,action:ReviewCsgAction) -> LineEmission { + let start=match action {BeginReviewSolid=>"result = " UnionReviewSolid=>"result = result.fuse(" CutReviewSolid=>"result = result.cut("} + let end=match action {BeginReviewSolid=>"\n" UnionReviewSolid=>")\n" CutReviewSolid=>")\n"} + match p { + Block {size:s,center:c}=>emit_line(substitutions:[ + SourceSubstitution {prefix:concat(start,"cq.Workplane('XY').box("),rendering:micrometre_millimetre_wire(m:s.size_x)}, + SourceSubstitution {prefix:",",rendering:micrometre_millimetre_wire(m:s.size_y)}, + SourceSubstitution {prefix:",",rendering:micrometre_millimetre_wire(m:s.size_z)}, + review_dimension(prefix:", centered=(True,True,True)).val().translate((",n:component(p:c,a:Axis3X)), + review_dimension(prefix:",",n:component(p:c,a:Axis3Y)),review_dimension(prefix:",",n:component(p:c,a:Axis3Z))],tail:concat("))",end)) + RoundPrism {diameter:d,length:l,axis:a,center:c}=>{ + let axis=match a {Axis3X=>"(1,0,0)" Axis3Y=>"(0,1,0)" Axis3Z=>"(0,0,1)"} + emit_line(substitutions:[ + SourceSubstitution {prefix:concat(start,"cq.Solid.makeCylinder("),rendering:micrometre_half_millimetre_wire(m:d)}, + SourceSubstitution {prefix:",",rendering:micrometre_millimetre_wire(m:l)}, + SourceSubstitution {prefix:",cq.Vector(0,0,0),cq.Vector",rendering:MillimetreRendered {wire:axis as NonEmptyStr}}, + review_dimension(prefix:").translate((",n:component(p:c,a:Axis3X)), + SourceSubstitution {prefix:"-",rendering:micrometre_half_millimetre_wire(m:micrometer(count:match a {Axis3X=>micrometer_count(m:l) Axis3Y=>0 Axis3Z=>0}))}, + review_dimension(prefix:",",n:component(p:c,a:Axis3Y)), + SourceSubstitution {prefix:"-",rendering:micrometre_half_millimetre_wire(m:micrometer(count:match a {Axis3Y=>micrometer_count(m:l) Axis3X=>0 Axis3Z=>0}))}, + review_dimension(prefix:",",n:component(p:c,a:Axis3Z)), + SourceSubstitution {prefix:"-",rendering:micrometre_half_millimetre_wire(m:micrometer(count:match a {Axis3Z=>micrometer_count(m:l) Axis3X=>0 Axis3Y=>0}))} + ],tail:concat("))",end)) + } + } +} +fn review_part_lines(p:AssemblyPart) -> List { + concat(map(enumerate(p.adds),r=>review_primitive_line(p:r.second,action:if r.first==0 {BeginReviewSolid} else {UnionReviewSolid})),concat(map(p.cuts,c=>review_primitive_line(p:c,action:CutReviewSolid)),[LineEmitted {text:"_review_shapes.append(result.clean())\n"}])) +} +fn realize_review_parts(parts:List) -> ReviewCadQueryEmission { + if length(parts)==0||!all(parts,p=>length(p.adds)>0&&all(concat(p.adds,p.cuts),q=>primitive_positive(p:q))) {return ReviewCadQueryRefused {reason:"Empty or non-positive review geometry"}} + let scan=fold(flat_map(parts,p=>review_part_lines(p:p)),init:LineScan {lines:[],refusal:NoDimensionRefused},f:scan_line) + match scan.refusal {DimensionRefused {micrometres:_}=>ReviewCadQueryRefused {reason:"Dimension unrenderable"} NoDimensionRefused=>ReviewCadQueryEmitted {program:ReviewCadQueryProgram {source:join([cadquery_preamble,"_review_shapes = []\n",join(scan.lines,"")],"") as NonEmptyStr}}} +} diff --git a/dag/gunbc/product/printed_chassis/cassette.dag b/dag/gunbc/product/printed_chassis/cassette.dag new file mode 100644 index 00000000000..b145fab8bdb --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cassette.dag @@ -0,0 +1,138 @@ +module product.printed_chassis.cassette + +import std.types { Bool, Int, String, List } +import std.measure { micrometer_count } +import std.euclidean_geometry { Axis3X, Axis3Y, Axis3Z } +import product.printed_chassis.assembly_geometry { + Primitive, AssemblyPart, MechanicalInterface, Printed, Purchased, Metal, Clearance, + Cassette, FixedFrame, LayOnXZ, LayOnXY, LayOnYZ, block, round, part, mechanical_interface, M3PanelClamp,PrintedRetainingPin,M4FanThroughBolt,M4SocketThroughBolt,BoardSupportPending,ProtectedDcBranch,BondingHardwarePending,Proposed,OriginalPlateFitOnly,Unresolved, +} +import product.printed_chassis.tray_panel_features {panel_seam_um,lap_center_offset_um,lap_along_joint_um,rail_foot_width_um,rail_foot_inset_um,rail_wall_width_um,rail_wall_offset_um,handle_center_from_edge_um,handle_depth_um,rear_socket_width_um,rear_socket_inset_um,rear_socket_wall_width_um,handle_support_center_from_edge_um,handle_support_depth_um} +import product.printed_chassis.cassette_layout {CassetteLayout,current_layout} +import product.printed_chassis.fan_mount { + arctic_p8_fan_mount, FanMountAdmitted, FanMountBoreReachesMountingHoles, FanMountBoreNotEnclosedByPlate, FanMountPlateSmallerThanPattern, fan_mount_plate, fan_mount_bore, fan_mount_mounting_holes, +} +import product.printed_chassis.coupon { OpThroughHole } +import extdeps.fans.arctic { arctic_p8_mounting_pattern } + +// This root owns the entire review assembly formerly authored in the browser. +// Nominal board lengths come from the board authority; design allowances below are proposals. +// No measured board thickness, standoff thread, PSU rating or thermal result is invented. +data printed_configuration_layout:CassetteLayout=current_layout() +data board_w: Int = printed_configuration_layout.board_width +data board_d: Int = printed_configuration_layout.board_depth +data margin: Int = printed_configuration_layout.edge_margin +data tray_w: Int = printed_configuration_layout.tray_width +data tray_d: Int = printed_configuration_layout.tray_depth +data panel_t: Int = 8000 +data seam: Int = panel_seam_um +data panel_w: Int = (tray_w-seam)/2 +data panel_d: Int = (tray_d-seam)/2 +data bay_pitch: Int = 140000 +data bay_w: Int = tray_w+48000 +data rear_plate_z: Int = 0-tray_d/2-76000 +data rear_beam_z: Int = rear_plate_z+20000 +data fan_spacing: Int = 94000 +data fan_hole_offset: Int = micrometer_count(m:arctic_p8_mounting_pattern.hole_pitch)/2 +data fan_hole_d: Int = micrometer_count(m:arctic_p8_mounting_pattern.hole_clearance_diameter) +data m3_clearance: Int = 3400 +data joint_clearance: Int = 250 + +type LapJoint {id:String,donor:String,receiver:String,x:Int,z:Int,along_x:Bool,span:Int,clamp_x:Int,clamp_z:Int} +fn lap_joints() -> List { + let cx=(tray_w+seam)/4 + let cz=(tray_d+seam)/4 + let dx=panel_w/2-36000 + let dz=panel_d/2-36000 + [LapJoint {id:"rear-row",donor:"P1",receiver:"P2",x:lap_center_offset_um-seam/2,z:0-cz,along_x:true,span:dz,clamp_x:10000,clamp_z:0-cz+dz}, + LapJoint {id:"front-row",donor:"P3",receiver:"P4",x:lap_center_offset_um-seam/2,z:cz,along_x:true,span:dz,clamp_x:10000,clamp_z:cz+dz}, + LapJoint {id:"left-column",donor:"P1",receiver:"P3",x:0-cx,z:lap_center_offset_um-seam/2,along_x:false,span:dx,clamp_x:0-cx-dx,clamp_z:10000}, + LapJoint {id:"right-column",donor:"P2",receiver:"P4",x:cx,z:lap_center_offset_um-seam/2,along_x:false,span:dx,clamp_x:cx+dx,clamp_z:10000}] +} +fn lap_features(j:LapJoint,receiver:Bool,cx:Int,cz:Int) -> List { + let clearance=if receiver {joint_clearance} else {0} + flat_map([-1,1],fn(sign){[block(w:(if j.along_x {lap_along_joint_um} else {28000})+2*clearance,h:4000+2*clearance,d:(if j.along_x {28000} else {lap_along_joint_um})+2*clearance,x:j.x-cx+(if j.along_x {0} else {sign*j.span}),y:2000,z:j.z-cz+(if j.along_x {sign*j.span} else {0}))]}) +} +fn hole_y(d:Int,x:Int,z:Int) -> Primitive {round(diameter:d,length:50000,axis:Axis3Y,x:x,y:0,z:z)} +fn panel(sx:Int,sz:Int,id:String) -> AssemblyPart { + let cx=sx*(tray_w+seam)/4 + let cz=sz*(tray_d+seam)/4 + let own=filter(lap_joints(),j=>j.donor==id) + let receiving=filter(lap_joints(),j=>j.receiver==id) + let laps=flat_map(own,j=>lap_features(j:j,receiver:false,cx:cx,cz:cz)) + let pockets=flat_map(receiving,j=>lap_features(j:j,receiver:true,cx:cx,cz:cz)) + let clamps=map(filter(lap_joints(),j=>j.donor==id||j.receiver==id),j=>hole_y(d:m3_clearance,x:j.clamp_x-cx,z:j.clamp_z-cz)) + let slot_w=(panel_w-56000)/4 + let vents=flat_map([0,1,2,3],fn(i){ + let x=0-panel_w/2+28000+slot_w/2+i*slot_w + let width=slot_w-5000 + let half=(panel_d-65000-width)/2 + [block(w:width,h:12000,d:2*half,x:x,y:4000,z:0),round(diameter:width,length:12000,axis:Axis3Y,x:x,y:4000,z:half),round(diameter:width,length:12000,axis:Axis3Y,x:x,y:4000,z:0-half)] + }) + let rails=[block(w:rail_foot_width_um,h:4000,d:panel_d,x:sx*(panel_w/2-rail_foot_inset_um),y: -2000,z:0),block(w:rail_wall_width_um,h:12000,d:panel_d,x:sx*(panel_w/2+rail_wall_offset_um),y:4000,z:0)] + let handle=if sz==1 {[block(w:54800,h:8000,d:handle_depth_um,x:sx*27600-cx,y:4000,z:tray_d/2+handle_center_from_edge_um-cz),block(w:10000,h:8000,d:handle_support_depth_um,x:sx*50000-cx,y:4000,z:tray_d/2+handle_support_center_from_edge_um-cz)]} else {[]} + let dock_x=sx*(tray_w/2-rear_socket_inset_um)-cx + let pin=if sz== -1 {[hole_y(d:4400,x:dock_x,z:0-tray_d/2+22000-cz)]} else {[]} + part(id:id,name:concat("Keyed tray panel ",id),kind:Printed,scope:Cassette,adds:concat([block(w:panel_w,h:panel_t,d:panel_d,x:0,y:4000,z:0)],concat(laps,concat(rails,handle))),cuts:concat(pockets,concat(vents,concat(clamps,pin))),x:cx,y:0,z:cz,ex:sx*65000,ey:12000,ez:sz*55000,plane:LayOnXZ,notes:"Two keyed laps per seam register and carry shear; one M3 clamp per seam, four total. Rail and front handle features are integral. Separate S1/S2 sockets accept the slide-in support arms; one M4 through-bolt retains each socket/arm/tray stack. Joint clearance is a proposal; first assembly is unpowered.") +} +// The socket is printed floor-down with its channel open, never under the tray. +// Its floor and side walls preserve the existing arm envelope and hole datum. +fn rear_socket(sx:Int,id:String) -> AssemblyPart { + part(id:id,name:"Separate open rear-arm socket",kind:Printed,scope:Cassette, + adds:[block(w:rear_socket_width_um,h:3000,d:42000,x:0,y:1500,z:0),block(w:rear_socket_wall_width_um,h:8000,d:42000,x:0-(rear_socket_width_um-rear_socket_wall_width_um)/2,y:7000,z:0),block(w:rear_socket_wall_width_um,h:8000,d:42000,x:(rear_socket_width_um-rear_socket_wall_width_um)/2,y:7000,z:0)], + cuts:[hole_y(d:4400,x:0,z: -2000)], + x:sx*(tray_w/2-rear_socket_inset_um),y: -15000,z:0-tray_d/2+24000,ex:sx*50000,ey: -45000,ez:0,plane:LayOnXZ, + notes:"Print floor-down, open channel upward, supports disabled. One M4 through-bolt with washers and nut replaces K1/K2 and clamps socket, arm and tray. Proposed M4 x 35 length; check engagement and do not crush plastic. Bearing/creep unqualified. Old torn panels are not assumed repairable.") +} +fn extension(sx:Int,id:String) -> AssemblyPart { + part(id:id,name:"Slide-in rear support arm",kind:Printed,scope:Cassette,adds:[block(w:20000,h:8000,d:112000,x:0,y:0,z:0)],cuts:[hole_y(d:4400,x:0,z:32000),hole_y(d:4400,x:0,z: -46000)],x:sx*(tray_w/2-7000),y: -8000,z:0-tray_d/2-10000,ex:sx*50000,ey: -20000,ez: -60000,plane:LayOnXZ,notes:"Slides into a separately printed tray socket. One M4 through-bolt retains the tray interface; a removable printed pin retains the fan-bank interface. Pin retention and loaded cantilever behavior remain unqualified.") +} +fn fan_plate_adds() -> List { + match arctic_p8_fan_mount {FanMountAdmitted {geometry:g}=>{ + let p=fan_mount_plate(g:g) + [block(w:micrometer_count(m:p.size_x),h:micrometer_count(m:p.size_y),d:micrometer_count(m:p.size_z),x:0,y:55000,z:0)] + } FanMountBoreReachesMountingHoles {bore_radius:_,hole_center_distance:_}=>[] FanMountBoreNotEnclosedByPlate {bore_diameter:_,plate_edge:_}=>[] FanMountPlateSmallerThanPattern {plate_edge:_,required_edge:_}=>[]} +} +fn fan_plate_cuts() -> List { + match arctic_p8_fan_mount {FanMountAdmitted {geometry:g}=>{ + let p=fan_mount_plate(g:g) + map(concat([fan_mount_bore(g:g)],fan_mount_mounting_holes(g:g)),op=>match op {OpThroughHole {diameter:d,center_x:x,center_y:y}=>round(diameter:micrometer_count(m:d),length:12000,axis:Axis3Z,x:micrometer_count(m:x)-micrometer_count(m:p.size_x)/2,y:55000+micrometer_count(m:y)-micrometer_count(m:p.size_y)/2,z:0)}) + } FanMountBoreReachesMountingHoles {bore_radius:_,hole_center_distance:_}=>[] FanMountBoreNotEnclosedByPlate {bore_diameter:_,plate_edge:_}=>[] FanMountPlateSmallerThanPattern {plate_edge:_,required_edge:_}=>[]} +} +fn fan_frame(i:Int,id:String) -> AssemblyPart { + let outer=i!=0 + let base=[block(w:92000,h:8000,d:36000,x:0,y:0,z:20000),block(w:92000,h:14000,d:8000,x:0,y:6000,z:2000)] + let tab=if outer {[block(w:16000,h:4000,d:24000,x:0-i*53000,y: -2000,z:20000)]} else {[block(w:92000,h:2000,d:36000,x:0,y: -5000,z:20000)]} + let tracks=if outer {[]} else {flat_map([-1,1],side=>[block(w:16500,h:4500,d:33000,x:side*41000,y: -2000,z:24200)])} + let guide=if outer {[block(w:3000,h:80000,d:120000,x:i*49000,y:53000,z:60000),block(w:10000,h:8000,d:12000,x:i*46000,y:17000,z:4000)]} else {[]} + let arm=if outer {[hole_y(d:4400,x:i*(tray_w/2-7000-fan_spacing),z:20000)]} else {[]} + let lock=if i== -1 {[hole_y(d:4400,x:55000,z:26000)]} else {if i==0 {[hole_y(d:4400,x: -39000,z:26000)]} else {[]}} + part(id:id,name:"Interlocking rear fan frame",kind:Printed,scope:Cassette,adds:concat(fan_plate_adds(),concat(base,concat(tab,guide))),cuts:concat(fan_plate_cuts(),concat(tracks,concat(arm,lock))),x:i*fan_spacing,y:0,z:rear_plate_z,ex:i*50000,ey:0,ez: -65000,plane:LayOnXZ,notes:"Fitted fan pattern retained; plate and foot are one piece. Center slides over keyed outer feet and one pin retains the bank. Outer frames include airflow guides. Two diagonal M4 bolts secure each fan and guard; the other holes take locating pins.") +} +fn guard(i:Int,id:String) -> AssemblyPart { + let rim=[block(w:92000,h:8000,d:3000,x:0,y:42000,z:0),block(w:92000,h:8000,d:3000,x:0,y: -42000,z:0),block(w:8000,h:92000,d:3000,x:42000,y:0,z:0),block(w:8000,h:92000,d:3000,x: -42000,y:0,z:0)] + let pads=flat_map([-1,1],x=>flat_map([-1,1],y=>[block(w:12000,h:12000,d:3000,x:x*fan_hole_offset,y:y*fan_hole_offset,z:0),round(diameter:8000,length:7000,axis:Axis3Z,x:x*fan_hole_offset,y:y*fan_hole_offset,z:5000)])) + let bars=flat_map([-3,-2,-1,0,1,2,3],j=>[block(w:84000,h:4000,d:3000,x:0,y:j*11000,z:0)]) + let pins=flat_map([-1,1],x=>[round(diameter:3800,length:9000,axis:Axis3Z,x:x*fan_hole_offset,y:0-x*fan_hole_offset,z:13000)]) + let holes=flat_map([-1,1],x=>[round(diameter:fan_hole_d,length:24000,axis:Axis3Z,x:x*fan_hole_offset,y:x*fan_hole_offset,z:5000)]) + part(id:id,name:"Guard with integral spacers and locating pins",kind:Printed,scope:Cassette,adds:concat(rim,concat(pads,concat(bars,pins))),cuts:holes,x:i*fan_spacing,y:55000,z:rear_plate_z-10500,ex:i*40000,ey:0,ez: -100000,plane:LayOnXY,notes:"Two diagonal bolts secure the guard/plate/fan stack; integral posts replace all loose spacers and opposite locating pins register the fan. Guard gap and blade clearance are proposals, not a certified finger-protection rating.") +} +fn retaining_pin(x:Int,z:Int,bottom:Int,top:Int,id:String) -> AssemblyPart { + part(id:id,name:"Large removable retaining pin",kind:Printed,scope:Cassette,adds:[round(diameter:4100,length:top-bottom,axis:Axis3Y,x:0,y:(top+bottom)/2,z:0),block(w:14000,h:4000,d:10000,x:0,y:top+2000,z:0)],cuts:[],x:x,y:0,z:z,ex:0,ey:45000,ez:0,plane:LayOnXZ,notes:"Unthreaded prototype retainer with a broad grip. Positive retention under vibration/load still needs assembly review; do not treat a friction fit as qualified service retention.") +} +fn printed_parts() -> List { + [panel(sx: -1,sz: -1,id:"P1"),panel(sx:1,sz: -1,id:"P2"),panel(sx: -1,sz:1,id:"P3"),panel(sx:1,sz:1,id:"P4"), + extension(sx: -1,id:"E1"),extension(sx:1,id:"E2"),fan_frame(i: -1,id:"M1"),fan_frame(i:0,id:"M2"),fan_frame(i:1,id:"M3"),guard(i: -1,id:"G1"),guard(i:0,id:"G2"),guard(i:1,id:"G3"), + rear_socket(sx: -1,id:"S1"),rear_socket(sx:1,id:"S2"), + retaining_pin(x:0-tray_w/2+7000,z:rear_beam_z,bottom: -12000,top:4000,id:"K3"),retaining_pin(x:tray_w/2-7000,z:rear_beam_z,bottom: -12000,top:4000,id:"K4"),retaining_pin(x: -39000,z:rear_plate_z+26000,bottom: -6000,top:4000,id:"K5")] +} +fn interfaces() -> List { + [mechanical_interface(id:"keyed-panel-laps",members:["P1","P2","P3","P4"],fastener:M3PanelClamp,quantity:4,status:Proposed,requirement:"Two keyed laps and one clamp per seam. Four top-accessible screws for the entire tray; joint clearance and engagement need assembly review."), + mechanical_interface(id:"slide-in-arms",members:["P1","P2","E1","E2","S1","S2"],fastener:M4SocketThroughBolt,quantity:2,status:Proposed,requirement:"Place arms in separate open sockets; retain each socket/arm/tray stack with one M4 through-bolt, washers and nut."), + mechanical_interface(id:"rear-bank-keys",members:["M1","M2","M3","K5"],fastener:PrintedRetainingPin,quantity:1,status:Proposed,requirement:"Center slides over outer keyed feet; one retainer holds the assembled bank."), + mechanical_interface(id:"rear-bank-arms",members:["M1","M3","E1","E2","K3","K4"],fastener:PrintedRetainingPin,quantity:2,status:Proposed,requirement:"Seat rear bank on arms and insert two retainers."), + mechanical_interface(id:"fan-and-guard",members:["M1","M2","M3","G1","G2","G3","F1","F2","F3"],fastener:M4FanThroughBolt,quantity:6,status:OriginalPlateFitOnly,requirement:"Two diagonal bolts per fan; opposite integral pins register it. Nominal 39 mm stack plus washers/nut; final bolt engagement unresolved."), + mechanical_interface(id:"board-support",members:["BOARD","P1","P2","P3","P4"],fastener:BoardSupportPending,quantity:0,status:Unresolved,requirement:"Known hole alignment passes are retained. Standoff thread/height and underside keep-outs remain open; no new gauge sequence."), + mechanical_interface(id:"shared-power",members:["PSU","PDB","DC","FEEDS"],fastener:ProtectedDcBranch,quantity:4,status:Unresolved,requirement:"Select rated protected/isolatable branches and fixed shared 12 V supply; no live hot-swap."), + mechanical_interface(id:"rack-ground",members:["FRAME","SHELF","PSU"],fastener:BondingHardwarePending,quantity:0,status:Unresolved,requirement:"Metal rack joining, runner attachment and permanent protective-earth bond remain hardware interfaces.")] +} diff --git a/dag/gunbc/product/printed_chassis/cassette_layout.dag b/dag/gunbc/product/printed_chassis/cassette_layout.dag new file mode 100644 index 00000000000..74adf8a083d --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cassette_layout.dag @@ -0,0 +1,65 @@ +module product.printed_chassis.cassette_layout +import std.types {Int,List} +import std.nat {nat_range_inclusive} +import std.measure {micrometer_count,millimeter_count} +import extdeps.printing.bambu_lab_a1_mini {a1_mini_build_envelope} +import extdeps.printing.fdm {build_envelope_x,build_envelope_y} +import product.printed_chassis.tray_panel_features {donor_joint_overhang_um,side_panel_overhang_um,front_handle_overhang_um} +import product.printed_chassis.bed_partition {AxisPrintAllowance,allocate_axis,AxisAllocated,AxisAllocationInvalid,AxisAllocationNoFit} +import product.printed_chassis.cassette_profile {CassetteProfile,BoardOutlineKnown,BoardOutlinePending} +import extdeps.boards.asrock_rack {asrock_altrad8ud_extent_along_rear_io,asrock_altrad8ud_extent_depth} +type CassetteLayout {board_width:Int,board_depth:Int,edge_margin:Int,tray_width:Int,tray_depth:Int} +type LayoutReading = LayoutKnown {layout:CassetteLayout} | LayoutNeedsBoardOutline | InvalidEdgeMargin +fn layout_for(profile:CassetteProfile,edge_margin_um:Int) -> LayoutReading { + if edge_margin_um<0 {return InvalidEdgeMargin} + match profile.outline {BoardOutlinePending=>LayoutNeedsBoardOutline BoardOutlineKnown {width:w,depth:d,standing:_}=>LayoutKnown {layout:layout_from_outline(width_um:micrometer_count(m:w),depth_um:micrometer_count(m:d),edge_margin_um:edge_margin_um)}} +} +// One owner for tray allowances. These dimensions reserve only the board; retained +// power/cooling/IO assemblies can enlarge the final envelope after their BOM is placed. +fn layout_from_outline(width_um:Int,depth_um:Int,edge_margin_um:Int) -> CassetteLayout { + CassetteLayout {board_width:width_um,board_depth:depth_um,edge_margin:edge_margin_um,tray_width:width_um+2*edge_margin_um,tray_depth:depth_um+2*edge_margin_um} +} +// Existing printed configuration retains its exact source dimensions. Resolve its +// known outline explicitly; unknown profiles never fall back to this row. +fn current_layout() -> CassetteLayout { + layout_from_outline(width_um:micrometer_count(m:asrock_altrad8ud_extent_along_rear_io),depth_um:micrometer_count(m:asrock_altrad8ud_extent_depth),edge_margin_um:14000) +} + +// The allocator accounts for the actual retained panel feature dimensions. The +// new multi-cell solid generator must still realize and validate these envelopes. +type TrayPrintPolicy {along_width:AxisPrintAllowance,along_depth:AxisPrintAllowance} +fn panel_print_policy(brim_each_side_um:Int,edge_clearance_each_side_um:Int) -> TrayPrintPolicy { + TrayPrintPolicy { + along_width:AxisPrintAllowance {bed_um:1000*millimeter_count(m:build_envelope_x(e:a1_mini_build_envelope)),brim_each_side_um:brim_each_side_um,edge_clearance_each_side_um:edge_clearance_each_side_um,outer_low_um:side_panel_overhang_um(),outer_high_um:side_panel_overhang_um(),internal_low_um:0,internal_high_um:donor_joint_overhang_um()}, + along_depth:AxisPrintAllowance {bed_um:1000*millimeter_count(m:build_envelope_y(e:a1_mini_build_envelope)),brim_each_side_um:brim_each_side_um,edge_clearance_each_side_um:edge_clearance_each_side_um,outer_low_um:0,outer_high_um:front_handle_overhang_um(),internal_low_um:0,internal_high_um:donor_joint_overhang_um()} + } +} +// Proposed slicing allowances, not a statement about previously printed jobs. +// A larger required brim must be supplied here and causes the layout to recompute. +data proposed_panel_print_policy:TrayPrintPolicy = panel_print_policy(brim_each_side_um:2000,edge_clearance_each_side_um:500) +type TrayCell {index:Int,column:Int,row:Int,x0_um:Int,x1_um:Int,z0_um:Int,z1_um:Int,print_width_um:Int,print_depth_um:Int} +type TrayEdge = AcrossColumns {donor:Int,receiver:Int} | AcrossRows {donor:Int,receiver:Int} +type TrayPartition {columns:Int,rows:Int,cells:List,edges:List,policy:TrayPrintPolicy} +type TrayPartitionReading = TrayPartitionKnown {partition:TrayPartition} | TrayPartitionInvalid | TrayPartitionNeedsOutline +fn partition_layout(layout:CassetteLayout,policy:TrayPrintPolicy) -> TrayPartitionReading { + match allocate_axis(span_um:layout.tray_width,allowance:policy.along_width) { + AxisAllocationInvalid=>TrayPartitionInvalid AxisAllocationNoFit=>TrayPartitionInvalid + AxisAllocated {cells:xs}=>match allocate_axis(span_um:layout.tray_depth,allowance:policy.along_depth) { + AxisAllocationInvalid=>TrayPartitionInvalid AxisAllocationNoFit=>TrayPartitionInvalid + AxisAllocated {cells:zs}=>{ + let cols=count(xs) + let rows=count(zs) + let cells=flat_map(enumerate(zs),r=>map(enumerate(xs),c=>TrayCell {index:r.first*cols+c.first,column:c.first,row:r.first,x0_um:c.second.start_um,x1_um:c.second.end_um,z0_um:r.second.start_um,z1_um:r.second.end_um,print_width_um:c.second.occupied_with_process_um,print_depth_um:r.second.occupied_with_process_um})) + let edges=flat_map(cells,c=>concat(if c.column+1 TrayPartitionReading { + match layout_for(profile:profile,edge_margin_um:edge_margin_um) { + LayoutKnown {layout:l}=>partition_layout(layout:l,policy:policy) + LayoutNeedsBoardOutline=>TrayPartitionNeedsOutline + InvalidEdgeMargin=>TrayPartitionInvalid + } +} diff --git a/dag/gunbc/product/printed_chassis/cassette_profile.dag b/dag/gunbc/product/printed_chassis/cassette_profile.dag new file mode 100644 index 00000000000..e05a7a6a0b7 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cassette_profile.dag @@ -0,0 +1,35 @@ +module product.printed_chassis.cassette_profile +import v2.std.optional {Present} +import std.types {NonEmptyStr,List,Int} +import std.measure {Micrometer,micrometer_count} +import product.printed_chassis.bed_partition {AxisPrintAllowance,allocate_axis,AxisAllocated,AxisAllocationInvalid,AxisAllocationNoFit} +import extdeps.external_authority {CitedFigureStanding} +import product.printed_chassis.board_mechanical_reference {BoardMountingReference} +type BoardOutline = BoardOutlineKnown {width:Micrometer,depth:Micrometer,standing:CitedFigureStanding} | BoardOutlinePending +type PowerArchitecture = RetainPlatformPowerAssembly | IndependentPowerCassette +// Product requirements; these are not claims that the incoming hardware includes them. +type RetainedSubsystem = ComputeAssembly | VendorPowerDistribution | PsuCageAndEarth + | ManagementAndSequencing | CoolingAndDucting | IoAndRisers | HarnessAndFrontPanel + | SocketBackplatesAndLoadPath +type DesignObligation = ExactAssemblyRevision | MountingCoordinates | UndersideKeepouts + | PowerPinoutAndEnable | ManagementDependencies | ThermalAndAirflow | CableSweptVolumes + | ComponentMassAndPlacement | StructuralJointsAndCreep | EarthAndMainsEnclosure + | PrinterSegmentation | CoolerLoadAndTransport +type CassetteProfile {id:NonEmptyStr,requested_hardware:NonEmptyStr,outline:BoardOutline,mounting:BoardMountingReference,power:PowerArchitecture,retain:List,open:List} +// Axis-aligned board-envelope tiling only, not a printable tray or minimum arbitrary packing. +fn board_grid_tiles(project:CassetteProfile,usable_tile_um:Int) -> Int? { + let a=AxisPrintAllowance {bed_um:usable_tile_um,brim_each_side_um:0,edge_clearance_each_side_um:0,outer_low_um:0,outer_high_um:0,internal_low_um:0,internal_high_um:0} + match project.outline { + BoardOutlinePending=>none + BoardOutlineKnown {width:w,depth:d,standing:_}=>match allocate_axis(span_um:micrometer_count(m:w),allowance:a) { + AxisAllocationInvalid=>none AxisAllocationNoFit=>none + AxisAllocated {cells:xs}=>match allocate_axis(span_um:micrometer_count(m:d),allowance:a) { + AxisAllocationInvalid=>none AxisAllocationNoFit=>none + AxisAllocated {cells:zs}=>Present {value:count(xs)*count(zs)} + } + } + } +} + +data transplant_obligations:List = [ExactAssemblyRevision,MountingCoordinates,UndersideKeepouts,PowerPinoutAndEnable,ManagementDependencies,ThermalAndAirflow,CableSweptVolumes,ComponentMassAndPlacement,StructuralJointsAndCreep,EarthAndMainsEnclosure,PrinterSegmentation,CoolerLoadAndTransport] +data retained_platform_subsystems:List = [ComputeAssembly,VendorPowerDistribution,PsuCageAndEarth,ManagementAndSequencing,CoolingAndDucting,IoAndRisers,HarnessAndFrontPanel,SocketBackplatesAndLoadPath] diff --git a/dag/gunbc/product/printed_chassis/cassette_profiles.dag b/dag/gunbc/product/printed_chassis/cassette_profiles.dag new file mode 100644 index 00000000000..8e3481b461f --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cassette_profiles.dag @@ -0,0 +1,44 @@ +module product.printed_chassis.cassette_profiles +import std.measure {micrometer_count} +import std.types {List} +import product.printed_chassis.cassette_profile {CassetteProfile,BoardOutline,BoardOutlineKnown,BoardOutlinePending,RetainPlatformPowerAssembly,IndependentPowerCassette,transplant_obligations,retained_platform_subsystems} +import product.printed_chassis.board_mechanical_reference {StandardLocationReference,ImageLocationReference,MountingReferencePending,jade_image_mount_reading} +import extdeps.external_authority {CitedToAuthority} +import extdeps.ocp.mt_jade.platform {mt_jade_motherboard_mechanical} +import extdeps.ocp.mt_jade.subject {mt_jade_subject_authority_anchor} +import extdeps.portwell.aptsa_627211_b4 {collins_datasheet_mechanicals} +import extdeps.boards.asrock_rack {asrock_altrad8ud_extent_along_rear_io,asrock_altrad8ud_extent_depth,asrock_altrad8ud_standard_location_correspondence,extdeps_external_authority_anchor} +// Configuration rows for one generator. Each profile carries its mounting +// evidence directly; changing an id cannot detach it from that evidence. +data altra_profile:CassetteProfile = CassetteProfile { + id:"altrad8ud",requested_hardware:"ASRock Rack ALTRAD8UD-1L2T", + outline:BoardOutlineKnown {width:asrock_altrad8ud_extent_along_rear_io,depth:asrock_altrad8ud_extent_depth,standing:CitedToAuthority {authority:extdeps_external_authority_anchor}}, + mounting:StandardLocationReference {locations:asrock_altrad8ud_standard_location_correspondence}, + power:IndependentPowerCassette,retain:[],open:transplant_obligations +} +data gh200_p4261_profile:CassetteProfile = CassetteProfile { + id:"gh200-p4261",requested_hardware:"NVIDIA P4261 / 694-24261-000-100 K1",outline:BoardOutlinePending, + mounting:MountingReferencePending {read_obligation:"Revision-matched P4261 mechanical drawing and mounting map required; no other NVIDIA board substituted."}, + power:RetainPlatformPowerAssembly,retain:retained_platform_subsystems,open:transplant_obligations +} +data mt_jade_profile:CassetteProfile = CassetteProfile { + id:"mt-jade",requested_hardware:"Ampere Mt. Jade board transplant", + outline:BoardOutlineKnown {width:mt_jade_motherboard_mechanical.outline_width,depth:mt_jade_motherboard_mechanical.outline_depth,standing:CitedToAuthority {authority:mt_jade_subject_authority_anchor}}, + mounting:ImageLocationReference {reading:jade_image_mount_reading}, + power:RetainPlatformPowerAssembly,retain:retained_platform_subsystems,open:transplant_obligations +} +// Axis assignment is this product's inference from Portwell's two dimensions and +// chassis width. Only the short side fits across the chassis. A changed source +// that no longer discriminates the axes refuses instead of silently rotating. +fn collins_outline() -> BoardOutline { + let r=collins_datasheet_mechanicals + if micrometer_count(m:r.first_board_dimension)>micrometer_count(m:r.chassis_width) && micrometer_count(m:r.second_board_dimension) = [altra_profile,gh200_p4261_profile,mt_collins_profile,mt_jade_profile] diff --git a/dag/gunbc/product/printed_chassis/cassette_review.dag b/dag/gunbc/product/printed_chassis/cassette_review.dag new file mode 100644 index 00000000000..8dfbe1ad435 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cassette_review.dag @@ -0,0 +1,64 @@ +module product.printed_chassis.cassette_review + +import std.types { Int, String, List } +import std.measure { micrometer_count, millimeter_count } +import std.affine_space { Point3, signed_frame_component_count } +import std.euclidean_geometry { Axis3X, Axis3Y, Axis3Z } +import extdeps.languages.json.emit { JsonValue,json_object,json_kv,json_string,json_int,json_array,json_bool,json_null,serialize_json } +import gunbc.cli_wire { CliWireResponse,CliWirePrintable } +import std.process { ExitSuccess,exit_failure } +import product.printed_chassis.assembly_review_checks {review_model_checks} +import product.printed_chassis.assembly_geometry { Primitive,Block,RoundPrism,AssemblyPart,MechanicalInterface,Printed,Purchased,Metal,Clearance,Cassette,FixedFrame,LayOnXZ,LayOnXY,LayOnYZ,InterfaceStatus,FastenerKind,M3PanelClamp,PrintedRetainingPin,M4FanThroughBolt,M4SocketThroughBolt,BoardSupportPending,ProtectedDcBranch,BondingHardwarePending,Proposed,OriginalPlateFitOnly,Unresolved } +import product.printed_chassis.cassette { printed_parts,interfaces,board_w,board_d,tray_w,tray_d,bay_w,bay_pitch,m3_clearance,fan_hole_d } +import product.printed_chassis.fit_observations {fan_fit_display} +import product.printed_chassis.rack_assembly { hardware,fixed_parts } +import product.printed_chassis.export_profile { exercised_stl_knobs } +import extdeps.printing.bambu_lab_a1_mini { a1_mini_build_envelope } +import extdeps.printing.fdm { build_envelope_x,build_envelope_y,build_envelope_z } + +fn xyz(p:Point3) -> JsonValue { json_array(elements:[json_int(n:signed_frame_component_count(c:p.x)),json_int(n:signed_frame_component_count(c:p.y)),json_int(n:signed_frame_component_count(c:p.z))]) } +fn text(s:String) -> JsonValue { json_string(s:s) } +fn primitive_json(p:Primitive) -> JsonValue { + match p { + Block {size:s,center:c} => json_object(members:[json_kv(key:"op",value:text(s:"box")),json_kv(key:"size_um",value:json_array(elements:[json_int(n:micrometer_count(m:s.size_x)),json_int(n:micrometer_count(m:s.size_y)),json_int(n:micrometer_count(m:s.size_z))])),json_kv(key:"center_um",value:xyz(p:c))]), + RoundPrism {diameter:d,length:l,axis:a,center:c} => json_object(members:[json_kv(key:"op",value:text(s:"cylinder")),json_kv(key:"diameter_um",value:json_int(n:micrometer_count(m:d))),json_kv(key:"length_um",value:json_int(n:micrometer_count(m:l))),json_kv(key:"axis",value:text(s:match a {Axis3X=>"x",Axis3Y=>"y",Axis3Z=>"z"})),json_kv(key:"center_um",value:xyz(p:c))]), + } +} +fn part_json(p:AssemblyPart) -> JsonValue { + json_object(members:[json_kv(key:"id",value:text(s:p.id)),json_kv(key:"name",value:text(s:p.name)),json_kv(key:"kind",value:text(s:match p.kind {Printed=>"printed",Purchased=>"purchased",Metal=>"metal",Clearance=>"clearance"})),json_kv(key:"scope",value:text(s:match p.scope {Cassette=>"cassette",FixedFrame=>"fixed"})),json_kv(key:"adds",value:json_array(elements:p.adds |> map(a=>primitive_json(p:a)))),json_kv(key:"cuts",value:json_array(elements:p.cuts |> map(a=>primitive_json(p:a)))),json_kv(key:"at_um",value:xyz(p:p.at)),json_kv(key:"explode_um",value:xyz(p:p.explode)),json_kv(key:"print_plane",value:text(s:match p.print_plane {LayOnXZ=>"XZ",LayOnXY=>"XY",LayOnYZ=>"YZ"})),json_kv(key:"notes",value:text(s:p.notes))]) +} +fn fastener_display(f:FastenerKind) -> String { + match f {M3PanelClamp=>"M3 top-accessible panel clamp + nut" M4SocketThroughBolt=>"M4 socket through-bolt + washers + nut" M4FanThroughBolt=>"M4 diagonal through-bolt + nut" PrintedRetainingPin=>"Broad-grip printed retaining pin" BoardSupportPending=>"Standoff hardware unresolved" ProtectedDcBranch=>"Rated protected/isolatable DC branch" BondingHardwarePending=>"Permanent metal bonding hardware unresolved"} +} +fn status_display(s:InterfaceStatus) -> String {match s {Proposed=>"proposed" OriginalPlateFitOnly=>"original plate fit passed; new stack proposed" Unresolved=>"unresolved"}} +fn interface_json(i:MechanicalInterface) -> JsonValue { + json_object(members:[json_kv(key:"id",value:text(s:i.id)),json_kv(key:"members",value:json_array(elements:i.members |> map(x=>text(s:x)))),json_kv(key:"fastener",value:text(s:fastener_display(f:i.fastener))),json_kv(key:"quantity",value:json_int(n:i.quantity)),json_kv(key:"status",value:text(s:status_display(s:i.status))),json_kv(key:"requirement",value:text(s:i.requirement))]) +} +fn all_parts() -> List { concat(printed_parts(),concat(hardware(),fixed_parts())) } +fn vector(x:Int,y:Int,z:Int) -> JsonValue { json_array(elements:[json_int(n:x),json_int(n:y),json_int(n:z)]) } +fn node(id:String,x:Int,y:Int) -> JsonValue { json_object(members:[json_kv(key:"id",value:text(s:id)),json_kv(key:"at_um",value:vector(x:x,y:y,z:0))]) } +fn airflow(x:Int) -> JsonValue { json_object(members:[json_kv(key:"direction",value:vector(x:0,y:0,z:-1)),json_kv(key:"start_um",value:vector(x:x,y:105000,z:tray_d/2+55000)),json_kv(key:"length_um",value:json_int(n:tray_d+140000))]) } +fn assembly_steps() -> List { + ["Mate the four keyed tray panels; install four top-accessible M3 clamps.", + "Slide the two rear arms into the tray sockets and insert their two broad-grip retainers.", + "Slide the center fan frame over the two outer frames; insert the center retainer.", + "Seat the fan bank on the arms and insert the two rear retainers.", + "Fit each fan and its guard over the locating pins; install two diagonal M4 bolts per fan."] +} +fn manifest_for(parts:List,joints:List) -> JsonValue { + json_object(members:[json_kv(key:"schema",value:text(s:"gunbc.cassette-review.v1")),json_kv(key:"revision",value:text(s:"06")),json_kv(key:"quantum",value:text(s:"micrometre")),json_kv(key:"axes",value:text(s:"X across rear I/O; Y up; +Z toward front handle")),json_kv(key:"release",value:text(s:"review-only; no print or powered-operation admission")),json_kv(key:"fan_fit",value:text(s:fan_fit_display())), + json_kv(key:"board_um",value:json_array(elements:[json_int(n:board_w),json_int(n:board_d)])),json_kv(key:"tray_um",value:json_array(elements:[json_int(n:tray_w),json_int(n:tray_d)])),json_kv(key:"bay_pitch_um",value:json_int(n:bay_pitch)),json_kv(key:"bay_width_um",value:json_int(n:bay_w)),json_kv(key:"nodes_per_block",value:json_int(n:4)),json_kv(key:"intended_nodes",value:json_int(n:8)), + json_kv(key:"assembly_steps",value:json_array(elements:map(assembly_steps(),s=>text(s:s)))), + json_kv(key:"fastener_clearance_um",value:json_array(elements:[json_int(n:m3_clearance),json_int(n:fan_hole_d)])), + json_kv(key:"nodes",value:json_array(elements:[node(id:"N1",x:-bay_w/2,y:0),node(id:"N2",x:bay_w/2,y:0),node(id:"N3",x:-bay_w/2,y:bay_pitch),node(id:"N4",x:bay_w/2,y:bay_pitch)])), + json_kv(key:"airflow",value:json_array(elements:[airflow(x:-75000),airflow(x:75000)])), + json_kv(key:"export",value:json_object(members:[json_kv(key:"linear_tolerance_um",value:json_int(n:micrometer_count(m:exercised_stl_knobs.linear_tolerance))),json_kv(key:"angular_tolerance_radians",value:text(s:exercised_stl_knobs.angular_tolerance_text as String))])), + json_kv(key:"power",value:json_object(members:[json_kv(key:"placement",value:text(s:"fixed-frame shared 12 V system per four-node block")),json_kv(key:"branches",value:json_int(n:4)),json_kv(key:"rated_watts",value:json_null()),json_kv(key:"live_hot_swap",value:json_bool(b:false)),json_kv(key:"redundancy",value:json_null())])), + json_kv(key:"print_envelope_mm",value:json_array(elements:[json_int(n:millimeter_count(m:build_envelope_x(e:a1_mini_build_envelope))),json_int(n:millimeter_count(m:build_envelope_y(e:a1_mini_build_envelope))),json_int(n:millimeter_count(m:build_envelope_z(e:a1_mini_build_envelope)))])), + json_kv(key:"parts",value:json_array(elements:parts |> map(p=>part_json(p:p)))),json_kv(key:"interfaces",value:json_array(elements:joints |> map(i=>interface_json(i:i)))), + json_kv(key:"unresolved",value:json_array(elements:[text(s:"Board standoff thread, installed height and underside keep-outs; no new gauge sequence."),text(s:"Fastener lengths/engagement, support material, rail clearance and loaded joint performance."),text(s:"Cooler location/orientation and rear I/O/cable service access."),text(s:"Shared supply selection, branch circuitry, protective earth and thermal validation."),text(s:"Drives and expansion cards are unspecified, not silently omitted requirements.")]))]) +} +fn manifest() -> JsonValue {manifest_for(parts:all_parts(),joints:interfaces())} +fn main() -> CliWireResponse { + if review_model_checks(parts:all_parts(),interfaces:interfaces()) {CliWirePrintable {bytes:serialize_json(v:manifest()),exit:ExitSuccess}} else {CliWirePrintable {bytes:"Assembly source checks refused",exit:exit_failure(reason:"Assembly source checks refused")}} +} diff --git a/dag/gunbc/product/printed_chassis/component_mass.dag b/dag/gunbc/product/printed_chassis/component_mass.dag new file mode 100644 index 00000000000..aec875976fc --- /dev/null +++ b/dag/gunbc/product/printed_chassis/component_mass.dag @@ -0,0 +1,131 @@ +module product.printed_chassis.component_mass + +import product.printed_chassis.cooler_configuration {CoolerConfiguration,DynatronW1,NoctuaU14sAmp4926,noctua_envelope} +import extdeps.cooling.noctua_ampere {nh_u14s_amp_4926} +import std.types {Bool,Int,List,String,NonEmptyStr} +import std.measure {Gram,gram,gram_count} +import std.affine_space {Point3,signed_frame_component_count} +import v2.std.optional {Present,Absent} +import extdeps.cpu.ampere {ampere_altra_max_typical_package_mass} +import extdeps.memory.samsung {m393a8g40d40_crb0q_catalog} +import extdeps.memory.types {HostMemoryPopulation} +import extdeps.cooling.dynatron {dynatron_w1_mass_active,dynatron_w1_mass_tolerance} +import extdeps.storage.samsung {samsung_970_evo_family_max_mass} +import extdeps.fans.arctic {arctic_p8_mass} +import product.printed_chassis.assembly_geometry {AssemblyPart,Printed,Purchased,Metal,Clearance,placed} +import product.printed_chassis.stack_geometry {cassette_parts,part_bounds} + +// Each component has its own mass and position evidence. This roster replaces the +// legacy BOARD/RAM slab-only mass approximation. Missing mass never folds as zero. +// Positions inherited from review CAD are envelope centres, NOT measured centres of mass. +type MassEvidence + = VendorNominal {mass:Gram} + | BoundedMass {lower:Gram,upper:Gram,basis:NonEmptyStr} + | PlanningMassRange {lower:Gram,upper:Gram,basis:NonEmptyStr} + | MassNeeded {reason:MassNeed} +type MassNeed = BoardWithCpu | BoardWithSocket | ProcessorPackage | OneDimm | SlicedOrWeighedPrint + | MountingHardware | HarnessAndConnectors | SelectedPowerSupply | DistributionHardware + | PowerEnclosure | CornerStructure | ServiceSupport + +type PositionEvidence = EnvelopeCentre | PlacementNeeded | SurveyedCentre + +type MassComponent { + id:String,quantity:Int,mass:MassEvidence,at:Point3,position:PositionEvidence, +} +type MassMeasurement {id:String,lower:Gram,upper:Gram,basis:NonEmptyStr} +type MassIssue = EmptyInventory | MissingMass {id:String,reason:MassNeed} + | DuplicateComponent {id:String} | DuplicateMeasurement {id:String} + | UnknownMeasurement {id:String} | InvalidMassRange {id:String} | InvalidQuantity {id:String} +type MassTotals {lower:Gram,upper:Gram,components:Int,nominal_vendor_rows:Int,estimated_rows:Int,provisional_positions:Int} +type MassLedger = MassIncomplete {issues:List} | MassComplete {total:MassTotals,rows:List} + +fn component(id:String,quantity:Int,mass:MassEvidence,x:Int,y:Int,z:Int,position:PositionEvidence) -> MassComponent { + MassComponent {id:id,quantity:quantity,mass:mass,at:placed(x:x,y:y,z:z),position:position} +} +fn envelope_centre(p:AssemblyPart) -> Point3 { + match part_bounds(p:p) { + Absent=>p.at + Present {value:b}=>placed(x:signed_frame_component_count(c:p.at.x)+(b.lo_x+b.hi_x)/4,y:signed_frame_component_count(c:p.at.y)+(b.lo_y+b.hi_y)/4,z:signed_frame_component_count(c:p.at.z)+(b.lo_z+b.hi_z)/4) + } +} +fn geometry_component(p:AssemblyPart) -> MassComponent { + let mass=if p.id=="CPU" {BoundedMass {lower:gram(count:gram_count(g:dynatron_w1_mass_active)-gram_count(g:dynatron_w1_mass_tolerance)),upper:gram(count:gram_count(g:dynatron_w1_mass_active)+gram_count(g:dynatron_w1_mass_tolerance)),basis:"Dynatron active W1 vendor mass +/- tolerance; includes its fan"}} + else {if p.id=="F1" || p.id=="F2" || p.id=="F3" {VendorNominal {mass:arctic_p8_mass}} else {MassNeeded {reason:SlicedOrWeighedPrint}}} + MassComponent {id:if p.id=="CPU" {"COOLER"} else {p.id},quantity:1,mass:mass,at:envelope_centre(p:p),position:EnvelopeCentre} +} +fn dimm_count(population:HostMemoryPopulation) -> Int {fold(population.rows,init:0,f:(n,r)=>n+r.stick_count)} + +// RAM bank slab geometry is not another mass. The population supplies the count. +// Until surveyed, all DIMMs share a central placement placeholder: the ledger reports +// that fact, so its mass completeness is never confused with stability qualification. +fn node_components(population:HostMemoryPopulation) -> List { + let geometry=map(filter(cassette_parts(),p=>match p.kind {Printed=>true Purchased=>p.id=="CPU" || p.id=="F1" || p.id=="F2" || p.id=="F3" Metal=>p.id=="CPU" || p.id=="F1" || p.id=="F2" || p.id=="F3" Clearance=>p.id=="CPU" || p.id=="F1" || p.id=="F2" || p.id=="F3"}),p=>geometry_component(p:p)) + concat(geometry,[ + component(id:"BOARD",quantity:1,mass:board_planning_mass,x:0,y:27000,z:0,position:EnvelopeCentre), + component(id:"PROCESSOR",quantity:1,mass:VendorNominal {mass:ampere_altra_max_typical_package_mass},x:0,y:29000,z:0,position:PlacementNeeded), + component(id:"DIMMS",quantity:dimm_count(population:population),mass:if all(population.rows,r=>r.module.part_number==m393a8g40d40_crb0q_catalog.part_number) {dimm_planning_mass} else {MassNeeded {reason:OneDimm}},x:0,y:47000,z:0,position:PlacementNeeded), + component(id:"SSD",quantity:1,mass:BoundedMass {lower:gram(count:0),upper:samsung_970_evo_family_max_mass,basis:"Samsung 970 EVO / EVO Plus brochure: maximum 8 g; this is an upper bound, not an 8 g measurement"},x:0,y:29000,z:0,position:PlacementNeeded), + component(id:"BOARD_MOUNTING",quantity:1,mass:MassNeeded {reason:MountingHardware},x:0,y:18000,z:0,position:PlacementNeeded), + component(id:"FASTENERS",quantity:1,mass:MassNeeded {reason:MountingHardware},x:0,y:30000,z:0,position:PlacementNeeded), + component(id:"NODE_HARNESS",quantity:1,mass:MassNeeded {reason:HarnessAndConnectors},x:0,y:30000,z:0,position:PlacementNeeded), + component(id:"CORNER_STRUCTURE",quantity:1,mass:MassNeeded {reason:CornerStructure},x:0,y:65000,z:0,position:PlacementNeeded), + ]) +} +fn station_components() -> List {[ + component(id:"PSU",quantity:1,mass:MassNeeded {reason:SelectedPowerSupply},x:0,y:50000,z:0,position:PlacementNeeded), + component(id:"POWER_DISTRIBUTION",quantity:1,mass:MassNeeded {reason:DistributionHardware},x:0,y:30000,z:0,position:PlacementNeeded), + component(id:"POWER_HARNESS",quantity:1,mass:MassNeeded {reason:HarnessAndConnectors},x:0,y:30000,z:0,position:PlacementNeeded), + component(id:"POWER_CASSETTE",quantity:1,mass:MassNeeded {reason:PowerEnclosure},x:0,y:50000,z:0,position:PlacementNeeded), +]} +fn resolve_mass(row:MassComponent,measurements:List) -> MassComponent { + let found=filter(measurements,m=>m.id==row.id) + let mass=match found.first() {Absent=>row.mass Present {value:m}=>BoundedMass {lower:m.lower,upper:m.upper,basis:m.basis}} + MassComponent {id:row.id,quantity:row.quantity,mass:mass,at:row.at,position:row.position} +} +fn mass_range_valid(m:MassEvidence) -> Bool {match m {VendorNominal {mass:g}=>gram_count(g:g)>0 PlanningMassRange {lower:l,upper:u,basis:_}=>gram_count(g:u)>0 && gram_count(g:u)>=gram_count(g:l) BoundedMass {lower:l,upper:u,basis:_}=>gram_count(g:u)>0 && gram_count(g:u)>=gram_count(g:l) MassNeeded {reason:_}=>true}} +fn lower_mass(m:MassEvidence) -> Gram {match m {VendorNominal {mass:g}=>g PlanningMassRange {lower:l,upper:_,basis:_}=>l BoundedMass {lower:l,upper:_,basis:_}=>l MassNeeded {reason:_}=>gram(count:0)}} +fn upper_mass(m:MassEvidence) -> Gram {match m {VendorNominal {mass:g}=>g PlanningMassRange {lower:_,upper:u,basis:_}=>u BoundedMass {lower:_,upper:u,basis:_}=>u MassNeeded {reason:_}=>gram(count:0)}} + +fn mass_ledger(components:List,measurements:List) -> MassLedger { + if count(components)==0 {return MassIncomplete {issues:[EmptyInventory]}} + let rows=map(components,r=>resolve_mass(row:r,measurements:measurements)) + let component_issues=flat_map(rows,r=>concat( + if count(filter(rows,q=>q.id==r.id))!=1 {[DuplicateComponent {id:r.id}]} else {[]}, + concat(if r.quantity<=0 {[InvalidQuantity {id:r.id}]} else {[]}, + concat(if !mass_range_valid(m:r.mass) {[InvalidMassRange {id:r.id}]} else {[]}, + match r.mass {MassNeeded {reason:n}=>[MissingMass {id:r.id,reason:n}] VendorNominal {mass:_}=>[] PlanningMassRange {lower:_,upper:_,basis:_}=>[] BoundedMass {lower:_,upper:_,basis:_}=>[]})))) + let measurement_issues=flat_map(measurements,m=>concat( + if count(filter(measurements,q=>q.id==m.id))!=1 {[DuplicateMeasurement {id:m.id}]} else {[]}, + if !any(rows,r=>r.id==m.id) {[UnknownMeasurement {id:m.id}]} else {[]})) + let issues=concat(component_issues,measurement_issues) + if count(issues)>0 {MassIncomplete {issues:issues}} else { + MassComplete {rows:rows,total:MassTotals { + lower:gram(count:fold(rows,init:0,f:(g,r)=>g+r.quantity*gram_count(g:lower_mass(m:r.mass)))), + upper:gram(count:fold(rows,init:0,f:(g,r)=>g+r.quantity*gram_count(g:upper_mass(m:r.mass)))), + components:fold(rows,init:0,f:(n,r)=>n+r.quantity), + nominal_vendor_rows:count(filter(rows,r=>match r.mass {VendorNominal {mass:_}=>true PlanningMassRange {lower:_,upper:_,basis:_}=>false BoundedMass {lower:_,upper:_,basis:_}=>false MassNeeded {reason:_}=>false})), + estimated_rows:count(filter(rows,r=>match r.mass {PlanningMassRange {lower:_,upper:_,basis:_}=>true VendorNominal {mass:_}=>false BoundedMass {lower:_,upper:_,basis:_}=>false MassNeeded {reason:_}=>false})), + provisional_positions:count(filter(rows,r=>r.position!=SurveyedCentre)), + }} + } +} + +// Operator direction: research online and use explicit planning ranges; do not +// request weighing or disassembly. These are scenario assumptions, not vendor +// tolerances or certified bounds. A complete estimated ledger is still only a screen. +data board_planning_mass: MassEvidence = PlanningMassRange { + lower:gram(count:910),upper:gram(count:1700), + basis:"Operator-supplied 0.91-1.70 kg planning bracket. Published store weights conflict and may include packaging; ASRock's reviewed specification gives geometry, not a verified bare-board mass. Do not treat either endpoint as a certified bound.", +} +data dimm_planning_mass: MassEvidence = PlanningMassRange { + lower:gram(count:20),upper:gram(count:100), + basis:"Engineering sensitivity allowance, 20-100 g per bare Samsung M393A8G40D40-CRB-family DIMM. Samsung Rev1.3 section18 specifies a 133.35 x 31.25 x 3.9 mm maximum envelope, not mass. No supplier shipping weight is promoted to component weight; endpoints are assumed, not certified.", +} + +// Swap the cooler assembly once; its two fans are already included in vendor mass. +fn node_components_for_cooler(population:HostMemoryPopulation,cooler:CoolerConfiguration,contact_plane_um:Int) -> List { + map(node_components(population:population),r=>if r.id=="COOLER" {match cooler { + DynatronW1=>r + NoctuaU14sAmp4926=>MassComponent {id:r.id,quantity:1,mass:VendorNominal {mass:nh_u14s_amp_4926.mass},at:envelope_centre(p:noctua_envelope(contact_plane_um:contact_plane_um)),position:EnvelopeCentre} + }} else {r}) +} diff --git a/dag/gunbc/product/printed_chassis/cooler_configuration.dag b/dag/gunbc/product/printed_chassis/cooler_configuration.dag new file mode 100644 index 00000000000..ef82cde94d0 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/cooler_configuration.dag @@ -0,0 +1,15 @@ +module product.printed_chassis.cooler_configuration +import std.types {Int} +import std.measure {micrometer_count} +import extdeps.cooling.noctua_ampere {nh_u14s_amp_4926} +import product.printed_chassis.assembly_geometry {AssemblyPart,Purchased,Cassette,LayOnXZ,block,part} +type CoolerConfiguration = DynatronW1 | NoctuaU14sAmp4926 +// User's residential design selection. Existing fleet observations still use W1. +data residential_cooler:CoolerConfiguration=NoctuaU14sAmp4926 +// Contact-plane height and pose remain review assumptions. The envelope is vendor +// data; a rectangular box is not a socket/RAM collision qualification or exact CG. +fn noctua_envelope(contact_plane_um:Int) -> AssemblyPart { + let c=nh_u14s_amp_4926 + let h=micrometer_count(m:c.height) + part(id:"CPU",name:"Noctua NH-U14S AMP-4926 dual-fan envelope",kind:Purchased,scope:Cassette,adds:[block(w:micrometer_count(m:c.width),h:h,d:micrometer_count(m:c.depth),x:0,y:h/2,z:0)],cuts:[],x:0,y:contact_plane_um,z:0,ex:0,ey:200000,ez:0,plane:LayOnXZ,notes:"Vendor 165 x 150 x 111 mm with both fans. Pose/contact plane provisional; airflow perpendicular to socket long axis requires board-specific alignment. RAM, clips, cable and extraction clearances unresolved.") +} diff --git a/dag/gunbc/product/printed_chassis/exhaust_thermal.dag b/dag/gunbc/product/printed_chassis/exhaust_thermal.dag new file mode 100644 index 00000000000..cd30513bcc5 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/exhaust_thermal.dag @@ -0,0 +1,99 @@ +module product.printed_chassis.exhaust_thermal +import extdeps.fans.arctic {arctic_p8_operating_ambient_max} + +import std.types { Bool, Int, List, NonEmptyStr } +import std.nat { Nat } +import std.measure { + Watt, watt_count, Celsius, celsius, celsius_count, CelsiusDelta, celsius_delta, celsius_delta_count, + MillilitrePerSecond, millilitre_per_second, millilitre_per_second_count, + gram_per_cubic_metre_count, joule_per_kilogram_kelvin_count, Percent, percent_count, +} +import extdeps.physics.dry_air { dry_air_at_20c } +import extdeps.fans.arctic { arctic_p8_published_airflow } +import extdeps.printing.fdm { FilamentTechnicalDataSheetRow } +import extdeps.cpu.ampere { ampere_altra_max_m12830_thermal_limits } +import extdeps.boards.asrock_rack { asrock_altrad8ud_operating_ambient_max } + +// Bulk-air sensitivity only: dT = Q / (rho V cp), with the explicit 20 C air +// properties. Catalog free-air flow is not installed flow; a percentage of that flow +// is NOT PWM free_air_fraction. No result here qualifies a mount or establishes a BMC fan curve. +// The 100 C die limit is not a local mount temperature. Radiation, conduction, +// recirculation, fan/system impedance and transient failure require separate evidence. +// HDT is a short-duration deflection test, not melting or continuous service rating. +type UnmodelledThermalCoupling + = RadiationFromHeatsinkFins + | ConductionThroughStandoffs + | CoolerFanInteractionWithBank + | HdtStressDependence + +data unmodelled_couplings: List = [RadiationFromHeatsinkFins, ConductionThroughStandoffs, CoolerFanInteractionWithBank, HdtStressDependence] + +// Exhaust rise rounded UP to whole kelvin: Q[W] / (rho[g/m3] * V[mL/s] * 1e-9 [kg/mL*m3/g... ] * cp) +// Arithmetic: mass flow in mg/s = rho(g/m3) * V(mL/s) / 1000; dT = Q * 1e6 / (mdot_mg_s * cp). +fn exhaust_rise(heat: Watt, flow: MillilitrePerSecond) -> CelsiusDelta? { + let mass_flow_mg_s = gram_per_cubic_metre_count(d: dry_air_at_20c.density) * millilitre_per_second_count(f: flow) / 1000 + if mass_flow_mg_s == 0 { none } else { + Present { value: celsius_delta(count: (watt_count(w: heat) * 1000000 + mass_flow_mg_s * joule_per_kilogram_kelvin_count(c: dry_air_at_20c.specific_heat_cp) - 1) / (mass_flow_mg_s * joule_per_kilogram_kelvin_count(c: dry_air_at_20c.specific_heat_cp))) } + } +} + +// Hypothetical fraction of aggregate free-air flow; no PWM-to-flow mapping is implied. +fn bank_flow(fans: Nat, free_air_fraction: Percent) -> MillilitrePerSecond { + millilitre_per_second(count: fans * millilitre_per_second_count(f: arctic_p8_published_airflow) * percent_count(p: free_air_fraction) / 100) +} + +type ThermalMargin + = AssumedAirBelowHdt { exhaust: Celsius, hdt: Celsius, margin: CelsiusDelta } + | AssumedAirMarginInsufficient { exhaust: Celsius, hdt: Celsius, excess: CelsiusDelta } + | NoAirflow + +type ExhaustCase { + node_heat: Watt + fans: Nat + free_air_fraction: Percent + inlet: Celsius + margin_required: CelsiusDelta +} + +fn assumed_air_margin(case: ExhaustCase, sheet: FilamentTechnicalDataSheetRow) -> ThermalMargin { + match exhaust_rise(heat: case.node_heat, flow: bank_flow(fans: case.fans, free_air_fraction: case.free_air_fraction)) { + Absent => NoAirflow + Present { value: rise } => { + let exhaust = celsius(count: celsius_count(c: case.inlet) + celsius_delta_count(delta: rise)) + let hdt = sheet.heat_deflection_at_0_45_mpa + let headroom = celsius_count(c: hdt) - celsius_count(c: exhaust) + if headroom >= celsius_delta_count(delta: case.margin_required) { + AssumedAirBelowHdt { exhaust: exhaust, hdt: hdt, margin: celsius_delta(count: headroom) } + } else { + AssumedAirMarginInsufficient { exhaust: exhaust, hdt: hdt, excess: celsius_delta(count: celsius_delta_count(delta: case.margin_required) - headroom) } + } + } + } +} + +// Board-rated ambient ceiling for sensitivity cases; local fan inlet is a separate limit. +fn worst_inlet() -> Celsius { asrock_altrad8ud_operating_ambient_max } + +// The junction limit is NOT an input to the air derivation; it is exposed so a reader can see +// the two numbers side by side and stop asking whether the mount sees the die. +fn processor_junction_limit() -> Celsius { ampere_altra_max_m12830_thermal_limits.maximum_continuous_junction_temperature } + + +// Installed airflow must be measured or solved at the fan/system pressure-curve +// intersection. PWM percentage and catalog free-air flow cannot establish it. +// HDT is a short-duration test, not an allowable service temperature (ISO 75-1). +type InstalledThermalReading + = InstalledThermalUnresolved { required:NonEmptyStr } + | MeasuredMountExceedsScreen { mount:Celsius, screen:Celsius } + | MeasuredMountBelowScreenOnly { mount:Celsius, screen:Celsius, unresolved:NonEmptyStr } +fn measured_mount_screen(mount:Celsius,lower_hdt:Celsius,margin:CelsiusDelta) -> InstalledThermalReading { + let limit=celsius(count:celsius_count(c:lower_hdt)-celsius_delta_count(delta:margin)) + if celsius_count(c:mount)>=celsius_count(c:limit) {MeasuredMountExceedsScreen {mount:mount,screen:limit}} + else {MeasuredMountBelowScreenOnly {mount:mount,screen:limit,unresolved:"Not a service rating: verify creep under actual stress, print orientation, time, local hot spots, fan limits and fan-failure shutdown."}} +} +fn installed_thermal_without_measurement() -> InstalledThermalReading {InstalledThermalUnresolved {required:"Measure installed airflow or pressure curve, inlet and fan-inlet temperatures, hottest mount/pillar temperature and fan-failure transient. No catalog PWM percentage admits operation."}} + +type FanInletReading = FanInletWithinCatalogAmbient | FanInletExceedsCatalogAmbient +fn fan_inlet_screen(measured:Celsius) -> FanInletReading { + if celsius_count(c:measured)>celsius_count(c:arctic_p8_operating_ambient_max) {FanInletExceedsCatalogAmbient} else {FanInletWithinCatalogAmbient} +} diff --git a/dag/gunbc/product/printed_chassis/fan_mount.dag b/dag/gunbc/product/printed_chassis/fan_mount.dag index 0eb4c4db37a..f3d8831638e 100644 --- a/dag/gunbc/product/printed_chassis/fan_mount.dag +++ b/dag/gunbc/product/printed_chassis/fan_mount.dag @@ -122,8 +122,8 @@ fn admit_fan_mount( } // THE FIRST PART TO PRINT. A 92 mm plate carries the 71.5 mm pattern with material to spare, 4 mm of -// thickness is stiff enough to hold a fan without warping on a textured plate, and a 76 mm bore clears the -// blades while staying well inside the 50.6 mm screw-hole circle. +// thickness is an unqualified prototype choice, and the 76 mm aperture stays inside the +// screw-hole circle. Held-fan fit, stiffness, cassette attachment and guarding remain to be checked. data arctic_p8_mount_plate_edge: Micrometer = micrometer(count: 92000) data arctic_p8_mount_plate_thickness: Micrometer = micrometer(count: 4000) data arctic_p8_mount_bore: Micrometer = micrometer(count: 76000) diff --git a/dag/gunbc/product/printed_chassis/fan_mount_print.dag b/dag/gunbc/product/printed_chassis/fan_mount_print.dag new file mode 100644 index 00000000000..a41217913ae --- /dev/null +++ b/dag/gunbc/product/printed_chassis/fan_mount_print.dag @@ -0,0 +1,33 @@ +module product.printed_chassis.fan_mount_print + +import std.types { String } +import std.process { ExitSuccess, exit_failure } +import gunbc.cli_wire { CliWireResponse, CliWirePrintable } +import product.printed_chassis.fan_mount { + arctic_p8_fan_mount, FanMountAdmitted, FanMountBoreReachesMountingHoles, FanMountBoreNotEnclosedByPlate, FanMountPlateSmallerThanPattern, +} +import product.printed_chassis.cadquery_realization { + plate_part_from_fan_mount, realize_plate_part, CadQueryEmitted, CadQueryDimensionUnrenderable, cadquery_program_source, +} +import product.printed_chassis.export_profile { exercised_stl_knobs } +import extdeps.cadquery.cadquery { stl_export_call } + +// First rear fan carrier part: the existing admitted 92 x 92 x 4 mm plate. +// Geometry and hole positions remain owned by fan_mount; no duplicate coordinates here. +// This emits an unpowered assembly-fit prototype, not qualified structural/powered hardware. +// Cassette attachment and a finger guard are separate, unresolved interfaces. +fn main() -> CliWireResponse { + match arctic_p8_fan_mount { + FanMountAdmitted { geometry: g } => match realize_plate_part(p: plate_part_from_fan_mount(g: g)) { + CadQueryEmitted { program: p } => CliWirePrintable { + bytes: concat(cadquery_program_source(p: p) as String, + stl_export_call(destination: "rear-fan-mount.stl", knobs: exercised_stl_knobs)), + exit: ExitSuccess, + }, + CadQueryDimensionUnrenderable {micrometres:_} => CliWirePrintable { bytes: "", exit: exit_failure(reason: "Fan mount CAD emission refused") }, + }, + FanMountBoreReachesMountingHoles {bore_radius:_,hole_center_distance:_} => CliWirePrintable { bytes: "", exit: exit_failure(reason: "Fan mount geometry refused") }, + FanMountBoreNotEnclosedByPlate {bore_diameter:_,plate_edge:_} => CliWirePrintable { bytes: "", exit: exit_failure(reason: "Fan mount geometry refused") }, + FanMountPlateSmallerThanPattern {plate_edge:_,required_edge:_} => CliWirePrintable { bytes: "", exit: exit_failure(reason: "Fan mount geometry refused") }, + } +} diff --git a/dag/gunbc/product/printed_chassis/fit_gauges.dag b/dag/gunbc/product/printed_chassis/fit_gauges.dag new file mode 100644 index 00000000000..bfe93241e47 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/fit_gauges.dag @@ -0,0 +1,99 @@ +module product.printed_chassis.fit_gauges + +import std.types { String, NonEmptyStr, List } +import extdeps.standards.micro_atx { + MountingLocationId, LocB, LocC, LocH, LocL, LocM, LocR, micro_atx_mounting_locations, +} +import extdeps.printing.bambu_lab_a1_mini { a1_mini_build_envelope } +import product.printed_chassis.standoff_gauge { + StandoffGaugeAdmission, GaugeAdmitted, GaugeLocationNotEnumerated, GaugeLocationDuplicated, + GaugeLocationsCoincide, GaugeExceedsBuildEnvelope, admit_standoff_gauge, + gauge_edge_margin, gauge_thickness, +} +import product.printed_chassis.cadquery_realization { + CadQueryEmitted, CadQueryDimensionUnrenderable, realize_plate_part, + plate_part_from_standoff_gauge, cadquery_program_source, +} +import product.printed_chassis.export_profile { exercised_stl_knobs } +import extdeps.cadquery.cadquery { stl_export_call } +import gunbc.cli_wire { CliWireResponse, CliWirePrintable } +import std.process { ExitSuccess, exit_failure } + +// Follow-on experiments to the existing F-C gauge. These five axis-aligned pairs connect +// the other five inferred board locations to that pair. J is explicitly excluded by the +// board authority and S is unobserved; neither earns a support by appearing in a standard. +// This roster selects experiments, not confirmed holes or structural support positions. +// The connected graph does not determine all relative angles: inspect against board edges +// and confirm the whole map in the adjustable fixture before freezing it. +// Each part uses the existing gauge admission, dimensions, build envelope and emitter. +type FitGaugePair { name: NonEmptyStr, a: MountingLocationId, b: MountingLocationId } + +data follow_on_fit_gauge_pairs: List = [ + FitGaugePair { name: "C-B", a: LocC, b: LocB }, + FitGaugePair { name: "C-H", a: LocC, b: LocH }, + FitGaugePair { name: "H-L", a: LocH, b: LocL }, + FitGaugePair { name: "L-M", a: LocL, b: LocM }, + FitGaugePair { name: "H-R", a: LocH, b: LocR }, +] + +fn fit_gauge_admission(pair: FitGaugePair) -> StandoffGaugeAdmission { + admit_standoff_gauge(rows: micro_atx_mounting_locations, envelope: a1_mini_build_envelope, + a: pair.a, b: pair.b, margin: gauge_edge_margin, thickness: gauge_thickness) +} + +// This generates a local experiment, not an absolute process-qualification receipt. +// The measured export knobs are reused without asserting that the unsettled profile is admitted. +// A refused experiment carries no executable source. +type FitGaugeSource = FitGaugeSourceReady { source: String } | FitGaugeSourceRefused { cause: NonEmptyStr } + +fn fit_gauge_source(pair: String) -> FitGaugeSource { + let matches = follow_on_fit_gauge_pairs |> filter(p => (p.name as String) == pair) + if (matches |> length) != 1 { + FitGaugeSourceRefused { cause: "expected exactly one declared fit gauge pair" } + } else { + match matches |> first { + Absent => FitGaugeSourceRefused { cause: "fit gauge pair absent" } + Present { value: p } => match fit_gauge_admission(pair: p) { + GaugeAdmitted { geometry: g } => match realize_plate_part(p: plate_part_from_standoff_gauge(g: g)) { + CadQueryEmitted { program: program } => FitGaugeSourceReady { source: concat( + cadquery_program_source(p: program) as String, + stl_export_call(destination: concat(p.name as String, ".stl"), knobs: exercised_stl_knobs)) }, + CadQueryDimensionUnrenderable { micrometres: _ } => FitGaugeSourceRefused { cause: "unrenderable gauge dimension" }, + }, + GaugeLocationNotEnumerated { requested: _ } => FitGaugeSourceRefused { cause: "gauge location absent" }, + GaugeLocationDuplicated { requested: _, count: _ } => FitGaugeSourceRefused { cause: "gauge location duplicated" }, + GaugeLocationsCoincide { requested: _ } => FitGaugeSourceRefused { cause: "gauge locations coincide" }, + GaugeExceedsBuildEnvelope { fit: _ } => FitGaugeSourceRefused { cause: "gauge exceeds build envelope" }, + }, + } + } +} + +// Machine-readable generated source, without terminal styling or any printer actuation. +fn main(pair: String) -> CliWireResponse { + fit_gauge_source_response(result: fit_gauge_source(pair: pair)) +} + +fn fit_gauge_source_response(result: FitGaugeSource) -> CliWireResponse { + match result { + FitGaugeSourceReady { source: s } => CliWirePrintable { bytes: s, exit: ExitSuccess }, + FitGaugeSourceRefused { cause: c } => CliWirePrintable { bytes: "", exit: exit_failure(reason: c) }, + } +} + +// One compiler load prepares the complete experiment set. Any refusal withholds the whole +// script, so a partial file cannot be mistaken for all five prepared gauges. +fn batch() -> CliWireResponse { + let source = fold(follow_on_fit_gauge_pairs, + init: FitGaugeSourceReady { source: "# Generated follow-on Altra fit gauges; experimental spacing checks.\n" }, + f: fn(acc, pair) { + match acc { + FitGaugeSourceRefused { cause: c } => FitGaugeSourceRefused { cause: c }, + FitGaugeSourceReady { source: previous } => match fit_gauge_source(pair: pair.name as String) { + FitGaugeSourceReady { source: next } => FitGaugeSourceReady { source: concat(previous, next) }, + FitGaugeSourceRefused { cause: c } => FitGaugeSourceRefused { cause: c }, + }, + } + }) + fit_gauge_source_response(result: source) +} diff --git a/dag/gunbc/product/printed_chassis/fit_observations.dag b/dag/gunbc/product/printed_chassis/fit_observations.dag new file mode 100644 index 00000000000..0afb87f8166 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/fit_observations.dag @@ -0,0 +1,16 @@ +module product.printed_chassis.fit_observations +import std.types {String,Bool} +import std.claim_evidence {RecordedFact,EvidenceProvenance} + +type FanFitProposition { original_plate_accepts_fan: Bool } +data rear_fan_fit: RecordedFact = RecordedFact { + id:"rear-fan-plate-fit-2026-10-04",value:FanFitProposition {original_plate_accepts_fan:true}, +} +data rear_fan_fit_provenance: EvidenceProvenance = EvidenceProvenance { + entity:"operator-conversation/rear-fan-fit-2026-10-04",activity:"unpowered-fan-plate-fit",agent:"operator", +} +// This qualitative observation establishes the original plate/fan pairing only. It +// supplies no measured length, thread, load, guard rating or new-frame fit evidence. +fn fan_fit_display() -> String { + if rear_fan_fit.value.original_plate_accepts_fan {"Operator reports the original printed rear fan plate fits (4 October 2026). The revised frame/guard stack remains proposed; no further gauge sequence."} else {"Original plate fit not established."} +} diff --git a/dag/gunbc/product/printed_chassis/harness_census.dag b/dag/gunbc/product/printed_chassis/harness_census.dag new file mode 100644 index 00000000000..cd1e176034a --- /dev/null +++ b/dag/gunbc/product/printed_chassis/harness_census.dag @@ -0,0 +1,147 @@ +module product.printed_chassis.harness_census + +import std.types { Bool, Int, List, NonEmptyStr } +import std.nat { Nat, nat_sub, NatSubtraction, NatDifference, NatSubtrahendExceedsMinuend } +import std.measure { Millimeter, millimeter, millimeter_count } +import product.printed_chassis.stack_geometry { stack_pitch, footprint_half_depth } +import product.printed_chassis.cassette { tray_d, tray_w, rear_plate_z } +import product.printed_chassis.power_cassette { PowerCassette, PowerCassettePlacement, TopOfColumn, BottomOfColumn, BesideColumn, units_lifted_to_pop, nodes_above } +import extdeps.fans.arctic { arctic_p8_cable_length, arctic_p8_pwm_sharing, PwmSharingChain, PstPassThrough, NoPassThrough } +import extdeps.boards.asrock_rack { asrock_altrad8ud_connector_edge, BoardEdge, RearIoEdge, FrontEdge, SideEdgeNearDimmBankA, SideEdgeNearPcieSlots } + +// HOW MUCH CABLE A STACK ACTUALLY CARRIES, counted and measured from the geometry rather than +// guessed. The operator's suspicion was that the naive picture undercounts; this is the +// instrument that answers it, and the things it finds are not in the naive picture: +// +// - the board's 12 V inputs and fan headers are on the FRONT edge (the manual's layout), so a +// feed arriving at the rear must cross the tray, and the rear fan bank's header cable must +// cross it the other way; +// - a fan bank on PST is one header cable plus short links, not three header cables; +// - a branch's length is a function of how many levels separate the node from the station, +// so placement decides the total. +// +// Lengths are lower-bound route estimates plus declared slack, NOT cut lengths. Connector +// bodies, side escape around the rear fan bank, bends and strain relief still require routing. + +type CableRole + = DcBranch + | Atx12vPigtail + | FanHeaderCable + | FanPstLink + | EthernetPatch + +type CableRun { + node_index_from_bottom: Nat + role: CableRole + count: Nat + each: Millimeter +} + +// WHERE THE NODE'S DC INLET SITS. The board wants power at the front; the pillar raceway is at +// the rear corners. Inlet at the rear means a long internal pigtail; inlet at the front means +// the branch drop runs down the front pillars. A selection, exposed as the two arms it has. +type DcInletPlacement = InletAtRear | InletAtFront + +type HarnessPlan { + node_count: Nat + station: PowerCassette + inlet: DcInletPlacement + inputs_populated: Nat + fans_per_node: Nat + ethernet_per_node: Nat + switch_level_from_bottom: Nat + service_allowance: Millimeter +} + +fn pitch_mm() -> Nat { stack_pitch() / 1000 } + +data no_levels: Nat = 0 +fn nat_minus(a: Nat, b: Nat) -> Nat { match nat_sub(a: a, b: b) { NatDifference { value: v } => v NatSubtrahendExceedsMinuend => no_levels } } +fn level_distance(a: Nat, b: Nat) -> Nat { if a > b { nat_minus(a: a, b: b) } else { nat_minus(a: b, b: a) } } + +// The station's level for a vertical run: above the top node, below the bottom one, or beside +// the column at the bottom node's level plus a lateral tray width. +fn station_vertical_levels(plan: HarnessPlan, node: Nat) -> Nat { + match plan.station.placement { + TopOfColumn => nodes_above(node_count: plan.node_count, node_index_from_bottom: node) + 1 + BottomOfColumn => node + 1 + BesideColumn => node + 1 + } +} +fn station_lateral_mm(plan: HarnessPlan) -> Nat { + match plan.station.placement { TopOfColumn => 0 BottomOfColumn => 0 BesideColumn => tray_w / 1000 } +} + +// One node's branch: vertical levels times pitch, a lateral run if beside, the tray crossing if +// the inlet is at the front and the raceway at the rear, plus the allowance. +fn dc_branch_length(plan: HarnessPlan, node: Nat) -> Millimeter { + let vertical = station_vertical_levels(plan: plan, node: node) * pitch_mm() + let crossing = match plan.inlet { InletAtRear => 0 InletAtFront => tray_d / 1000 } + millimeter(count: vertical + station_lateral_mm(plan: plan) + crossing + millimeter_count(m: plan.service_allowance)) +} + +// Inside the cassette: from the inlet to the front-edge header. Rear inlet crosses the tray. +fn pigtail_length(plan: HarnessPlan) -> Millimeter { + let crossing = match plan.inlet { InletAtRear => tray_d / 1000 InletAtFront => 0 } + millimeter(count: crossing + millimeter_count(m: plan.service_allowance)) +} + +// The fan bank: fans at the rear, header at the front, so one header cable crosses the tray. +// The fan's own 400 mm lead covers it if the crossing fits; otherwise an extension is a second +// cable and the census says so by exceeding the lead. +fn fan_header_run(plan: HarnessPlan) -> Millimeter { + millimeter(count: (tray_d / 2 - rear_plate_z) / 1000 + millimeter_count(m: plan.service_allowance)) +} +fn fan_header_needs_extension(plan: HarnessPlan) -> Bool { + millimeter_count(m: fan_header_run(plan: plan)) > millimeter_count(m: arctic_p8_cable_length) +} + +fn fan_cables(plan: HarnessPlan, node: Nat) -> List { + match arctic_p8_pwm_sharing { + PstPassThrough { max_chained_standing: _ } => [ + CableRun { node_index_from_bottom: node, role: FanHeaderCable, count: 1, each: fan_header_run(plan: plan) }, + CableRun { node_index_from_bottom: node, role: FanPstLink, count: nat_minus(a: plan.fans_per_node, b: 1), each: millimeter(count: 100) }, + ] + NoPassThrough => [CableRun { node_index_from_bottom: node, role: FanHeaderCable, count: plan.fans_per_node, each: fan_header_run(plan: plan) }] + } +} + +fn ethernet_length(plan: HarnessPlan, node: Nat) -> Millimeter { + millimeter(count: level_distance(a: plan.switch_level_from_bottom, b: node) * pitch_mm() + millimeter_count(m: plan.service_allowance)) +} + +fn node_cables(plan: HarnessPlan, node: Nat) -> List { + concat([ + CableRun { node_index_from_bottom: node, role: DcBranch, count: 1, each: dc_branch_length(plan: plan, node: node) }, + CableRun { node_index_from_bottom: node, role: Atx12vPigtail, count: plan.inputs_populated, each: pigtail_length(plan: plan) }, + CableRun { node_index_from_bottom: node, role: EthernetPatch, count: plan.ethernet_per_node, each: ethernet_length(plan: plan, node: node) }, + ], fan_cables(plan: plan, node: node)) +} + +fn node_indices(n: Nat) -> List { nat_range_below(n: n) } +fn nat_range_below(n: Nat) -> List { range_acc(i: 0, n: n, acc: []) } +fn range_acc(i: Nat, n: Nat, acc: List) -> List { if i >= n { reverse(acc) } else { range_acc(i: i + 1, n: n, acc: concat([i], acc)) } } + +type HarnessCensus { + runs: List + cable_count: Nat + total_length: Millimeter + fan_header_extension_needed: Bool +} + +fn harness_census(plan: HarnessPlan) -> HarnessCensus { + let runs = flat_map(node_indices(n: plan.node_count), node => node_cables(plan: plan, node: node)) + HarnessCensus { + runs: runs, + cable_count: fold(runs, init: 0, f: (acc, r) => acc + r.count), + total_length: millimeter(count: fold(runs, init: 0, f: (acc, r) => acc + r.count * millimeter_count(m: r.each))), + fan_header_extension_needed: fan_header_needs_extension(plan: plan), + } +} + +// THE SERVICE COST OF A POP, in disconnects: every unit lifted has its cables unplugged first, +// and the target itself. A stack of eight with the station on top pays nine unplug sets to +// reach the bottom node; two columns of four pay five. +fn disconnect_sets_to_pop(plan: HarnessPlan, node: Nat) -> Nat { + units_lifted_to_pop(station: plan.station, node_count: plan.node_count, node_index_from_bottom: node) + 1 +} diff --git a/dag/gunbc/product/printed_chassis/node_power_feed.dag b/dag/gunbc/product/printed_chassis/node_power_feed.dag new file mode 100644 index 00000000000..6cc57d6c31c --- /dev/null +++ b/dag/gunbc/product/printed_chassis/node_power_feed.dag @@ -0,0 +1,147 @@ +module product.printed_chassis.node_power_feed + +import std.types { Bool, Int, List, NonEmptyStr } +import std.nat { Nat, nat_sub, NatSubtraction, NatDifference, NatSubtrahendExceedsMinuend, nat_ceil_div } +import std.measure { + Watt, watt_count, Volt, volt_count, Millivolt, millivolt, millivolt_count, + Ampere, ampere_count, Milliampere, milliampere, milliampere_count, + Millimeter, millimeter_count, MicroohmPerMetre, microohm_per_metre_count, Percent, percent, percent_count, +} +import v2.std.optional { Present, Absent } +import extdeps.boards.types { PowerInputPopulationStanding, PowerInputPopulationSpecified, PowerInputMinimumOneStated, PowerInputPopulationUnresolved } +import extdeps.boards.asrock_rack { + asrock_altrad8ud_required_input_population, asrock_altrad8ud_atx12v_twelve_volt_pins, asrock_altrad8ud_atx12v_nominal_input, + asrock_altrad8ud_system_fan_header_count, +} +import extdeps.connector.molex_mini_fit_jr { MiniFitJrTerminalRating } +import extdeps.standards.awg { awg_copper_resistance } +import extdeps.standards.atx12v_psdg { DcOutputRegulationBand } +import extdeps.fans.arctic { arctic_p8_rated_current, arctic_p8_pwm_sharing, PwmSharingChain, PstPassThrough, NoPassThrough } + +// Node-feed screening under declared load, connector and voltage-band assumptions. +// This does not establish ASRock's high-load input population or direct-DC voltage +// tolerance. Branch-only drop omits contacts, protection, hot wiring and pigtails. +// The result is a sensitivity screen or a named refusal, never electrical approval. +// +// THE LOAD IS SUPPLIED, NOT INVENTED HERE. product.node_power_envelope derives a node's DC load and +// refuses while the DIMM population is unread; a caller passes that derived figure, or passes a +// declared bound and says so. This module never reads a TDP and calls it a load. + +// Declared, not cited: the fraction of the connector's rated current the design RETAINS. A +// connector rating is a 30 C rise figure at the vendor's test condition; a chassis with warm +// exhaust across its front edge wants headroom, and the amount is policy. Carried as the retained +// fraction (70 means seventy percent of the rating is used) so no subtraction is needed. +type DeclaredConnectorDerating sole_constructor { retained: Percent } +fn declared_connector_derating(retained: Percent) -> DeclaredConnectorDerating { DeclaredConnectorDerating { retained: retained } } +data zero_ma: Nat = 0 +data one_input: Nat = 1 + +type NodeFeedInputs { + node_dc_load: Watt + connector_rating: MiniFitJrTerminalRating + derating: DeclaredConnectorDerating + branch_gauge_awg: Nat + branch_length: Millimeter + station_setpoint: Millivolt + regulation: DcOutputRegulationBand +} + +type FeedRefusalCause + = LoadExceedsBoardInputs { required_inputs: Nat, available_inputs: Nat, per_input_limit: Milliampere } + | PopulationRuleUnresolved + | BranchGaugeUnknown { gauge: Nat } + | ConnectorVoltageBelowBand { at_connector: Millivolt, band_minimum: Millivolt, drop: Millivolt } + | SetpointAboveBand { setpoint: Millivolt, band_maximum: Millivolt } + +type NodeFeed { + node_current: Milliampere + inputs_populated: Nat + per_input_limit: Milliampere + branch_round_trip_drop: Millivolt + at_connector: Millivolt +} + +type NodeFeedReading + = NodeFeedScreeningOnly { feed: NodeFeed } + | NodeFeedRefused { cause: FeedRefusalCause } + +// I = P / V at the nominal input, in milliamperes: W * 1e6 / mV. +fn node_current_ma(load: Watt, nominal: Volt) -> Milliampere { + milliampere(count: watt_count(w: load) * 1000000 / (volt_count(v: nominal) * 1000)) +} + +// What one 8-pin input may carry: four 12 V contacts at the rated current, derated as declared. +fn per_input_limit_ma(rating: MiniFitJrTerminalRating, derating: DeclaredConnectorDerating) -> Milliampere { + let contacts = count(asrock_altrad8ud_atx12v_twelve_volt_pins) + milliampere(count: contacts * ampere_count(a: rating.rated_current_per_circuit) * 1000 * percent_count(p: derating.retained) / 100) +} + +fn ceil_div(a: Nat, b: Nat) -> Nat { match nat_ceil_div(a: a, b: b) { Present { value: v } => v Absent => b } } + +// The population: at least the manual's floor, at most what the board offers, otherwise refuse. +fn inputs_required(current: Milliampere, limit: Milliampere, standing: PowerInputPopulationStanding) -> NodeFeedReading? { + let needed = ceil_div(a: milliampere_count(i: current), b: milliampere_count(i: limit)) + match standing { + PowerInputPopulationSpecified { required_inputs: r } => if needed > r { Present { value: NodeFeedRefused { cause: LoadExceedsBoardInputs { required_inputs: needed, available_inputs: r, per_input_limit: limit } } } } else { none } + PowerInputMinimumOneStated { available_inputs: a, stated_condition: _ } => if needed > a { Present { value: NodeFeedRefused { cause: LoadExceedsBoardInputs { required_inputs: needed, available_inputs: a, per_input_limit: limit } } } } else { none } + PowerInputPopulationUnresolved { available_inputs: _ } => Present { value: NodeFeedRefused { cause: PopulationRuleUnresolved } } + } +} + +fn populated_count(current: Milliampere, limit: Milliampere) -> Nat { + let needed = ceil_div(a: milliampere_count(i: current), b: milliampere_count(i: limit)) + if needed < one_input { one_input } else { needed } +} + +// Round-trip branch drop in millivolts: two conductors of the branch length at the gauge's +// resistance per metre, times the node current. uohm/m * mm / 1000 = uohm; uohm * mA / 1e6 = mV. +fn branch_drop_mv(resistance: MicroohmPerMetre, length: Millimeter, current: Milliampere) -> Millivolt { + let round_trip_uohm = 2 * microohm_per_metre_count(r: resistance) * millimeter_count(m: length) / 1000 + millivolt(count: (round_trip_uohm * milliampere_count(i: current)+999999) / 1000000) +} + +// Constant-power current bounded at the selected minimum voltage, rounded up. +// NodeFeedScreeningOnly omits connector/pigtail/protection resistances, hot-wire resistance, +// crimp quality, installed board header identity, ampacity, unequal current sharing and transients. +// The manual does not authorize deriving high-load connector population solely from family ampacity. +fn node_feed(inputs: NodeFeedInputs) -> NodeFeedReading { + if percent_count(p:inputs.derating.retained)==0 || percent_count(p:inputs.derating.retained)>100 || ampere_count(a:inputs.connector_rating.rated_current_per_circuit)==0 {return NodeFeedRefused {cause:PopulationRuleUnresolved}} + let floor_mv=millivolt_count(v:inputs.regulation.minimum) + if floor_mv==0 {return NodeFeedRefused {cause:PopulationRuleUnresolved}} + let current=milliampere(count:(watt_count(w:inputs.node_dc_load)*1000000+floor_mv-1)/floor_mv) + let limit = per_input_limit_ma(rating: inputs.connector_rating, derating: inputs.derating) + match inputs_required(current: current, limit: limit, standing: asrock_altrad8ud_required_input_population) { + Present { value: refused } => refused + Absent => match awg_copper_resistance(gauge: inputs.branch_gauge_awg) { + Absent => NodeFeedRefused { cause: BranchGaugeUnknown { gauge: inputs.branch_gauge_awg } } + Present { value: resistance } => { + if millivolt_count(v: inputs.station_setpoint) > millivolt_count(v: inputs.regulation.maximum) { + NodeFeedRefused { cause: SetpointAboveBand { setpoint: inputs.station_setpoint, band_maximum: inputs.regulation.maximum } } + } else { + let drop = branch_drop_mv(resistance: resistance, length: inputs.branch_length, current: current) + let at_connector = match nat_sub(a: millivolt_count(v: inputs.station_setpoint), b: millivolt_count(v: drop)) { NatDifference { value: v } => millivolt(count: v) NatSubtrahendExceedsMinuend => millivolt(count: zero_ma) } + if millivolt_count(v: at_connector) < millivolt_count(v: inputs.regulation.minimum) { + NodeFeedRefused { cause: ConnectorVoltageBelowBand { at_connector: at_connector, band_minimum: inputs.regulation.minimum, drop: drop } } + } else { + NodeFeedScreeningOnly { feed: NodeFeed { node_current: current, inputs_populated: populated_count(current: current, limit: limit), per_input_limit: limit, branch_round_trip_drop: drop, at_connector: at_connector } } + } + } + } + } + } +} + +// THE FAN BANK ON ONE HEADER. The rear bank chains fan-to-fan through PST and reaches the board on +// one header, so that header carries the whole bank's current. The board states no header limit +// (asrock_altrad8ud_fan_header_current_limit_standing), so the verdict is the load plus the +// unread limit, never a pass. +type FanHeaderLoad + = FanHeaderLoadAgainstUnreadLimit { header_current: Milliampere, fans: Nat } + | FanBankNeedsOneHeaderPerFan { fans: Nat, headers_available: Nat } + +fn fan_bank_header_load(fans: Nat) -> FanHeaderLoad { + match arctic_p8_pwm_sharing { + PstPassThrough { max_chained_standing: _ } => FanHeaderLoadAgainstUnreadLimit { header_current: milliampere(count: fans * milliampere_count(i: arctic_p8_rated_current)), fans: fans } + NoPassThrough => FanBankNeedsOneHeaderPerFan { fans: fans, headers_available: asrock_altrad8ud_system_fan_header_count } + } +} diff --git a/dag/gunbc/product/printed_chassis/placed_mass_model.dag b/dag/gunbc/product/printed_chassis/placed_mass_model.dag new file mode 100644 index 00000000000..f03d1cf1c3c --- /dev/null +++ b/dag/gunbc/product/printed_chassis/placed_mass_model.dag @@ -0,0 +1,68 @@ +module product.printed_chassis.placed_mass_model +import std.types {Bool,Int,List,NonEmptyStr,String} +import std.measure {Gram,gram,gram_count} +import product.printed_chassis.component_mass {MassLedger,MassIncomplete,MassComplete,lower_mass,upper_mass} +import std.affine_space {Point3,signed_frame_component_count} + +// Rigid-body statics for a DECLARED mass/position scenario, not FEA or admission. +// At each interface pass the components above it, translated into that interface's +// frame. A bottom power cassette shifts every node's y; its own mass stays at base. +// Units: mass g, position um, moment g.um; gravity force microN = g * 9806.65. +type PlacedMass {id:String,mass:Gram,at:Point3} +type SupportRectangle {half_x_um:Int,half_z_um:Int} +type LoadScenario {components:List,support:SupportRectangle,push_height_um:Int} +type LoadScreen + = InvalidLoadScenario {reason:NonEmptyStr} + | RigidBodyScreen {mass_g:Int,cg_x_um:Int,cg_y_um:Int,cg_z_um:Int, + restoring_x_positive_mn_mm:Int,restoring_x_negative_mn_mm:Int, + restoring_z_positive_mn_mm:Int,restoring_z_negative_mn_mm:Int, + tip_push_mn:Int,tip_base_acceleration_um_s2:Int, + corner_reactions_mn:List,boundary:NonEmptyStr} + +fn minimum(a:Int,b:Int) -> Int {if a Int {if n<0 {0-n} else {n}} +// Four equal vertical springs under a rigid platform: reaction W/4*(1 +/- x/a +/- z/b). +// This differs from the bilinear load split. Negative reactions require a contact +// re-solve (one foot lifts); they must not be clamped to zero or called a pass. +// Compute milligram reactions first to avoid overflowing g.um^2 products; <0.01 mN quantization. +fn placed_load_screen(case:LoadScenario) -> LoadScreen { + let m=fold(case.components,init:0,f:(n,c)=>n+gram_count(g:c.mass)) + let sx=fold(case.components,init:0,f:(n,c)=>n+gram_count(g:c.mass)*signed_frame_component_count(c:c.at.x)) + let sy=fold(case.components,init:0,f:(n,c)=>n+gram_count(g:c.mass)*signed_frame_component_count(c:c.at.y)) + let sz=fold(case.components,init:0,f:(n,c)=>n+gram_count(g:c.mass)*signed_frame_component_count(c:c.at.z)) + let a=case.support.half_x_um + let b=case.support.half_z_um + if m<=0 || a<=0 || b<=0 || sy<=0 || case.push_height_um<=0 {return InvalidLoadScenario {reason:"Positive total mass, support spans, CG height and push height required"}} + if any(case.components,c=>count(filter(case.components,q=>q.id==c.id))!=1) {return InvalidLoadScenario {reason:"Duplicate component identity in placed load scenario"}} + if any(case.components,c=>signed_frame_component_count(c:c.at.y)<0) {return InvalidLoadScenario {reason:"Interface load set includes a mass below that interface"}} + let rxp=m*a-sx + let rxn=m*a+sx + let rzp=m*b-sz + let rzn=m*b+sz + let least=minimum(a:minimum(a:rxp,b:rxn),b:minimum(a:rzp,b:rzn)) + let corners=flat_map([-1,1],x=>map([-1,1],z=>(m*1000+x*sx*1000/a+z*sz*1000/b)*980665/400000000)) + RigidBodyScreen { + mass_g:m,cg_x_um:sx/m,cg_y_um:sy/m,cg_z_um:sz/m, + restoring_x_positive_mn_mm:rxp*980665/100000000, + restoring_x_negative_mn_mm:rxn*980665/100000000, + restoring_z_positive_mn_mm:rzp*980665/100000000, + restoring_z_negative_mn_mm:rzn*980665/100000000, + tip_push_mn:least*980665/(case.push_height_um*100000), + tip_base_acceleration_um_s2:least*9806650/sy, + corner_reactions_mn:corners, + boundary:"Scenario only: four equal-stiffness contacts assumed. Negative reaction requires unilateral contact solve. Sliding, cable pulls, joint bending/shear, buckling, print anisotropy and hot creep remain unqualified. No safe stack height is certified.", + } +} + +// Endpoint sweeps are explicitly declared scenarios, not an interval proof: the +// adverse combination for tipping need not put every component at its upper mass. +type MassEndpoint = LowerMassEndpoint | UpperMassEndpoint +fn load_from_ledger(ledger:MassLedger,endpoint:MassEndpoint,support:SupportRectangle,push_height_um:Int) -> LoadScreen { + match ledger { + MassIncomplete {issues:_}=>InvalidLoadScenario {reason:"Complete component mass evidence is required before constructing a load scenario"} + MassComplete {total:_,rows:rows}=>placed_load_screen(case:LoadScenario { + components:map(rows,r=>PlacedMass {id:r.id,mass:gram(count:r.quantity*gram_count(g:match endpoint {LowerMassEndpoint=>lower_mass(m:r.mass) UpperMassEndpoint=>upper_mass(m:r.mass)})),at:r.at}), + support:support,push_height_um:push_height_um, + }) + } +} diff --git a/dag/gunbc/product/printed_chassis/platform_projects_review.dag b/dag/gunbc/product/printed_chassis/platform_projects_review.dag new file mode 100644 index 00000000000..0b50a790c48 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/platform_projects_review.dag @@ -0,0 +1,12 @@ +module product.printed_chassis.platform_projects_review +import std.types {List,String} +import product.printed_chassis.board_mechanical_reference {mounting_reference_json,cited_figure_json} +import std.measure {micrometer_count} +import v2.std.optional {Present,Absent} +import product.printed_chassis.cassette_profile {CassetteProfile,BoardOutlineKnown,BoardOutlinePending,RetainPlatformPowerAssembly,IndependentPowerCassette,board_grid_tiles} +import product.printed_chassis.cassette_profiles {cassette_profiles} +import product.printed_chassis.cassette_layout {proposed_panel_print_policy,partition_profile,TrayPartitionKnown,TrayPartitionInvalid,TrayPartitionNeedsOutline,layout_for,LayoutKnown,LayoutNeedsBoardOutline,InvalidEdgeMargin} +import extdeps.languages.json.emit {JsonValue,json_object,json_kv,json_array,json_string,json_int,json_null} +fn platform_projects_review() -> JsonValue { + json_array(elements:map(cassette_profiles,p=>json_object(members:[json_kv(key:"id",value:json_string(s:p.id as String)),json_kv(key:"hardware",value:json_string(s:p.requested_hardware as String)),json_kv(key:"board_outline_um",value:match p.outline {BoardOutlinePending=>json_null() BoardOutlineKnown {width:w,depth:d,standing:_}=>json_array(elements:[json_int(n:micrometer_count(m:w)),json_int(n:micrometer_count(m:d))])}),json_kv(key:"outline_evidence",value:match p.outline {BoardOutlinePending=>json_null() BoardOutlineKnown {width:_,depth:_,standing:s}=>cited_figure_json(standing:s)}),json_kv(key:"mounting_reference",value:mounting_reference_json(reference:p.mounting)),json_kv(key:"power_intent",value:json_string(s:match p.power {RetainPlatformPowerAssembly=>"retain vendor power assembly with node; compatibility unresolved" IndependentPowerCassette=>"separate power cassette"})),json_kv(key:"board_only_180mm_grid_tiles",value:match board_grid_tiles(project:p,usable_tile_um:180000) {Absent=>json_null() Present {value:n}=>json_int(n:n)}),json_kv(key:"shared_tray_outline_um",value:match layout_for(profile:p,edge_margin_um:14000) {LayoutKnown {layout:l}=>json_array(elements:[json_int(n:l.tray_width),json_int(n:l.tray_depth)]) LayoutNeedsBoardOutline=>json_null() InvalidEdgeMargin=>json_null()}),json_kv(key:"shared_partition",value:match partition_profile(profile:p,edge_margin_um:14000,policy:proposed_panel_print_policy) {TrayPartitionKnown {partition:g}=>json_object(members:[json_kv(key:"columns",value:json_int(n:g.columns)),json_kv(key:"rows",value:json_int(n:g.rows)),json_kv(key:"cells",value:json_int(n:count(g.cells))),json_kv(key:"joints",value:json_int(n:count(g.edges))),json_kv(key:"brim_each_side_um",value:json_int(n:g.policy.along_width.brim_each_side_um)),json_kv(key:"edge_clearance_each_side_um",value:json_int(n:g.policy.along_width.edge_clearance_each_side_um)),json_kv(key:"cell_envelopes",value:json_array(elements:map(g.cells,c=>json_object(members:[json_kv(key:"column",value:json_int(n:c.column)),json_kv(key:"row",value:json_int(n:c.row)),json_kv(key:"panel_width_um",value:json_int(n:c.x1_um-c.x0_um)),json_kv(key:"panel_depth_um",value:json_int(n:c.z1_um-c.z0_um)),json_kv(key:"print_width_with_process_um",value:json_int(n:c.print_width_um)),json_kv(key:"print_depth_with_process_um",value:json_int(n:c.print_depth_um))])))),json_kv(key:"standing",value:json_string(s:"minimum axis grid under declared feature envelopes and proposed slicing allowances; solid realization and mounting map still required"))]) TrayPartitionInvalid=>json_null() TrayPartitionNeedsOutline=>json_null()}),json_kv(key:"open_design_obligations",value:json_int(n:count(p.open))),json_kv(key:"standing",value:json_string(s:"project and interface model; no fabrication-ready tray or electrical redesign"))]))) +} diff --git a/dag/gunbc/product/printed_chassis/power_cassette.dag b/dag/gunbc/product/printed_chassis/power_cassette.dag new file mode 100644 index 00000000000..01c6dde75ed --- /dev/null +++ b/dag/gunbc/product/printed_chassis/power_cassette.dag @@ -0,0 +1,84 @@ +module product.printed_chassis.power_cassette + +import std.types { Bool, Int, List, NonEmptyStr } +import std.nat { Nat, nat_sub, NatSubtraction, NatDifference, NatSubtrahendExceedsMinuend } +import std.measure { Watt, watt, watt_count, Gram } +import product.node_power_envelope { EfficiencyRating, efficiency_percent, PsuAdequacy, PsuAdequate, PsuUndersized, PsuUndecidable, psu_adequacy_for_load } +import std.measure { percent_count } + +// THE POWER STATION AS A STACK MEMBER. Operator direction, 2026-10-04: a cassette in the same +// format that stacks with 1..N node cassettes and feeds everything below or beside it. It is the +// mains boundary for its assigned node group: the supply stays in its vendor enclosure inside this +// cassette, the IEC inlet and the protective-earth bond are here, and what leaves it is 12 V on +// protected branches. Every node cassette below is then a low-voltage zone with no mains part and +// bonding/ESD obligations to resolve for the selected electrical design. Low voltage alone +// does not establish insulation, touch-current or grounding compliance. +// +// PLACEMENT IS A PARAMETER, NOT A RULING. Top of column is the operator's first picture; bottom of +// column puts the heaviest unit lowest (the mechanics instrument prices both) and keeps the one +// unit nobody wants to move out of every LIFO pop. Beside the column is the withdrawn frame +// proposal's arrangement, kept as an arm so the harness census can show what it costs in cable. + +type PowerCassettePlacement = TopOfColumn | BottomOfColumn | BesideColumn + +type PowerCassette { + nodes_fed: Nat + placement: PowerCassettePlacement + supply_rated_output: Watt + supply_efficiency: EfficiencyRating + mass: Gram +} + +// Station adequacy is the per-node verdict applied to the sum, reusing the one adequacy fold. +fn station_adequacy(station: PowerCassette, node_dc_loads: List) -> PsuAdequacy { + let total = fold(node_dc_loads, init: 0, f: (acc, w) => acc + watt_count(w: w)) + psu_adequacy_for_load(load: watt(count: total), rated: station.supply_rated_output) +} + +// Wall draw of the column at the station's efficiency grade, for the site feed. +fn station_wall_draw(station: PowerCassette, node_dc_loads: List) -> Watt { + let total = fold(node_dc_loads, init: 0, f: (acc, w) => acc + watt_count(w: w)) + watt(count: total * 100 / percent_count(p: efficiency_percent(e: station.supply_efficiency))) +} + +// WHAT THE GROUNDING WITNESSES G1-G5 BECOME under this arrangement, as rows a build can check off. +// They were written against a metal rack; the subject moves to the power cassette and node bonding/ESD requirements remain an electrical-design decision, not inferred from voltage alone. +type EarthObligation + = SupplyEnclosureBondedToInletEarth + | DcReturnBondingRequiresSelectedSupplyDesign + | NoMainsPartOutsidePowerCassette + | NoPrintedPolymerInEarthPath + | NodeCassetteRemovalLeavesEarthIntact + +data power_cassette_earth_obligations: List = [ + SupplyEnclosureBondedToInletEarth, + DcReturnBondingRequiresSelectedSupplyDesign, + NoMainsPartOutsidePowerCassette, + NoPrintedPolymerInEarthPath, + NodeCassetteRemovalLeavesEarthIntact, +] + +// Which units a pop of node k must lift, by placement. A pop lifts everything above k; a station +// on top is lifted by every pop, a station at the bottom by none. +data no_units: Nat = 0 +// Nodes above index i in a column of n: n - (i + 1); an index past the top is no node and lifts nothing. +fn nodes_above(node_count: Nat, node_index_from_bottom: Nat) -> Nat { + match nat_sub(a: node_count, b: node_index_from_bottom + 1) { NatDifference { value: v } => v NatSubtrahendExceedsMinuend => no_units } +} +fn units_lifted_to_pop(station: PowerCassette, node_count: Nat, node_index_from_bottom: Nat) -> Nat { + let nodes_above = nodes_above(node_count: node_count, node_index_from_bottom: node_index_from_bottom) + match station.placement { + TopOfColumn => nodes_above + 1 + BottomOfColumn => nodes_above + BesideColumn => nodes_above + } +} + +// Supply telemetry: this legacy route describes one board managing one supply. A shared station +// is monitored by at most one of its nodes, or by nothing. Stated so a fleet reader does not +// assume every node sees the supply. +type StationTelemetryRoute + = MonitoredByOneNodePsuSmbus { node_index_from_bottom: Nat } + | StationUnmonitored + +data station_telemetry_default: StationTelemetryRoute = StationUnmonitored diff --git a/dag/gunbc/product/printed_chassis/rack_assembly.dag b/dag/gunbc/product/printed_chassis/rack_assembly.dag new file mode 100644 index 00000000000..9bad9fee2fe --- /dev/null +++ b/dag/gunbc/product/printed_chassis/rack_assembly.dag @@ -0,0 +1,43 @@ +module product.printed_chassis.rack_assembly + +import std.types { Int, String, List } +import std.measure { micrometer_count } +import extdeps.cooling.dynatron { dynatron_w1_active_length,dynatron_w1_active_width,dynatron_w1_active_height } +import extdeps.fans.arctic { arctic_p8_frame_depth,arctic_p8_mounting_pattern } +import std.euclidean_geometry { Axis3Y, Axis3Z } +import product.printed_chassis.assembly_geometry { AssemblyPart, Primitive, Purchased, Metal, Clearance, Cassette, FixedFrame, LayOnXZ, LayOnXY, block,round,part } +import product.printed_chassis.cassette { tray_w,tray_d,board_w,board_d,bay_pitch,bay_w,rear_plate_z,fan_spacing,fan_hole_offset,fan_hole_d } + +// Hardware bodies are catalog envelopes or explicit space reservations, not manufacturing CAD. +// Positions are design proposals; drives, GPUs and real connector pinouts remain unresolved. +fn hardware() -> List { + concat([ + part(id:"BOARD",name:"ALTRAD8UD-1L2T reference",kind:Clearance,scope:Cassette,adds:[block(w:board_w,h:1000,d:board_d,x:0,y:0,z:0)],cuts:[],x:0,y:27000,z:0,ex:0,ey:90000,ez:0,plane:LayOnXZ,notes:"Nominal board outline from vendor authority. 1 mm display slab is not measured PCB thickness. Elevation and socket/component placement provisional; board support hardware is unresolved."), + part(id:"CPU",name:"Dynatron W1 active envelope",kind:Purchased,scope:Cassette,adds:[block(w:micrometer_count(m:dynatron_w1_active_width),h:micrometer_count(m:dynatron_w1_active_height),d:micrometer_count(m:dynatron_w1_active_length),x:0,y:micrometer_count(m:dynatron_w1_active_height)/2,z:0)],cuts:[],x:0,y:30000,z:0,ex:0,ey:140000,ez:0,plane:LayOnXZ,notes:"Manufacturer active envelope 141.1 x 84 x 72 mm (dynatron.co/product-page/w1); simplified envelope. Location, orientation and installed offset provisional."), + part(id:"RAM1",name:"Left DIMM clearance",kind:Clearance,scope:Cassette,adds:[block(w:25000,h:40000,d:140000,x:0,y:20000,z:0)],cuts:[],x: -87000,y:27000,z:0,ex: -35000,ey:70000,ez:0,plane:LayOnXZ,notes:"Space reservation only; not surveyed DIMM positions or height."), + part(id:"RAM2",name:"Right DIMM clearance",kind:Clearance,scope:Cassette,adds:[block(w:25000,h:40000,d:140000,x:0,y:20000,z:0)],cuts:[],x:87000,y:27000,z:0,ex:35000,ey:70000,ez:0,plane:LayOnXZ,notes:"Space reservation only; not surveyed DIMM positions or height."), + part(id:"DC",name:"Isolatable DC disconnect reservation",kind:Clearance,scope:Cassette,adds:[block(w:24000,h:20000,d:18000,x:0,y:0,z:0)],cuts:[],x:tray_w/2-10000,y:22000,z:0-tray_d/2-12000,ex:45000,ey:30000,ez:0,plane:LayOnXZ,notes:"Rated/keyed connector not selected. One protected 12 V feed per node; shut down, isolate and disconnect before extraction. Not hot-swap."), + part(id:"CABLE",name:"Node harness corridor",kind:Clearance,scope:Cassette,adds:[block(w:10000,h:10000,d:tray_d-50000,x:0,y:0,z:0)],cuts:[],x:tray_w/2-15000,y:18000,z: -10000,ex:45000,ey:20000,ez:0,plane:LayOnXZ,notes:"Reserved route, not actual harness CAD; connector endpoints and bend radii pending."), + part(id:"IO",name:"Rear I/O service volume",kind:Clearance,scope:Cassette,adds:[block(w:board_w,h:45000,d:50000,x:0,y:0,z:0)],cuts:[],x:0,y:50000,z:0-tray_d/2-25000,ex:0,ey:40000,ez: -20000,plane:LayOnXZ,notes:"50 mm proposed service space ahead of rear fan bank. Actual rear connectors, cable bends and hand access require assembly review; this is not a measured keep-out.") + ],flat_map([-1,0,1],fn(i){[ + part(id:concat("F",to_string(i+2)),name:"ARCTIC P8 rear exhaust",kind:Purchased,scope:Cassette, + adds:[block(w:micrometer_count(m:arctic_p8_mounting_pattern.frame_edge),h:micrometer_count(m:arctic_p8_mounting_pattern.frame_edge),d:micrometer_count(m:arctic_p8_frame_depth),x:0,y:0,z:0)],cuts:concat([round(diameter:73000,length:30000,axis:Axis3Z,x:0,y:0,z:0)],flat_map([-1,1],x=>flat_map([-1,1],y=>[round(diameter:fan_hole_d,length:30000,axis:Axis3Z,x:x*fan_hole_offset,y:y*fan_hole_offset,z:0)]))), + x:i*fan_spacing,y:55000,z:rear_plate_z+2000+micrometer_count(m:arctic_p8_frame_depth)/2,ex:i*40000,ey:0,ez: -25000,plane:LayOnXY, + notes:"80 x 80 x 25 mm catalog frame; rotor omitted. Rear exhaust, three-fan layout proposal; thermal adequacy and PST header loading unqualified.") + ]})) +} +fn fixed_parts() -> List { + let rack_d=tray_d+150000 + let cz= -30000 + let posts=flat_map([-1,0,1],x=>flat_map([-1,1],z=>[block(w:14000,h:bay_pitch*2+45000,d:14000,x:x*bay_w,y:bay_pitch-12000,z:cz+z*(rack_d/2+18000))])) + let beams=flat_map([-28000,bay_pitch-28000,bay_pitch*2+10000],y=>concat( + map([-1,1],z=>block(w:bay_w*2+14000,h:12000,d:14000,x:0,y:y,z:cz+z*(rack_d/2+18000))), + map([-1,0,1],x=>block(w:14000,h:12000,d:rack_d+36000,x:x*bay_w,y:y,z:cz)))) + let runners=flat_map([-1,1],col=>flat_map([0,1],row=>map([-1,1],side=>block(w:8000,h:6000,d:rack_d+36000,x:col*bay_w/2+side*(tray_w/2+6000),y:row*bay_pitch-7000,z:cz)))) + let feeds=flat_map([-1,1],x=>map([0,1],row=>block(w:5000,h:bay_pitch*2+20000-row*bay_pitch,d:5000,x:x*bay_w/2+tray_w/2-10000,y:(row*bay_pitch+bay_pitch*2+64000)/2,z:0-tray_d/2-35000))) + [part(id:"FRAME",name:"Fixed metal 2 x 2 frame",kind:Metal,scope:FixedFrame,adds:concat(posts,concat(beams,runners)),cuts:[],x:0,y:0,z:0,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"Metal stock proposal: posts 14 mm square and beams 14 x 12 mm. Cut lengths derived; 8 x 6 mm fixed runner proposals contact the outer printed rail strip. Actual stock, runner brackets, joining and stiffness to be selected. Not printable."), + part(id:"SHELF",name:"Shared power shelf",kind:Metal,scope:FixedFrame,adds:[block(w:bay_w*2,h:8000,d:rack_d,x:0,y:0,z:0)],cuts:[],x:0,y:bay_pitch*2+28000,z:cz,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"Top shelf proposal; metal gauge and fastening unqualified. Fixed protective earth must survive cassette removal."), + part(id:"PSU",name:"Shared 12 V supply reservation",kind:Clearance,scope:FixedFrame,adds:[block(w:150000,h:85000,d:150000,x:0,y:42500,z:0)],cuts:[],x:0,y:bay_pitch*2+32000,z:0,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"One supply system per four nodes. 150 x 85 x 150 mm is a space reservation, not a selected model. Wattage and redundancy unresolved; 400-600 W per-node example is superseded."), + part(id:"PDB",name:"Four-branch distribution reservation",kind:Clearance,scope:FixedFrame,adds:[block(w:150000,h:30000,d:35000,x:0,y:0,z:0)],cuts:[],x:0,y:bay_pitch*2+55000,z:0-rack_d/2-15000,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"Four protected, isolatable branches. No circuit, ratings, wire gauge or connector pinouts invented."), + part(id:"FEEDS",name:"Fixed branch route reservations",kind:Clearance,scope:FixedFrame,adds:feeds,cuts:[],x:0,y:0,z:0,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"Four schematic branch corridors remain on the frame when a node is removed. Actual distribution routing and disconnect positioning require review.")] +} diff --git a/dag/gunbc/product/printed_chassis/rear_cooling.dag b/dag/gunbc/product/printed_chassis/rear_cooling.dag new file mode 100644 index 00000000000..55a1b65ded8 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/rear_cooling.dag @@ -0,0 +1,61 @@ +module product.printed_chassis.rear_cooling +import std.types {Int,List,String,Bool} +import std.measure {micrometer,micrometer_count} +import v2.std.optional {Present,Absent} +import std.nat {nat_range_inclusive} +import extdeps.fans.types {FanMountingPattern} +import extdeps.fans.arctic {arctic_p8_mounting_pattern,arctic_f9_mounting_pattern,arctic_p12_mounting_pattern,arctic_p14_mounting_pattern} +import product.printed_chassis.fan_mount {FanMountAdmission,admit_fan_mount} +import extdeps.languages.json.emit {JsonValue,json_object,json_kv,json_string,json_int,json_array,json_null} + +// Fit is geometric only: frame size cannot establish airflow, header capacity or noise. +// RearOpening is a single unobstructed rectangle after subtracting structural and service space. +// Multiple disjoint openings must be evaluated separately, never added into a fictitious width. +type RearOpening {width_um:Int,height_um:Int,depth_um:Int} +type RearFanCandidate {id:String,pattern:FanMountingPattern,depth_um:Int} +type RearFanBank {fan:RearFanCandidate,count:Int,plate_edge_um:Int,centres_x_um:List} +type RearCoolingReading = RearFanGeometry {bank:RearFanBank} | RearCoolingInvalidInput | RearCoolingNoFit + +// 40 mm is a conservative design reservation for a fan body, not a vendor depth claim. +// It includes the published 25/27 mm bodies; mounts/guards/cables reserve another 20 mm. +data rear_fan_candidates:List = [ + RearFanCandidate {id:"arctic-p14-pwm-pst",pattern:arctic_p14_mounting_pattern,depth_um:40000}, + RearFanCandidate {id:"arctic-p12-pwm-pst",pattern:arctic_p12_mounting_pattern,depth_um:40000}, + RearFanCandidate {id:"arctic-f9-pwm-pst",pattern:arctic_f9_mounting_pattern,depth_um:40000}, + RearFanCandidate {id:"arctic-p8-pwm-pst",pattern:arctic_p8_mounting_pattern,depth_um:40000}, +] +data rear_mount_margin_um:Int=6000 +data rear_plate_gap_um:Int=2000 +data rear_mount_service_depth_um:Int=20000 +fn rear_plate_edge(fan:RearFanCandidate) -> Int {micrometer_count(m:fan.pattern.frame_edge)+2*rear_mount_margin_um} +fn rear_opening(width_um:Int,pitch_um:Int,left_um:Int,right_um:Int,bottom_um:Int,top_um:Int,depth_um:Int) -> RearOpening { + RearOpening {width_um:width_um-left_um-right_um,height_um:pitch_um-bottom_um-top_um,depth_um:depth_um} +} +fn rear_fan_fits(fan:RearFanCandidate,opening:RearOpening,bed_um:Int) -> Bool { + let edge=rear_plate_edge(fan:fan) + edge<=opening.width_um && edge<=opening.height_um && edge<=bed_um && fan.depth_um+rear_mount_service_depth_um<=opening.depth_um +} +fn select_rear_fans(candidates:List,opening:RearOpening,bed_um:Int) -> RearCoolingReading { + if opening.width_um<=0 || opening.height_um<=0 || opening.depth_um<=0 || bed_um<=0 {return RearCoolingInvalidInput} + let fits=filter(candidates,c=>c.depth_um>0 && micrometer_count(m:c.pattern.frame_edge)>0 && rear_fan_fits(fan:c,opening:opening,bed_um:bed_um)) + match fits.first() { + Absent=>RearCoolingNoFit + Present {value:first}=>{ + let selected=fold(fits,init:first,f:(a,b)=>if micrometer_count(m:b.pattern.frame_edge)>micrometer_count(m:a.pattern.frame_edge) {b} else {a}) + let edge=rear_plate_edge(fan:selected) + let n=(opening.width_um+rear_plate_gap_um)/(edge+rear_plate_gap_um) + let span=n*edge+(n-1)*rear_plate_gap_um + RearFanGeometry {bank:RearFanBank {fan:selected,count:n,plate_edge_um:edge,centres_x_um:map(nat_range_inclusive(lo:0,hi:n-1),i=>0-span/2+edge/2+i*(edge+rear_plate_gap_um))}} + } + } +} +fn rear_mount(bank:RearFanBank) -> FanMountAdmission { + admit_fan_mount(pattern:bank.fan.pattern,plate_edge:micrometer(count:bank.plate_edge_um),plate_thickness:micrometer(count:4000),bore_diameter:micrometer(count:micrometer_count(m:bank.fan.pattern.frame_edge)-4000)) +} +fn rear_cooling_json(reading:RearCoolingReading) -> JsonValue { + match reading { + RearCoolingInvalidInput=>json_object(members:[json_kv(key:"standing",value:json_string(s:"invalid rear opening or bed"))]) + RearCoolingNoFit=>json_object(members:[json_kv(key:"standing",value:json_string(s:"no standard candidate fits"))]) + RearFanGeometry {bank:b}=>json_object(members:[json_kv(key:"fan",value:json_string(s:b.fan.id)),json_kv(key:"count",value:json_int(n:b.count)),json_kv(key:"frame_edge_um",value:json_int(n:micrometer_count(m:b.fan.pattern.frame_edge))),json_kv(key:"hole_pitch_um",value:json_int(n:micrometer_count(m:b.fan.pattern.hole_pitch))),json_kv(key:"plate_edge_um",value:json_int(n:b.plate_edge_um)),json_kv(key:"centres_x_um",value:json_array(elements:map(b.centres_x_um,x=>json_int(n:x)))),json_kv(key:"standing",value:json_string(s:"geometric candidate; airflow, header load, acoustics and mount attachment unresolved"))]) + } +} diff --git a/dag/gunbc/product/printed_chassis/rear_fan_assembly.dag b/dag/gunbc/product/printed_chassis/rear_fan_assembly.dag new file mode 100644 index 00000000000..4780f4523a9 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/rear_fan_assembly.dag @@ -0,0 +1,96 @@ +module product.printed_chassis.rear_fan_assembly +import std.types {List,Int,String} +import std.measure {micrometer_count} +import std.euclidean_geometry {Axis3Y,Axis3Z} +import product.printed_chassis.assembly_geometry {AssemblyPart,Primitive,MechanicalInterface,Printed,Purchased,Cassette,LayOnXY,LayOnXZ,block,round,part,mechanical_interface,PrintedRetainingPin,M4FanThroughBolt,M4SocketThroughBolt,Proposed} +import product.printed_chassis.cassette {tray_w,tray_d,rear_plate_z,rear_beam_z,extension,retaining_pin,rear_socket} +import product.printed_chassis.rear_cooling {RearFanBank,rear_mount} +import product.printed_chassis.fan_mount {FanMountAdmitted,FanMountBoreReachesMountingHoles,FanMountBoreNotEnclosedByPlate,FanMountPlateSmallerThanPattern,fan_mount_bore,fan_mount_mounting_holes} +import product.printed_chassis.coupon {OpThroughHole} +import extdeps.fans.arctic {arctic_p14_frame_depth} + +// Prototype joinery geometry. Original arm geometry is retained; sockets are separate support-free prints. +// Rear-bank self-weight is carried by the arms, never by the motherboard or cooler. +// This assembly is the current single-fan variant; selection of multiple fans needs +// a corresponding crossmember layout before this producer can admit it. +type RearAssemblyReading = RearAssemblyGeometry {parts:List,interfaces:List} | RearAssemblyUnsupportedBank | RearAssemblyMountRefused + +data rear_pin_clearance_um:Int=4400 +data rear_cradle_foot_x_um:Int=60000 +data rear_cradle_foot_z_um:Int=20000 +data rear_guard_spacing_um:Int=6000 +data rear_guard_bar_um:Int=3000 +data rear_guard_pitch_um:Int=10000 +fn through_y(x:Int,z:Int) -> Primitive {round(diameter:rear_pin_clearance_um,length:50000,axis:Axis3Y,x:x,y:0,z:z)} +fn crossmember(side:Int,id:String) -> AssemblyPart { + let half=tray_w/2+6000 + let centre=side*(half+200)/2 + part(id:id,name:"Split rear crossmember",kind:Printed,scope:Cassette, + adds:[block(w:half-200,h:8000,d:40000,x:0,y:0,z:0)], + cuts:[through_y(x:side*(tray_w/2-7000)-centre,z:0),through_y(x:side*rear_cradle_foot_x_um-centre,z:0)], + x:centre,y:0,z:rear_beam_z,ex:side*55000,ey: -18000,ez: -50000,plane:LayOnXZ, + notes:"Each half rests on one original support arm. Outer pin retains beam to arm; inner pin retains the one-piece cradle to beam. The cradle bridges the 0.4 mm centre gap. Prototype bearing and bending capacity unqualified.") +} +fn cradle(bank:RearFanBank,cuts:List) -> AssemblyPart { + let edge=bank.plate_edge_um + let cy=20000+edge/2 + let feet=flat_map([-1,1],s=>[ + block(w:24000,h:8000,d:40000,x:s*rear_cradle_foot_x_um,y:8000-cy,z:20000), + block(w:16000,h:16000,d:8000,x:s*(edge/2-8000),y:20000-cy,z:2000), + ]) + part(id:"M140",name:"Keyed 140 mm fan cradle",kind:Printed,scope:Cassette, + adds:concat([block(w:edge,h:edge,d:4000,x:0,y:0,z:0)],feet), + cuts:concat(cuts,map([-1,1],s=>round(diameter:rear_pin_clearance_um,length:50000,axis:Axis3Y,x:s*rear_cradle_foot_x_um,y:8000-cy,z:20000))), + x:0,y:cy,z:rear_plate_z,ex:0,ey:40000,ez: -70000,plane:LayOnXY, + notes:"One-piece plate with two feet and rising webs. Seats on split crossmember; two broad-grip pins retain it. Fan and guard use two diagonal bolts. Layer direction, foot/web strength and print supports require review.") +} +fn guard(bank:RearFanBank) -> AssemblyPart { + let edge=bank.plate_edge_um + let off=micrometer_count(m:bank.fan.pattern.hole_pitch)/2 + let t=3000 + let rim=flat_map([-1,1],s=>[ + block(w:edge,h:8000,d:t,x:0,y:s*(edge/2-4000),z:0), + block(w:8000,h:edge,d:t,x:s*(edge/2-4000),y:0,z:0), + ]) + let bars=map([-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6],i=>block(w:edge-12000,h:rear_guard_bar_um,d:t,x:0,y:i*rear_guard_pitch_um,z:0)) + let spacers=flat_map([-1,1],x=>map([-1,1],y=>round(diameter:10000,length:rear_guard_spacing_um+1500,axis:Axis3Z,x:x*off,y:y*off,z:(rear_guard_spacing_um+1500)/2))) + let pins=map([-1,1],s=>round(diameter:3800,length:6000,axis:Axis3Z,x:s*off,y:0-s*off,z:rear_guard_spacing_um+4500)) + let cuts=map([-1,1],s=>round(diameter:micrometer_count(m:bank.fan.pattern.hole_clearance_diameter),length:60000,axis:Axis3Z,x:s*off,y:s*off,z:0)) + part(id:"G140",name:"Rear guard with integral spacers",kind:Printed,scope:Cassette,adds:concat(rim,concat(bars,concat(spacers,pins))),cuts:cuts, + x:0,y:20000+edge/2,z:rear_plate_z-2000-micrometer_count(m:arctic_p14_frame_depth)-rear_guard_spacing_um-1500,ex:0,ey:0,ez: -155000,plane:LayOnXY, + notes:"3 mm bars on 10 mm pitch: nominal 7 mm gaps. Integral 6 mm spacers separate grille from fan rear frame; opposite locating pins register the guard. Two diagonal through-bolts join cradle, fan and guard. Blade clearance, deflection and finger protection are unqualified.") +} +fn fan_body(bank:RearFanBank) -> AssemblyPart { + let frame=micrometer_count(m:bank.fan.pattern.frame_edge) + let depth=micrometer_count(m:arctic_p14_frame_depth) + let off=micrometer_count(m:bank.fan.pattern.hole_pitch)/2 + let holes=flat_map([-1,1],x=>map([-1,1],y=>round(diameter:micrometer_count(m:bank.fan.pattern.hole_clearance_diameter),length:depth+2000,axis:Axis3Z,x:x*off,y:y*off,z:0))) + part(id:"F140",name:"ARCTIC P14 PWM PST envelope",kind:Purchased,scope:Cassette, + adds:[block(w:frame,h:frame,d:depth,x:0,y:0,z:0)],cuts:concat([round(diameter:frame-8000,length:depth+2000,axis:Axis3Z,x:0,y:0,z:0)],holes), + x:0,y:20000+bank.plate_edge_um/2,z:rear_plate_z-2000-depth/2,ex:0,ey:0,ez: -110000,plane:LayOnXY, + notes:"Publisher 140 x 140 x 27 mm frame envelope and 125 mm mounting pitch. Simplified frame aperture; rotor not modeled. Installed-flow, PWM floor and board header current limit remain unresolved.") +} +fn rear_interfaces() -> List {[ + mechanical_interface(id:"rear-arm-sockets",members:["P1","P2","E1","E2","S1","S2"],fastener:M4SocketThroughBolt,quantity:2,status:Proposed,requirement:"Separate open sockets retain original arm geometry. One M4 through-bolt per socket/arm/tray stack replaces the friction pin; load qualification pending."), + mechanical_interface(id:"split-crossmember",members:["E1","E2","B140L","B140R","K3","K4"],fastener:PrintedRetainingPin,quantity:2,status:Proposed,requirement:"Crossmember rests on arms; outer pins prevent separation. No centre splice screw."), + mechanical_interface(id:"keyed-cradle",members:["B140L","B140R","M140","K6","K7"],fastener:PrintedRetainingPin,quantity:2,status:Proposed,requirement:"Cradle feet bear on crossmember; retain with two inner pins. Loaded joints not qualified."), + mechanical_interface(id:"fan-and-guard-140",members:["M140","F140","G140"],fastener:M4FanThroughBolt,quantity:2,status:Proposed,requirement:"Two diagonal M4 through-bolts plus opposite locating pins. Nominal stack 40 mm before washers/nut: 4 mm plate + 27 mm fan + 6 mm spacer + 3 mm grille. Final length/engagement unresolved."), +]} +fn rear_assembly(bank:RearFanBank) -> RearAssemblyReading { + if bank.count!=1 || bank.fan.id!="arctic-p14-pwm-pst" {return RearAssemblyUnsupportedBank} + match rear_mount(bank:bank) { + FanMountBoreReachesMountingHoles {bore_radius:_,hole_center_distance:_}=>RearAssemblyMountRefused + FanMountBoreNotEnclosedByPlate {bore_diameter:_,plate_edge:_}=>RearAssemblyMountRefused + FanMountPlateSmallerThanPattern {plate_edge:_,required_edge:_}=>RearAssemblyMountRefused + FanMountAdmitted {geometry:g}=>{ + let edge=bank.plate_edge_um + let cuts=map(concat([fan_mount_bore(g:g)],fan_mount_mounting_holes(g:g)),op=>match op {OpThroughHole {diameter:d,center_x:x,center_y:y}=>round(diameter:micrometer_count(m:d),length:12000,axis:Axis3Z,x:micrometer_count(m:x)-edge/2,y:micrometer_count(m:y)-edge/2,z:0)}) + RearAssemblyGeometry {interfaces:rear_interfaces(),parts:[ + extension(sx: -1,id:"E1"),extension(sx:1,id:"E2"),crossmember(side: -1,id:"B140L"),crossmember(side:1,id:"B140R"),cradle(bank:bank,cuts:cuts),fan_body(bank:bank),guard(bank:bank), + rear_socket(sx: -1,id:"S1"),rear_socket(sx:1,id:"S2"), + retaining_pin(x:0-tray_w/2+7000,z:rear_beam_z,bottom: -12000,top:4000,id:"K3"),retaining_pin(x:tray_w/2-7000,z:rear_beam_z,bottom: -12000,top:4000,id:"K4"), + retaining_pin(x: -rear_cradle_foot_x_um,z:rear_beam_z,bottom: -4000,top:12000,id:"K6"),retaining_pin(x:rear_cradle_foot_x_um,z:rear_beam_z,bottom: -4000,top:12000,id:"K7"), + ]} + } + } +} diff --git a/dag/gunbc/product/printed_chassis/residential_review.dag b/dag/gunbc/product/printed_chassis/residential_review.dag new file mode 100644 index 00000000000..6e40d26192b --- /dev/null +++ b/dag/gunbc/product/printed_chassis/residential_review.dag @@ -0,0 +1,44 @@ +module product.printed_chassis.residential_review +import std.measure {micrometer_count} +import product.printed_chassis.cassette {printed_parts,tray_w,tray_d,board_w,board_d} +import product.printed_chassis.rack_assembly {hardware} +import product.printed_chassis.cooler_configuration {noctua_envelope,residential_cooler} +import product.printed_chassis.stack_geometry {stack_pitch_for_cooler} +import product.printed_chassis.rear_cooling {RearFanGeometry,RearCoolingInvalidInput,RearCoolingNoFit,rear_opening,select_rear_fans,rear_fan_candidates,rear_cooling_json} +import product.printed_chassis.cassette_review {part_json,interface_json} +import product.printed_chassis.stack_joinery {stack_joinery,StackJoineryGeometry,StackJoineryHeightUnsupported} +import product.printed_chassis.rear_fan_assembly {rear_assembly,RearAssemblyGeometry,RearAssemblyUnsupportedBank,RearAssemblyMountRefused} +import product.printed_chassis.cadquery_realization {realize_review_parts,ReviewCadQueryEmitted,ReviewCadQueryRefused,review_program_source} +import extdeps.languages.json.emit {serialize_json,json_object,json_kv,json_array,json_string,json_int} +import gunbc.cli_wire {CliWireResponse,CliWirePrintable} +import std.process {ExitSuccess,exit_failure} + +fn main() -> CliWireResponse { + let pitch=stack_pitch_for_cooler(cooler:residential_cooler,contact_plane_um:30000) + let selection=select_rear_fans(candidates:rear_fan_candidates,opening:rear_opening(width_um:tray_w,pitch_um:pitch,left_um:20000,right_um:20000,bottom_um:20000,top_um:20000,depth_um:60000),bed_um:170000) + match selection { + RearCoolingInvalidInput=>CliWirePrintable {bytes:"Invalid rear opening",exit:exit_failure(reason:"Invalid rear opening")} + RearCoolingNoFit=>CliWirePrintable {bytes:"No rear fan fits",exit:exit_failure(reason:"No rear fan fits")} + RearFanGeometry {bank:b}=>{ + if b.count!=1 || micrometer_count(m:b.fan.pattern.frame_edge)!=140000 {return CliWirePrintable {bytes:"Residential review requires current single 140 mm selection",exit:exit_failure(reason:"Review configuration changed")}} + let tray=filter(printed_parts(),p=>p.id=="P1"||p.id=="P2"||p.id=="P3"||p.id=="P4") + let board=filter(hardware(),p=>p.id=="BOARD"||p.id=="RAM1"||p.id=="RAM2"||p.id=="IO") + match rear_assembly(bank:b) { + RearAssemblyUnsupportedBank=>CliWirePrintable {bytes:"Unsupported rear assembly",exit:exit_failure(reason:"Unsupported rear assembly")} + RearAssemblyMountRefused=>CliWirePrintable {bytes:"Rear mount refused",exit:exit_failure(reason:"Rear mount refused")} + RearAssemblyGeometry {parts:rear,interfaces:joints}=>{ + match stack_joinery(pitch_um:pitch) { + StackJoineryHeightUnsupported=>CliWirePrintable {bytes:"Stack joinery height unsupported",exit:exit_failure(reason:"Stack joinery height unsupported")} + StackJoineryGeometry {parts:posts}=>{ + let parts=concat(tray,concat(board,concat([noctua_envelope(contact_plane_um:30000)],concat(rear,posts)))) + match realize_review_parts(parts:parts) { + ReviewCadQueryRefused {reason:_}=>CliWirePrintable {bytes:"Review CAD refused",exit:exit_failure(reason:"Review CAD refused")} + ReviewCadQueryEmitted {program:p}=>CliWirePrintable {bytes:serialize_json(v:json_object(members:[json_kv(key:"program",value:json_string(s:review_program_source(p:p))),json_kv(key:"parts",value:json_array(elements:map(parts,p=>part_json(p:p)))),json_kv(key:"interfaces",value:json_array(elements:map(joints,j=>interface_json(i:j)))),json_kv(key:"pitch_um",value:json_int(n:pitch)),json_kv(key:"tray_um",value:json_array(elements:[json_int(n:tray_w),json_int(n:tray_d)])),json_kv(key:"board_um",value:json_array(elements:[json_int(n:board_w),json_int(n:board_d)])),json_kv(key:"cooling",value:rear_cooling_json(reading:selection))])),exit:ExitSuccess} + } + } + } +} +} +} +} +} diff --git a/dag/gunbc/product/printed_chassis/review_realization.dag b/dag/gunbc/product/printed_chassis/review_realization.dag new file mode 100644 index 00000000000..df8bf0cca35 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/review_realization.dag @@ -0,0 +1,181 @@ +module product.printed_chassis.review_realization + +import std.types { String,List } +import product.printed_chassis.assembly_geometry {AssemblyPart,MechanicalInterface} +import gunbc.cli_wire { CliWireResponse, CliWirePrintable, CliWireUnprintable } +import std.process { ExitSuccess,exit_failure } +import product.printed_chassis.cadquery_realization {realize_review_parts,ReviewCadQueryEmitted,ReviewCadQueryRefused,review_program_source} +import product.printed_chassis.cassette_review {all_parts,manifest_for} +import extdeps.languages.json.emit {serialize_json,json_string,json_object,json_kv} +import product.printed_chassis.cassette {interfaces} +import product.printed_chassis.assembly_review_checks {review_model_checks} + +// Review-only host adapter. Mechanical geometry, placements, materials and tolerances +// are inputs from cassette_review, never decisions made here. This emits no printer +// command and grants no AdmittedRealizationV0 / fabrication capability. The existing +// admission workflow remains mandatory after assembly review and before printing. +// Exercise with CadQuery 2.8.0; rejection is atomic (no partial review package). +// Python is emitted transiently because this repository owns tools in .dag/.rs. +fn adapter_source() -> String { join([ + "#!/usr/bin/env python3\n", + "\"\"\"Realize a DAG-emitted review manifest. No cassette dimensions live in this adapter.\n", + "\n", + "CadQuery 2.8.0 is the exercised kernel. Only the closed box/cylinder union/difference\n", + "vocabulary is accepted; unknown operations, disconnected printed solids and oversized\n", + "parts refuse. This is review-output validation, not powered/structural qualification.\n", + "\"\"\"\n", + "import argparse, collections, csv, hashlib, io, json, math, pathlib, re, shutil, tempfile, zipfile\n", + "import cadquery as cq\n", + "from OCP.Bnd import Bnd_Box\n", + "from OCP.BRepBndLib import BRepBndLib\n", + "from vtkmodules.vtkIOGeometry import vtkSTLReader\n", + "from vtkmodules.vtkFiltersCore import vtkFeatureEdges, vtkMassProperties\n", + "\n", + "\n", + "def mm(values):\n", + " assert all(type(v) is int for v in values), 'Expected integer micrometres'\n", + " return [v / 1000 for v in values]\n", + "\n", + "\n", + "def exact_bounds(shape):\n", + " b=Bnd_Box(); BRepBndLib.AddOptimal_s(shape.wrapped,b,False,False)\n", + " return b.Get()\n", + "\n", + "def extent(shape):\n", + " b=exact_bounds(shape); return [b[i+3]-b[i] for i in range(3)]\n", + "\n", + "\n", + "def near(a,b,tolerance=2e-5):\n", + " assert len(a)==len(b) and all(abs(x-y)0\n", + " e=vtkFeatureEdges(); e.SetInputData(mesh); e.BoundaryEdgesOn(); e.NonManifoldEdgesOn(); e.FeatureEdgesOff(); e.ManifoldEdgesOff(); e.Update()\n", + " assert e.GetOutput().GetNumberOfCells()==0, 'Open/nonmanifold STL'\n", + " b=mesh.GetBounds(); near([b[1]-b[0],b[3]-b[2],b[5]-b[4]],extent(shape),2*tolerance+2e-5)\n", + " mass=vtkMassProperties();mass.SetInputData(mesh);mass.Update()\n", + " assert math.isclose(mass.GetVolume(),shape.Volume(),rel_tol=3e-4), 'STL volume divergence'\n", + " return mesh.GetNumberOfCells()\n", + "\n", + "\n", + "def primitive_bounds(p):\n", + " c=mm(p['center_um'])\n", + " if p['op']=='box': s=mm(p['size_um'])\n", + " else:\n", + " s=[p['diameter_um']/1000]*3;s['xyz'.index(p['axis'])]=p['length_um']/1000\n", + " return [c[i]-s[i]/2 for i in range(3)]+[c[i]+s[i]/2 for i in range(3)]\n", + "\n", + "\n", + "def model_bounds(p):\n", + " b=[primitive_bounds(a) for a in p['adds']]\n", + " return [min(r[i] for r in b) for i in range(3)]+[max(r[i+3] for r in b) for i in range(3)]\n", + "\n", + "\n", + "def overlaps(a,b):\n", + " return all(min(a[i+3],b[i+3])-max(a[i],b[i])>1e-5 for i in range(3))\n", + "\n", + "\n", + "def realize(model, out):\n", + " assert dict(model,source_manifest_sha256=None) == dict(_expected_manifest,source_manifest_sha256=None), 'Manifest differs from the emitted geometry'\n", + " assert model['schema']=='gunbc.cassette-review.v1' and model['quantum']=='micrometre'\n", + " assert model['release']=='review-only; no print or powered-operation admission'\n", + " assert cq.__version__=='2.8.0', 'Use the recorded CadQuery 2.8.0 environment'\n", + " ids=[p['id'] for p in model['parts']];assert len(set(ids))==len(ids)\n", + " assert all(re.fullmatch(r'[A-Za-z0-9_-]+',i) for i in ids)\n", + " for interface in model['interfaces']:\n", + " assert set(interface['members'])<=set(ids), interface\n", + " shapes=\{\}; world=\{\}; bounds=\{\}; groups=\{\}; errors=[]\n", + " tol=model['export']['linear_tolerance_um']/1000\n", + " angular=float(model['export']['angular_tolerance_radians'])\n", + " for part_index,p in enumerate(model['parts']):\n", + " try:\n", + " assert p['adds'], 'No positive geometry (upstream admission refused?)'\n", + " body=_review_shapes[part_index]\n", + " body=body.clean(); assert body.isValid() and body.Volume()>0\n", + " if p['kind']=='printed': assert len(body.Solids())==1, f'\{len(body.Solids())\} disconnected solids'\n", + " expected=model_bounds(p)\n", + " near(extent(body),[expected[i+3]-expected[i] for i in range(3)])\n", + " vertices, faces=body.tessellate(tol,angular)\n", + " mesh=\{'vertices':[c for v in vertices for c in v.toTuple()], 'indices':[i for f in faces for i in f]\}\n", + " (out/f'\{p[\"id\"]\}.mesh.json').write_text(json.dumps(mesh,separators=(',',':')))\n", + " p['mesh']=f'\{p[\"id\"]\}.mesh.json';p['extent_mm']=extent(body);p['volume_mm3']=body.Volume()\n", + " shapes[p['id']]=body;at=mm(p['at_um']);world[p['id']]=body.translate(at)\n", + " bounds[p['id']]=[expected[i]+at[i%3] for i in range(6)]\n", + " if p['kind']=='printed':\n", + " oriented=body\n", + " if p['print_plane']=='XZ':oriented=body.rotate((0,0,0),(1,0,0),90)\n", + " elif p['print_plane']=='YZ':oriented=body.rotate((0,0,0),(0,1,0),-90)\n", + " else:assert p['print_plane']=='XY'\n", + " b=exact_bounds(oriented);oriented=oriented.translate(tuple(-v for v in b[:3]))\n", + " assert all(n<=limit for n,limit in zip(extent(oriented),model['print_envelope_mm'])), 'Build envelope exceeded'\n", + " stl=out/f'\{p[\"id\"]\}.stl';oriented.exportStl(str(stl),tolerance=tol,angularTolerance=angular,ascii=False,relative=False,parallel=True)\n", + " triangles=mesh_check(stl,oriented,tol)\n", + " step=out/f'\{p[\"id\"]\}.step';cq.exporters.export(body,str(step),exportType='STEP')\n", + " readback=cq.importers.importStep(str(step)).val();near(extent(readback),extent(body))\n", + " assert math.isclose(readback.Volume(),body.Volume(),rel_tol=1e-8)\n", + " p.update(stl=stl.name,step=step.name,print_extent_mm=extent(oriented),triangles=triangles,sha256=hashlib.sha256(stl.read_bytes()).hexdigest(),checks='single solid; STEP round-trip; closed STL; volume/bounds; bed fit')\n", + " identity=hashlib.sha256(json.dumps(\{k:p[k] for k in ['adds','cuts','print_plane']\},sort_keys=True).encode()).hexdigest()\n", + " groups.setdefault(identity,[]).append(p['id'])\n", + " print('OK',p['id'],p['name'],flush=True)\n", + " except Exception as exc:\n", + " errors.append(f'\{p[\"id\"]\}: \{exc\}');print('REFUSED',errors[-1],flush=True)\n", + " if errors: raise ValueError('\\n'.join(errors))\n", + " # Exact CAD intersections between moving printed parts, after a broad-phase AABB check.\n", + " printed=[p for p in model['parts'] if p['kind']=='printed'];collisions=[]\n", + " for n,a in enumerate(printed):\n", + " for b in printed[n+1:]:\n", + " if overlaps(bounds[a['id']],bounds[b['id']]):\n", + " volume=world[a['id']].intersect(world[b['id']]).Volume()\n", + " if volume>1e-4:collisions.append(\{'a':a['id'],'b':b['id'],'volume_mm3':volume\})\n", + " if collisions:raise ValueError('Printed-part collisions: '+json.dumps(collisions))\n", + " model['validation']=\{'cadquery':cq.__version__,'printed_part_count':len(printed),'unique_print_geometries':len(groups),'printed_pair_collisions':collisions,'source_model_checks':'DAG bed/mate/member checks','scope':'geometry only; no fastener engagement, thermal, structural or electrical qualification'\}\n", + " model['print_groups']=[\{'representative':items[0],'quantity':len(items),'instances':items\} for items in groups.values()]\n", + " assembly=cq.Assembly()\n", + " for p in model['parts']:\n", + " if p['scope']=='cassette' and p['kind']!='clearance':assembly.add(shapes[p['id']],name=p['id'],loc=cq.Location(cq.Vector(*mm(p['at_um']))))\n", + " assembly.export(str(out/'cassette-assembly.step'))\n", + " (out/'assembly.json').write_text(json.dumps(model,indent=2)+'\\n')\n", + " with (out/'parts.csv').open('w') as f:\n", + " w=csv.writer(f);w.writerow(['id','part','kind','scope','x_mm','y_mm','z_mm','notes'])\n", + " for p in model['parts']:w.writerow([p['id'],p['name'],p['kind'],p['scope'],*p['extent_mm'],p['notes']])\n", + " with (out/'interfaces.csv').open('w') as f:\n", + " w=csv.writer(f);w.writerow(['interface','members','fastener','quantity','status','requirement'])\n", + " for i in model['interfaces']:w.writerow([i['id'],' '.join(i['members']),i['fastener'],i['quantity'],i['status'],i['requirement']])\n", + " with (out/'print-list.csv').open('w') as f:\n", + " w=csv.writer(f);w.writerow(['representative','quantity','instances','print_file'])\n", + " for g in model['print_groups']:w.writerow([g['representative'],g['quantity'],' '.join(g['instances']),g['representative']+'.stl'])\n", + " (out/'README.txt').write_text('Cassette r06 review package. Generated from product.printed_chassis.cassette_review.\\n'+model['release']+'\\nSTLs are bed-oriented; individual STEPs and assembly STEP use assembly-local axes.\\nparts.csv / print-list.csv list all parts and repeat quantities. One M fan plate is already fit-tested.\\nNo additional gauge sequence. Review assembly, fasteners and unresolved board support before scheduling cassette prints.\\n')\n", + " with zipfile.ZipFile(out/'cassette-review.zip','w',zipfile.ZIP_DEFLATED) as z:\n", + " for f in sorted(out.iterdir()):\n", + " if f.suffix in ['.stl','.step','.csv','.txt'] or f.name=='assembly.json':z.write(f,f.name)\n", + "\n", + "\n", + "def main():\n", + " ap=argparse.ArgumentParser();ap.add_argument('manifest',type=pathlib.Path);ap.add_argument('output',type=pathlib.Path);args=ap.parse_args()\n", + " model=json.loads(args.manifest.read_text());model['source_manifest_sha256']=hashlib.sha256(args.manifest.read_bytes()).hexdigest()\n", + " args.output.parent.mkdir(parents=True,exist_ok=True)\n", + " staging=pathlib.Path(tempfile.mkdtemp(prefix='cad-review-',dir=args.output.parent))\n", + " try:\n", + " realize(model,staging)\n", + " if args.output.exists():raise ValueError('Output already exists; choose a fresh version directory')\n", + " staging.rename(args.output)\n", + " except Exception:\n", + " shutil.rmtree(staging);raise\n", + " print(json.dumps(model['validation']))\n", + "\n", + "if __name__=='__main__':main()\n" +], "") } +fn program_for(parts:List,joints:List) -> CliWireResponse { + if !review_model_checks(parts:parts,interfaces:joints) {return CliWirePrintable {bytes:"Assembly source checks refused",exit:exit_failure(reason:"Assembly source checks refused")}} + match realize_review_parts(parts:parts) {ReviewCadQueryRefused {reason:_}=>CliWirePrintable {bytes:"Review CAD emission refused",exit:exit_failure(reason:"Review CAD emission refused")} ReviewCadQueryEmitted {program:p}=>CliWirePrintable {bytes:join([review_program_source(p:p),"import json\n_expected_manifest = json.loads(",serialize_json(v:json_string(s:serialize_json(v:manifest_for(parts:parts,joints:joints)))),")\n",adapter_source()],""),exit:ExitSuccess}} +} +fn main() -> CliWireResponse {program_for(parts:all_parts(),joints:interfaces())} + +// Both payloads derive from the same parts and joints. Callers cannot supply a +// manifest from another model, and program_for owns the single admission check. +fn bundle_for(parts:List,joints:List) -> CliWireResponse { + match program_for(parts:parts,joints:joints) {CliWirePrintable {bytes:b,exit:e}=>CliWirePrintable {bytes:serialize_json(v:json_object(members:[json_kv(key:"manifest",value:manifest_for(parts:parts,joints:joints)),json_kv(key:"program",value:json_string(s:b))])),exit:e} CliWireUnprintable {cause:c}=>CliWireUnprintable {cause:c}} +} +fn bundle() -> CliWireResponse {bundle_for(parts:all_parts(),joints:interfaces())} diff --git a/dag/gunbc/product/printed_chassis/server_power_interface.dag b/dag/gunbc/product/printed_chassis/server_power_interface.dag new file mode 100644 index 00000000000..b406b51f182 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/server_power_interface.dag @@ -0,0 +1,85 @@ +module product.printed_chassis.server_power_interface +import std.types {Int,List,NonEmptyStr,String,Bool} +import std.measure {Watt,watt,watt_count,Volt,volt,volt_count,milliampere_count,Percent,percent,percent_count} +import v2.std.optional {Present,Absent} +import extdeps.power.fsp_crps {ServerPowerReference,fsp800_50fs_reference,fsp1200_50fs_reference} +import product.node_power_envelope {PsuAdequacy,PsuAdequate,PsuUndersized,PsuUndecidable,psu_adequacy_for_load} +import extdeps.languages.json.emit {JsonValue,json_object,json_kv,json_string,json_int,json_array,json_null} + +// Three distinct boundaries: module-to-matched-PDB, protected station-to-node, +// and internal harness-to-board. A PSU's hot-swap capability does not make the +// board connection live-mate capable. This module creates no fabrication/start grant. +type InterfaceObligation = ExactPsuPdbPairAndPinout | PsuEnableAndSense | SharingAndOring + | StandbyAndMinimumLoad | PmbusAddressAndOwnership | PerNodeFaultIsolation + | KeyedRatedNodeConnector | NodeInrushAndDischarge | GroundingAndStrainRelief + | CoolingAndInsertionEnvelope | BoardHeaderPopulation + +data server_power_interface_obligations:List = [ExactPsuPdbPairAndPinout,PsuEnableAndSense,SharingAndOring,StandbyAndMinimumLoad,PmbusAddressAndOwnership,PerNodeFaultIsolation,KeyedRatedNodeConnector,NodeInrushAndDischarge,GroundingAndStrainRelief,CoolingAndInsertionEnvelope,BoardHeaderPopulation] +type IntegrationStanding = ReferenceArchitectureOnly {open:List} +fn integration_standing() -> IntegrationStanding {ReferenceArchitectureOnly {open:server_power_interface_obligations}} +// Architecture requirements, not concrete interchangeable pinouts or mount geometry. +// A mechanical insert and an electrical adapter travel together when PSU family changes. +type CassettePowerBoundary = CommonColumnAttachment | ReplaceableMechanicalInsert + | PsuSpecificElectricalAdapter | CommonProtectedNodeInterface +data cassette_power_boundaries:List = [CommonColumnAttachment,ReplaceableMechanicalInsert,PsuSpecificElectricalAdapter,CommonProtectedNodeInterface] +type AvailabilityCase = AllModulesPresent | LargestModuleUnavailable + +// Pure capacity arithmetic conditional on an already-engineered compatible parallel +// system. Worst one-module loss removes the LARGEST module. A PDB/bus/harness limit +// remains binding in either state. This function does not authorize paralleling PSUs. +fn available_main_power(modules:List,path_limit:Watt,state:AvailabilityCase) -> Watt { + let total=fold(modules,init:0,f:(n,w)=>n+watt_count(w:w)) + let largest=fold(modules,init:0,f:(n,w)=>if watt_count(w:w)>n {watt_count(w:w) as Int} else {n}) + let remaining=match state {AllModulesPresent=>total LargestModuleUnavailable=>total-largest} + watt(count:if remaining Watt? { + let rows=filter(reference.main_ratings,r=>volt_count(v:input)>=volt_count(v:r.input_min) && volt_count(v:input)<=volt_count(v:r.input_max)) + if count(rows)!=1 {none} else {match rows.first() {Absent=>none Present {value:r}=>Present {value:watt(count:volt_count(v:reference.main_voltage)*milliampere_count(i:r.current)/1000)}}} +} +// Reserve is a caller-declared engineering allowance, not an electrical-code rule. +// overhead covers declared DC distribution/controller losses; PSU AC conversion +// loss affects the wall feed, not a second derating of its rated DC output. +fn required_station_power(node_loads:List,overhead:Watt,reserve:Percent) -> Watt { + let sum=fold(node_loads,init:watt_count(w:overhead),f:(n,w)=>n+watt_count(w:w)) + watt(count:(sum*(100+percent_count(p:reserve))+99)/100) +} +fn capacity_screen(node_loads:List,overhead:Watt,reserve:Percent,available:Watt) -> PsuAdequacy { + psu_adequacy_for_load(load:required_station_power(node_loads:node_loads,overhead:overhead,reserve:reserve),rated:available) +} +fn example(reference:ServerPowerReference,input_v:Int,each_node_w:Int) -> JsonValue { + let required=required_station_power(node_loads:[watt(count:each_node_w),watt(count:each_node_w)],overhead:watt(count:20),reserve:percent(count:20)) + let available=reference_main_power(reference:reference,input:volt(count:input_v)) + json_object(members:[json_kv(key:"reference",value:json_string(s:reference.model as String)),json_kv(key:"ac_input_v",value:json_int(n:input_v)),json_kv(key:"each_node_assumption_w",value:json_int(n:each_node_w)),json_kv(key:"required_with_20w_overhead_and_20percent_reserve_w",value:json_int(n:watt_count(w:required))),json_kv(key:"main_12v_w",value:match available {Absent=>json_null() Present {value:w}=>json_int(n:watt_count(w:w))}),json_kv(key:"capacity_screen",value:json_string(s:match available {Absent=>"input rating unresolved" Present {value:w}=>match capacity_screen(node_loads:[watt(count:each_node_w),watt(count:each_node_w)],overhead:watt(count:20),reserve:percent(count:20),available:w) {PsuAdequate {dc_load:_,rated:_,headroom:_}=>"capacity screen only; integration unresolved" PsuUndersized {dc_load:_,rated:_,shortfall:_}=>"insufficient capacity with declared reserve" PsuUndecidable {cause:_}=>"unresolved"}}))]) +} +fn server_power_review() -> JsonValue { + let standing=match integration_standing() {ReferenceArchitectureOnly {open:o}=>count(o)} + json_object(members:[json_kv(key:"standing",value:json_string(s:"reference architecture; exact S15-550P1A / S550E004L mating interface is unverified")),json_kv(key:"heterogeneous_node_pairing_example",value:json_string(s:match pairing_example() {PairingCapacityOnly=>"300 W + 200 W + 100 W assigned to independent ports; capacity only" PairingRefused=>"refused"})),json_kv(key:"modular_interface_boundaries",value:json_int(n:count(cassette_power_boundaries))),json_kv(key:"open_interface_obligations",value:json_int(n:standing)),json_kv(key:"two_550w_modules_after_largest_loss_w",value:json_int(n:watt_count(w:available_main_power(modules:[watt(count:550),watt(count:550)],path_limit:watt(count:1100),state:LargestModuleUnavailable)))),json_kv(key:"examples",value:json_array(elements:[example(reference:fsp800_50fs_reference,input_v:115,each_node_w:300),example(reference:fsp800_50fs_reference,input_v:115,each_node_w:400),example(reference:fsp1200_50fs_reference,input_v:115,each_node_w:400),example(reference:fsp1200_50fs_reference,input_v:230,each_node_w:400)]))]) +} + +// Named power cassettes and explicit branch assignments replace a fixed board:PSU ratio. +// Budgets are usable main-rail output at the selected AC input. Interface identity +// denotes a selected electrical connector/harness contract, not merely its shape. +type PowerCassetteBudget {id:NonEmptyStr,interface_id:NonEmptyStr,usable_main:Watt,branch_limits:List,overhead:Watt,reserve:Percent} +type NodePowerAssignment {node:NonEmptyStr,power_cassette:NonEmptyStr,interface_id:NonEmptyStr,branch:Int,load:Watt} +type PairingScreen = PairingCapacityOnly | PairingRefused +fn pairing_screen(supplies:List,assignments:List) -> PairingScreen { + let supply_ids_unique=all(supplies,s=>count(filter(supplies,q=>q.id==s.id))==1) + let assignments_valid=all(assignments,a=>{ + let sources=filter(supplies,s=>s.id==a.power_cassette) + let node_unique=count(filter(assignments,b=>b.node==a.node))==1 + let branch_unique=count(filter(assignments,b=>b.power_cassette==a.power_cassette && b.branch==a.branch))==1 + let compatible=match sources.first() {Absent=>false Present {value:s}=>{ + let indexed=fold(s.branch_limits,init:0,f:(n,w)=>n+1) + let port=fold(s.branch_limits,init:BranchLookup {index:0,matched:false},f:(r,w)=>BranchLookup {index:r.index+1,matched:r.matched || (r.index==a.branch && watt_count(w:a.load)<=watt_count(w:w))}) + a.interface_id==s.interface_id && a.branch>=0 && a.branchmatch capacity_screen(node_loads:map(filter(assignments,a=>a.power_cassette==s.id),a=>a.load),overhead:s.overhead,reserve:s.reserve,available:s.usable_main) {PsuAdequate {dc_load:_,rated:_,headroom:_}=>true PsuUndersized {dc_load:_,rated:_,shortfall:_}=>false PsuUndecidable {cause:_}=>false}) + if supply_ids_unique && assignments_valid && totals_fit {PairingCapacityOnly} else {PairingRefused} +} +type BranchLookup {index:Int,matched:Bool} +fn pairing_example() -> PairingScreen { + pairing_screen(supplies:[PowerCassetteBudget {id:"power-A",interface_id:"proposed-protected-12v",usable_main:watt(count:780),branch_limits:[watt(count:400),watt(count:400),watt(count:400)],overhead:watt(count:20),reserve:percent(count:20)}],assignments:[NodePowerAssignment {node:"node-A",power_cassette:"power-A",interface_id:"proposed-protected-12v",branch:0,load:watt(count:300)},NodePowerAssignment {node:"node-B",power_cassette:"power-A",interface_id:"proposed-protected-12v",branch:1,load:watt(count:200)},NodePowerAssignment {node:"node-C",power_cassette:"power-A",interface_id:"proposed-protected-12v",branch:2,load:watt(count:100)}]) +} diff --git a/dag/gunbc/product/printed_chassis/stack_geometry.dag b/dag/gunbc/product/printed_chassis/stack_geometry.dag new file mode 100644 index 00000000000..a59736acd4c --- /dev/null +++ b/dag/gunbc/product/printed_chassis/stack_geometry.dag @@ -0,0 +1,78 @@ +module product.printed_chassis.stack_geometry + +import product.printed_chassis.cooler_configuration {CoolerConfiguration,DynatronW1,NoctuaU14sAmp4926,noctua_envelope} +import std.types { Bool, Int, List } +import std.nat { Nat } +import std.affine_space { signed_frame_component_count } +import product.printed_chassis.assembly_geometry { AssemblyPart, Cassette, FixedFrame } +import product.printed_chassis.assembly_review_checks { Bounds, primitive_bounds, joined } +import product.printed_chassis.cassette { tray_w, tray_d, panel_t, printed_parts } +import product.printed_chassis.rack_assembly { hardware } + +// THE MILK-CRATE STACK'S DIMENSIONS, DERIVED FROM THE CASSETTE RATHER THAN TRANSCRIBED. +// +// The withdrawn frame proposal carried `stack_pitch: Int = 158000` as a literal. A pitch is a +// consequence: the tallest thing a cassette carries plus the lowest thing hanging under its tray +// plus the hand clearance a service action needs. Reading it off the parts means a taller cooler +// or a deeper rear socket moves the pitch, and nothing has to remember to edit a number. +// +// Y is up, as in the cassette frame; part bounds are the half-micrometre doubled bounds +// assembly_review_checks already computes, halved here once. Clearance volumes count: a service +// volume that the next tray would intrude on is a real obstruction. +// +// WHAT IS A PROPOSAL AND SAYS SO: service_clearance and the pillar section. Neither is derived +// from anything cited; both are design choices the mechanics and harness instruments consume, +// and both are the knobs a review would turn. + +fn cassette_parts() -> List { + filter(concat(printed_parts(), hardware()), p => match p.scope { Cassette => true FixedFrame => false }) +} + +fn part_bounds(p: AssemblyPart) -> Bounds? { + match p.adds.first() { + Absent => none + Present { value: first } => Present { value: fold(p.adds, init: primitive_bounds(p: first), f: fn(acc, q) { joined(a: acc, b: primitive_bounds(p: q)) }) } + } +} + +// Highest and lowest Y a part reaches in the assembly frame, micrometres. +fn part_top(p: AssemblyPart) -> Int { + match part_bounds(p: p) { Absent => 0 Present { value: b } => signed_frame_component_count(c: p.at.y) + b.hi_y / 2 } +} +fn part_bottom(p: AssemblyPart) -> Int { + match part_bounds(p: p) { Absent => 0 Present { value: b } => signed_frame_component_count(c: p.at.y) + b.lo_y / 2 } +} + +fn tallest_cassette_part_top() -> Int { + fold(cassette_parts(), init: 0, f: fn(acc, p) { let t = part_top(p: p) if t > acc { t } else { acc } }) +} +fn lowest_cassette_part_bottom() -> Int { + fold(cassette_parts(), init: 0, f: fn(acc, p) { let b = part_bottom(p: p) if b < acc { b } else { acc } }) +} + +// Hand clearance between the tallest part of one unit and the lowest hanging part of the next. +data service_clearance: Int = 20000 + +fn stack_pitch() -> Int { tallest_cassette_part_top() - lowest_cassette_part_bottom() + service_clearance } + +// The corner pillar section, a square of this side, four per cassette. A proposal; the +// mechanics instrument prices it and the pillar geometry change owns its final shape. The known +// obstruction it must resolve: the rear arm sockets occupy the rear corners below the tray. +data pillar_side: Int = 20000 +fn pillar_section_area_mm2() -> Nat { (pillar_side / 1000) * (pillar_side / 1000) } +fn pillar_height() -> Int { stack_pitch() - panel_t } + +// Half-extents of the footprint the stack stands on, for the tipping derivation. +fn footprint_half_depth() -> Int { tray_d / 2 } +fn footprint_half_width() -> Int { tray_w / 2 } + +// Preserve the W1 review while deriving the residential variant from its own envelope. +fn cassette_parts_for_cooler(cooler:CoolerConfiguration,contact_plane_um:Int) -> List { + map(cassette_parts(),p=>if p.id=="CPU" {match cooler {DynatronW1=>p NoctuaU14sAmp4926=>noctua_envelope(contact_plane_um:contact_plane_um)}} else {p}) +} +fn stack_pitch_for_cooler(cooler:CoolerConfiguration,contact_plane_um:Int) -> Int { + let parts=cassette_parts_for_cooler(cooler:cooler,contact_plane_um:contact_plane_um) + let top=fold(parts,init:0,f:(n,p)=>if part_top(p:p)>n {part_top(p:p)} else {n}) + let bottom=fold(parts,init:0,f:(n,p)=>if part_bottom(p:p) StackServiceReading { + if node_count<=0 || index_from_bottom<0 || index_from_bottom>=node_count {InvalidStackService} else { + let above=node_count-index_from_bottom-1 + if above==0 {TopUnitMayLift} else {UpperUnitsNeedIndependentSupport {count:above}} + } +} +fn join_pin_hole(y:Int) -> Primitive {round(diameter:4400,length:30000,axis:Axis3X,x:0,y:y,z:0)} +fn lower_post(sx:Int,sz:Int) -> AssemblyPart { + let inset=0-sx*9000 + let front_pad=if sz==1 {[block(w:16000,h:11000,d:24000,x:0-sx*18000,y: -9500,z:0)]} else {[]} + part(id:concat("CL",concat(to_string(sx),to_string(sz))),name:"Corner shoe and lower hollow post",kind:Printed,scope:Cassette, + adds:concat([ + block(w:post_outer_um,h:post_split_um-shoe_bottom_um,d:post_outer_um,x:0,y:(post_split_um+shoe_bottom_um)/2,z:0), + block(w:38000,h:4000,d:24000,x:inset,y: -17000,z:0), + block(w:38000,h:4000,d:24000,x:inset,y:12000,z:0), + block(w:middle_key_um,h:middle_engagement_um,d:middle_key_um,x:0,y:post_split_um+middle_engagement_um/2,z:0), + ],front_pad), + cuts:concat(if sz== -1 {map([-17000,13000],y=>block(w:14500,h:6000,d:10500,x:0-sx*24000,y:y,z:8000))} else {[]},[block(w:post_inner_um,h:100000,d:post_inner_um,x:0,y:60000,z:0), + block(w:stack_key_um+2*key_clearance_um,h:stack_key_depth_um+key_clearance_um+1000,d:stack_key_um+2*key_clearance_um,x:0,y:shoe_bottom_um+(stack_key_depth_um+key_clearance_um-1000)/2,z:0),join_pin_hole(y:post_split_um+middle_engagement_um/2)]), + x:sx*post_x_um,y:0,z:sz*post_z_um,ex:sx*45000,ey:0,ez:sz*25000,plane:LayOnXZ, + notes:"Cassette-owned clip shoe. Rear lower lip bears under socket floor with an open corner notch for its through-bolt hardware; front pad bears under the rail. Hollow 20/12 mm post. 16 mm middle key, 20 mm engagement, 0.25 mm clearance per side. Tray clipping and loaded lifting are not qualified.") +} +fn upper_post(sx:Int,sz:Int,pitch_um:Int) -> AssemblyPart { + let top=pitch_um+shoe_bottom_um + part(id:concat("CU",concat(to_string(sx),to_string(sz))),name:"Upper hollow post and stack locator",kind:Printed,scope:Cassette, + adds:[block(w:post_outer_um,h:top-post_split_um,d:post_outer_um,x:0,y:(top+post_split_um)/2,z:0),block(w:stack_key_um,h:stack_key_depth_um,d:stack_key_um,x:0,y:top+stack_key_depth_um/2,z:0)], + cuts:[block(w:middle_key_um+2*key_clearance_um,h:middle_engagement_um+key_clearance_um+1000,d:middle_key_um+2*key_clearance_um,x:0,y:post_split_um+(middle_engagement_um+key_clearance_um-1000)/2,z:0), + block(w:post_inner_um,h:top-post_split_um-30000,d:post_inner_um,x:0,y:(top+post_split_um+20000)/2,z:0),join_pin_hole(y:post_split_um+middle_engagement_um/2)], + x:sx*post_x_um,y:0,z:sz*post_z_um,ex:sx*45000,ey:45000,ez:sz*25000,plane:LayOnXZ, + notes:"Shoulder carries vertical load; the cross-pin retains the two post segments. Top key locates the next cassette shoe. Bearing area at the shoe socket and printed joint strength require qualification. Middle extraction needs independent support for every unit above.") +} +fn post_pin(sx:Int,sz:Int) -> AssemblyPart { + part(id:concat("CP",concat(to_string(sx),to_string(sz))),name:"Broad-grip post retaining pin",kind:Printed,scope:Cassette, + adds:[round(diameter:4100,length:24000,axis:Axis3X,x:0,y:0,z:0),block(w:4000,h:14000,d:10000,x:14000,y:0,z:0)],cuts:[], + x:sx*post_x_um,y:post_split_um+middle_engagement_um/2,z:sz*post_z_um,ex:sx*75000,ey:0,ez:sz*25000,plane:LayOnYZ, + notes:"Unthreaded cross-pin retains the middle joint; vertical load belongs on the shoulder. Positive retention and vibration behavior still need validation.") +} +type StackJoineryReading = StackJoineryGeometry {parts:List} | StackJoineryHeightUnsupported +fn stack_joinery(pitch_um:Int) -> StackJoineryReading { + if pitch_um<190000 || pitch_um>280000 {StackJoineryHeightUnsupported} else { + StackJoineryGeometry {parts:flat_map([-1,1],sx=>flat_map([-1,1],sz=>[lower_post(sx:sx,sz:sz),upper_post(sx:sx,sz:sz,pitch_um:pitch_um),post_pin(sx:sx,sz:sz)]))} + } +} +// Section values are geometric screens only; the keyed joints are not a continuous tube. +fn post_wall_area_mm2() -> Int {(post_outer_um*post_outer_um-post_inner_um*post_inner_um)/1000000} +fn shoe_bearing_area_um2() -> Int {post_outer_um*post_outer_um-(stack_key_um+2*key_clearance_um)*(stack_key_um+2*key_clearance_um)} diff --git a/dag/gunbc/product/printed_chassis/stack_mass_review.dag b/dag/gunbc/product/printed_chassis/stack_mass_review.dag new file mode 100644 index 00000000000..c1d9e9446fa --- /dev/null +++ b/dag/gunbc/product/printed_chassis/stack_mass_review.dag @@ -0,0 +1,35 @@ +module product.printed_chassis.stack_mass_review +import product.printed_chassis.rear_cooling {rear_opening,select_rear_fans,rear_fan_candidates,rear_cooling_json} +import product.printed_chassis.cassette {tray_w} +import product.printed_chassis.platform_projects_review {platform_projects_review} +import product.printed_chassis.cooler_configuration {residential_cooler} +import product.printed_chassis.server_power_interface {server_power_review} +import std.types {String,List,Int} +import std.measure {gram_count} +import std.affine_space {signed_frame_component_count} +import gunbc.fleet_intent {srv3_memory_population,srv4_memory_population} +import product.printed_chassis.component_mass {MassComponent,MassLedger,MassIncomplete,MassComplete,MassNeeded,VendorNominal,BoundedMass,PlanningMassRange,node_components_for_cooler,node_components,station_components,mass_ledger} +import product.printed_chassis.placed_mass_model {load_from_ledger,LowerMassEndpoint,SupportRectangle,InvalidLoadScenario,RigidBodyScreen} +import product.printed_chassis.stack_geometry {stack_pitch_for_cooler,stack_pitch,footprint_half_width,footprint_half_depth} +import extdeps.languages.json.emit {JsonValue,json_object,json_kv,json_string,json_int,json_array,json_null,serialize_json} +import gunbc.cli_wire {CliWireResponse,CliWirePrintable} +import std.process {ExitSuccess} +fn row_json(r:MassComponent) -> JsonValue { + let bounds=match r.mass {MassNeeded {reason:_}=>json_null() VendorNominal {mass:g}=>json_array(elements:[json_int(n:gram_count(g:g)),json_int(n:gram_count(g:g))]) PlanningMassRange {lower:l,upper:u,basis:_}=>json_array(elements:[json_int(n:gram_count(g:l)),json_int(n:gram_count(g:u))]) BoundedMass {lower:l,upper:u,basis:_}=>json_array(elements:[json_int(n:gram_count(g:l)),json_int(n:gram_count(g:u))])} + json_object(members:[json_kv(key:"id",value:json_string(s:r.id)),json_kv(key:"mass_evidence",value:json_string(s:match r.mass {PlanningMassRange {lower:_,upper:_,basis:_}=>"planning_estimate" VendorNominal {mass:_}=>"vendor_nominal" BoundedMass {lower:_,upper:_,basis:_}=>"bounded" MassNeeded {reason:_}=>"design_input_pending"})),json_kv(key:"mass_basis",value:json_string(s:match r.mass {PlanningMassRange {lower:_,upper:_,basis:b}=>b as String BoundedMass {lower:_,upper:_,basis:b}=>b as String VendorNominal {mass:_}=>"Vendor nominal; manufacturing tolerance not supplied" MassNeeded {reason:_}=>"Resolve from selected BOM / slicing / design allowance; no operator weighing request"})),json_kv(key:"quantity",value:json_int(n:r.quantity)),json_kv(key:"each_mass_bounds_g",value:bounds),json_kv(key:"position_um",value:json_array(elements:[json_int(n:signed_frame_component_count(c:r.at.x)),json_int(n:signed_frame_component_count(c:r.at.y)),json_int(n:signed_frame_component_count(c:r.at.z))])),json_kv(key:"position_evidence",value:json_string(s:"review placement; centre of mass not surveyed"))]) +} +fn node_json(id:String,rows:List,pitch_um:Int) -> JsonValue { + let hardware=filter(rows,r=>r.id=="BOARD" || r.id=="PROCESSOR" || r.id=="DIMMS" || r.id=="COOLER" || r.id=="F1" || r.id=="F2" || r.id=="F3" || r.id=="SSD") + let subtotal=match mass_ledger(components:hardware,measurements:[]) { + MassIncomplete {issues:_}=>json_null() + MassComplete {total:t,rows:_}=>json_object(members:[json_kv(key:"lower_g",value:json_int(n:gram_count(g:t.lower))),json_kv(key:"upper_g",value:json_int(n:gram_count(g:t.upper))),json_kv(key:"estimated_rows",value:json_int(n:t.estimated_rows)),json_kv(key:"scope",value:json_string(s:"Hardware planning subtotal only; excludes printed structure, mounting hardware, harness and power cassette. Endpoints are not certified bounds."))]) + } + + let ledger=mass_ledger(components:rows,measurements:[]) + let reading=load_from_ledger(ledger:ledger,endpoint:LowerMassEndpoint,support:SupportRectangle {half_x_um:footprint_half_width(),half_z_um:footprint_half_depth()},push_height_um:pitch_um) + json_object(members:[json_kv(key:"id",value:json_string(s:id)),json_kv(key:"hardware_planning_subtotal",value:subtotal),json_kv(key:"components",value:json_array(elements:map(rows,r=>row_json(r:r)))),json_kv(key:"load_screen",value:json_string(s:match reading {InvalidLoadScenario {reason:r}=>r as String RigidBodyScreen {mass_g:_,cg_x_um:_,cg_y_um:_,cg_z_um:_,restoring_x_positive_mn_mm:_,restoring_x_negative_mn_mm:_,restoring_z_positive_mn_mm:_,restoring_z_negative_mn_mm:_,tip_push_mn:_,tip_base_acceleration_um_s2:_,corner_reactions_mn:_,boundary:b}=>b as String}))]) +} +fn main() -> CliWireResponse { + let report=json_object(members:[json_kv(key:"schema",value:json_string(s:"gunbc.stack-mass-review.v1")),json_kv(key:"nodes",value:json_array(elements:[node_json(id:"srv3",pitch_um:stack_pitch(),rows:node_components(population:srv3_memory_population)),node_json(id:"srv4",pitch_um:stack_pitch(),rows:node_components(population:srv4_memory_population))])),json_kv(key:"residential_noctua_planning_pitch_um",value:json_int(n:stack_pitch_for_cooler(cooler:residential_cooler,contact_plane_um:30000))),json_kv(key:"residential_noctua_variant",value:node_json(id:"srv3-population-with-Noctua-design-variant",pitch_um:stack_pitch_for_cooler(cooler:residential_cooler,contact_plane_um:30000),rows:node_components_for_cooler(population:srv3_memory_population,cooler:residential_cooler,contact_plane_um:30000))),json_kv(key:"power_station",value:json_array(elements:map(station_components(),r=>row_json(r:r)))),json_kv(key:"server_power_interface",value:server_power_review()),json_kv(key:"platform_projects",value:platform_projects_review()),json_kv(key:"residential_rear_cooling",value:rear_cooling_json(reading:select_rear_fans(candidates:rear_fan_candidates,opening:rear_opening(width_um:tray_w,pitch_um:stack_pitch_for_cooler(cooler:residential_cooler,contact_plane_um:30000),left_um:20000,right_um:20000,bottom_um:20000,top_um:20000,depth_um:60000),bed_um:170000))),json_kv(key:"safe_stack_height",value:json_null())]) + CliWirePrintable {bytes:serialize_json(v:report),exit:ExitSuccess} +} diff --git a/dag/gunbc/product/printed_chassis/stack_mechanics.dag b/dag/gunbc/product/printed_chassis/stack_mechanics.dag new file mode 100644 index 00000000000..f86bf59e423 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/stack_mechanics.dag @@ -0,0 +1,242 @@ +module product.printed_chassis.stack_mechanics + +import std.types { Bool, Int, List, NonEmptyStr, String } +import std.nat { Nat } +import std.measure { + Gram, gram, gram_count, Millinewton, millinewton, millinewton_count, Kilopascal, kilopascal, kilopascal_count, + Megapascal, megapascal_count, KilogramPerCubicMetre, kilogram_per_cubic_metre_count, Percent, percent_count, + micrometer_count, MicrometrePerSecondSquared, micrometre_per_second_squared, micrometre_per_second_squared_count, + Newton, newton, newton_count, +} +import v2.std.optional { Present, Absent } +import product.printed_chassis.assembly_geometry { AssemblyPart, Primitive, Block, RoundPrism, Printed, Purchased, Metal, Clearance } +import product.printed_chassis.stack_geometry { cassette_parts, stack_pitch, pillar_side, pillar_section_area_mm2, pillar_height, footprint_half_depth, footprint_half_width } +import product.printed_chassis.power_cassette { PowerCassette, PowerCassettePlacement, TopOfColumn, BottomOfColumn, BesideColumn, nodes_above } +import extdeps.printing.fdm { FilamentTechnicalDataSheetRow } +import extdeps.cooling.dynatron { dynatron_w1_mass_active } +import extdeps.fans.arctic { arctic_p8_mass } + +// Legacy uniform-unit sensitivity screen. It is not the complete node mass ledger and +// must not be used to qualify a populated stack. component_mass folds the complete +// component inventory; placed_mass_model handles explicit load positions. +// Positive primitive volume here is only a solid-envelope estimate (not sliced mass). +// Euler buckling assumes an ideal homogeneous square column. No compressive allowable, +// printed-joint stiffness, anisotropy or long-term creep rating is supplied. +// Standard gravity is 9.80665 m/s^2. +data standard_gravity: MicrometrePerSecondSquared = micrometre_per_second_squared(count: 9806650) + +// pi to four places, used only for cylinder volumes in the mass estimate. +data pi_ten_thousandths: Nat = 31416 +data no_mass: Nat = 0 + +type MassBasis + = VendorStated + | DerivedFromModeledVolume { density: KilogramPerCubicMetre } + | DeclaredByCaller { basis: NonEmptyStr } + +type MassRow { part_id: String, mass: Gram, basis: MassBasis } + +type DeclaredMass { part_id: String, mass: Gram, basis: NonEmptyStr } + +// Volume of a part's positive solids in cubic millimetres; cuts are ignored so the figure is an +// positive-volume estimate. It is not a conservative stability input: extra mass can add restoring moment. +// Use measured/sliced masses and intervals for actual load cases. +fn primitive_volume_mm3(p: Primitive) -> Nat { + match p { + Block { size: s, center: _ } => micrometer_count(m: s.size_x) * micrometer_count(m: s.size_y) / 1000000 * micrometer_count(m: s.size_z) / 1000 + RoundPrism { diameter: d, length: l, axis: _, center: _ } => { + let dmm = micrometer_count(m: d) / 1000 + pi_ten_thousandths * dmm * dmm / 4 * (micrometer_count(m: l) / 1000) / 10000 + } + } +} +fn part_volume_mm3(p: AssemblyPart) -> Nat { fold(p.adds, init: 0, f: (acc, q) => acc + primitive_volume_mm3(p: q)) } + +// grams = mm3 * kg/m3 / 1e6 +fn printed_mass(p: AssemblyPart, sheet: FilamentTechnicalDataSheetRow) -> Gram { + gram(count: part_volume_mm3(p: p) * kilogram_per_cubic_metre_count(d: sheet.density) / 1000000) +} + +fn declared_for(declared: List, id: String) -> DeclaredMass? { + fold(declared, init: none, f: (acc, d) => if d.part_id == id { Present { value: d } } else { acc }) +} + +// The vendor rows the cassette's purchased parts have. Ids are the cassette model's own. +fn vendor_mass(id: String) -> Gram? { + if id == "CPU" { Present { value: dynatron_w1_mass_active } } + else { if id == "F1" || id == "F2" || id == "F3" { Present { value: arctic_p8_mass } } else { none } } +} + +type MassRosterReading + = MassRosterComplete { rows: List, declared_count: Nat } + | MassRosterIncomplete { unweighed: List } + +// Space reservations that stand for real mass: the board and DIMM slabs are Clearance parts in +// the cassette model because their geometry is a reservation, but they weigh what they weigh. +// Named here so the roster demands a figure for them rather than skipping them with the volumes. +data mass_bearing_reservations: List = ["BOARD", "RAM1", "RAM2"] + +fn mass_roster(sheet: FilamentTechnicalDataSheetRow, declared: List) -> MassRosterReading { + let subjects = filter(cassette_parts(), p => match p.kind { Printed => true Purchased => true Metal => true Clearance => any(mass_bearing_reservations, id => id == p.id) }) + let rows = flat_map(subjects, p => match p.kind { + Printed => [MassRow { part_id: p.id, mass: printed_mass(p: p, sheet: sheet), basis: DerivedFromModeledVolume { density: sheet.density } }] + Purchased => match vendor_mass(id: p.id) { + Present { value: m } => [MassRow { part_id: p.id, mass: m, basis: VendorStated }] + Absent => match declared_for(declared: declared, id: p.id) { Present { value: d } => [MassRow { part_id: p.id, mass: d.mass, basis: DeclaredByCaller { basis: d.basis } }] Absent => [] } + } + Metal => match declared_for(declared: declared, id: p.id) { Present { value: d } => [MassRow { part_id: p.id, mass: d.mass, basis: DeclaredByCaller { basis: d.basis } }] Absent => [] } + Clearance => match declared_for(declared: declared, id: p.id) { Present { value: d } => [MassRow { part_id: p.id, mass: d.mass, basis: DeclaredByCaller { basis: d.basis } }] Absent => [] } + }) + let unweighed = flat_map(subjects, p => if any(rows, r => r.part_id == p.id) { [] } else { [p.id] }) + if count(unweighed) > 0 { MassRosterIncomplete { unweighed: unweighed } } + else { MassRosterComplete { rows: rows, declared_count: count(filter(rows, r => match r.basis { DeclaredByCaller { basis: _ } => true VendorStated => false DerivedFromModeledVolume { density: _ } => false })) } } +} + +fn roster_mass(rows: List) -> Gram { gram(count: fold(rows, init: 0, f: (acc, r) => acc + gram_count(g: r.mass))) } + +// Pillars are not in the r06 cassette model yet; their mass is derived from the proposal section. +fn pillar_set_mass(sheet: FilamentTechnicalDataSheetRow) -> Gram { + let side_mm = pillar_side / 1000 + gram(count: 4 * side_mm * side_mm * (pillar_height() / 1000) * kilogram_per_cubic_metre_count(d: sheet.density) / 1000000) +} + +fn unit_mass(rows: List, sheet: FilamentTechnicalDataSheetRow) -> Gram { + gram(count: gram_count(g: roster_mass(rows: rows)) + gram_count(g: pillar_set_mass(sheet: sheet))) +} + +// Weight in millinewtons: g * um/s^2 = nN, / 1e6 = mN. +fn weight_mn(m: Gram) -> Millinewton { millinewton(count: gram_count(g: m) * micrometre_per_second_squared_count(a: standard_gravity) / 1000000) } + +// Declared sensitivity inputs. Bending strength is deliberately not used as a +// compressive allowable. Top push and base acceleration use different lever arms. +type StackPolicy { + effective_length_factor_percent: Nat + service_push: Newton + service_acceleration: MicrometrePerSecondSquared +} + +type StackLimit + = PillarBucklingExceeded { level_from_bottom: Nat, pillar_load: Millinewton, critical: Millinewton } + | TipsUnderServicePush { tip_force: Newton, push: Newton } + | TipsUnderServiceAcceleration { tip_acceleration: MicrometrePerSecondSquared, required: MicrometrePerSecondSquared } + +type StackVerdict { + node_count: Nat + unit_mass: Gram + column_mass: Gram + bottom_pillar_load: Millinewton + bottom_bearing_stress: Kilopascal + pillar_critical_load: Millinewton + tip_force: Newton + tip_acceleration: MicrometrePerSecondSquared + rests_on_declared_masses: Nat +} + +type StackAdmission + = StackScreeningOnly { verdict: StackVerdict, creep_unqualified: NonEmptyStr } + | StackLimited { first_limit: StackLimit, verdict: StackVerdict } + | StackRefusedUnweighed { unweighed: List } + +// Mass resting on level i's pillars: every node above it, plus the station if it is above. +fn mass_above_level(unit: Gram, station: PowerCassette, node_count: Nat, level: Nat) -> Gram { + let nodes_above = nodes_above(node_count: node_count, node_index_from_bottom: level) + let station_above = match station.placement { TopOfColumn => gram_count(g: station.mass) BottomOfColumn => no_mass BesideColumn => no_mass } + gram(count: nodes_above * gram_count(g: unit) + station_above) +} + +// Euler critical load for one pillar, in millinewtons: pi^2 E I / (K L)^2 with E in MPa (N/mm^2), +// I = a^4/12 in mm^4, L in mm, so the quotient is newtons. +fn pillar_critical_load(sheet: FilamentTechnicalDataSheetRow, policy: StackPolicy) -> Millinewton { + let a = pillar_side / 1000 + let i_mm4 = a * a * a * a / 12 + let kl = policy.effective_length_factor_percent * (pillar_height() / 1000) / 100 + let pi_squared_ten_thousandths = 98696 + millinewton(count: pi_squared_ten_thousandths * megapascal_count(p: sheet.bending_modulus) * i_mm4 / (kl * kl) / 10) +} + +// Approximate uniform-unit CG. Bottom power cassette shifts every node upward one pitch. +// Top-applied push uses the TOP HEIGHT, not this CG height. The centre +// of mass of N equal units at pitch p is at N p / 2 above the floor; a station adds its own term. +fn centre_of_mass_height_mm(unit: Gram, station: PowerCassette, node_count: Nat) -> Nat { + let p = stack_pitch() / 1000 + let offset = match station.placement {BottomOfColumn=>p TopOfColumn=>0 BesideColumn=>0} + let node_moment = node_count * gram_count(g: unit) * (node_count * p + 2 * offset) / 2 + let station_height = match station.placement { TopOfColumn => node_count * p + p / 2 BottomOfColumn => p / 2 BesideColumn => p / 2 } + let station_term = match station.placement { TopOfColumn => gram_count(g: station.mass) * station_height BottomOfColumn => gram_count(g: station.mass) * station_height BesideColumn => no_mass } + let total = match station.placement { TopOfColumn => node_count * gram_count(g: unit) + gram_count(g: station.mass) BottomOfColumn => node_count * gram_count(g: unit) + gram_count(g: station.mass) BesideColumn => node_count * gram_count(g: unit) } + if total == 0 { total } else { (node_moment + station_term) / total } +} + +fn column_mass(unit: Gram, station: PowerCassette, node_count: Nat) -> Gram { + let station_mass = match station.placement { TopOfColumn => gram_count(g: station.mass) BottomOfColumn => gram_count(g: station.mass) BesideColumn => no_mass } + gram(count: node_count * gram_count(g: unit) + station_mass) +} + +fn tip_force(unit: Gram, station: PowerCassette, node_count: Nat) -> Newton { + let station_levels = match station.placement {BesideColumn=>0 TopOfColumn=>1 BottomOfColumn=>1} + let h = (node_count + station_levels) * (stack_pitch()/1000) + let half_base = footprint_half_width() / 1000 + if h == 0 { newton(count: no_mass) } else { newton(count: millinewton_count(f: weight_mn(m: column_mass(unit: unit, station: station, node_count: node_count))) * half_base / h / 1000) } +} + +// Lateral acceleration that tips the column: g * half-base / centre-of-mass height. +fn tip_acceleration(unit: Gram, station: PowerCassette, node_count: Nat) -> MicrometrePerSecondSquared { + let h = centre_of_mass_height_mm(unit: unit, station: station, node_count: node_count) + if h == 0 { micrometre_per_second_squared(count: no_mass) } else { micrometre_per_second_squared(count: micrometre_per_second_squared_count(a: standard_gravity) * (footprint_half_width() / 1000) / h) } +} + +// Bending strength is not a compressive allowable. Bearing stress is reported, not admitted. +fn first_limit(unit: Gram, station: PowerCassette, sheet: FilamentTechnicalDataSheetRow, policy: StackPolicy, node_count: Nat) -> StackLimit? { + let per_pillar = millinewton(count: millinewton_count(f: weight_mn(m: mass_above_level(unit: unit, station: station, node_count: node_count, level: 0))) / 4) + let critical = pillar_critical_load(sheet: sheet, policy: policy) + let tip = tip_force(unit: unit, station: station, node_count: node_count) + let tip_a = tip_acceleration(unit: unit, station: station, node_count: node_count) + if millinewton_count(f: per_pillar) > millinewton_count(f: critical) { Present { value: PillarBucklingExceeded { level_from_bottom: 0, pillar_load: per_pillar, critical: critical } } } + else { if newton_count(f: tip) < newton_count(f: policy.service_push) { Present { value: TipsUnderServicePush { tip_force: tip, push: policy.service_push } } } + else { if micrometre_per_second_squared_count(a: tip_a) < micrometre_per_second_squared_count(a: policy.service_acceleration) { Present { value: TipsUnderServiceAcceleration { tip_acceleration: tip_a, required: policy.service_acceleration } } } else { none } } } +} + +fn verdict_for(unit: Gram, station: PowerCassette, sheet: FilamentTechnicalDataSheetRow, policy: StackPolicy, node_count: Nat, declared_count: Nat) -> StackVerdict { + let per_pillar = millinewton(count: millinewton_count(f: weight_mn(m: mass_above_level(unit: unit, station: station, node_count: node_count, level: 0))) / 4) + StackVerdict { + node_count: node_count, + unit_mass: unit, + column_mass: column_mass(unit: unit, station: station, node_count: node_count), + bottom_pillar_load: per_pillar, + bottom_bearing_stress: kilopascal(count: millinewton_count(f: per_pillar) / pillar_section_area_mm2()), + pillar_critical_load: pillar_critical_load(sheet: sheet, policy: policy), + tip_force: tip_force(unit: unit, station: station, node_count: node_count), + tip_acceleration: tip_acceleration(unit: unit, station: station, node_count: node_count), + rests_on_declared_masses: declared_count, + } +} + +data creep_boundary: NonEmptyStr = "SCREENING ONLY: uniform unit CG approximation, ideal Euler column, unknown joint/contact stiffness, compressive allowable, print anisotropy, creep, friction and cable forces. No result admits a populated stack or states a safe height. Use placed_mass_model for actual load locations; qualify physical joints/material separately." + +fn stack_screening(sheet: FilamentTechnicalDataSheetRow, declared: List, station: PowerCassette, policy: StackPolicy, node_count: Nat) -> StackAdmission { + match mass_roster(sheet: sheet, declared: declared) { + MassRosterIncomplete { unweighed: u } => StackRefusedUnweighed { unweighed: u } + MassRosterComplete { rows: rows, declared_count: dc } => { + let unit = unit_mass(rows: rows, sheet: sheet) + let verdict = verdict_for(unit: unit, station: station, sheet: sheet, policy: policy, node_count: node_count, declared_count: dc) + match first_limit(unit: unit, station: station, sheet: sheet, policy: policy, node_count: node_count) { + Present { value: l } => StackLimited { first_limit: l, verdict: verdict } + Absent => StackScreeningOnly { verdict: verdict, creep_unqualified: creep_boundary } + } + } + } +} + +// Largest uniform approximation passing these screens; NOT a safe stack height. +fn max_screened_height(sheet: FilamentTechnicalDataSheetRow, declared: List, station: PowerCassette, policy: StackPolicy, ceiling: Nat) -> Nat { + height_search(sheet: sheet, declared: declared, station: station, policy: policy, n: 1, ceiling: ceiling, best: 0) +} +fn height_search(sheet: FilamentTechnicalDataSheetRow, declared: List, station: PowerCassette, policy: StackPolicy, n: Nat, ceiling: Nat, best: Nat) -> Nat { + if n > ceiling { best } else { + match stack_screening(sheet: sheet, declared: declared, station: station, policy: policy, node_count: n) { + StackScreeningOnly { verdict: _, creep_unqualified: _ } => height_search(sheet: sheet, declared: declared, station: station, policy: policy, n: n + 1, ceiling: ceiling, best: n) + StackLimited { first_limit: _, verdict: _ } => best + StackRefusedUnweighed { unweighed: _ } => best + } + } +} diff --git a/dag/gunbc/product/printed_chassis/tray_panel_features.dag b/dag/gunbc/product/printed_chassis/tray_panel_features.dag new file mode 100644 index 00000000000..9f3402bcd39 --- /dev/null +++ b/dag/gunbc/product/printed_chassis/tray_panel_features.dag @@ -0,0 +1,36 @@ +module product.printed_chassis.tray_panel_features +import std.types {Int} + +// Shared feature dimensions for the retained Altra solids and the planning +// allocator. Moving a protrusion changes both its geometry and its bed envelope. +data panel_seam_um:Int = 400 +data lap_center_offset_um:Int = 9500 +data lap_along_joint_um:Int = 21000 +data rail_foot_width_um:Int = 14000 +data rail_foot_inset_um:Int = 1000 +data rail_wall_width_um:Int = 6000 +data rail_wall_offset_um:Int = 2000 +data rear_socket_width_um:Int = 27000 +data rear_socket_wall_width_um:Int = 3000 +data rear_socket_inset_um:Int = 7000 +data handle_center_from_edge_um:Int = 22000 +data handle_depth_um:Int = 10000 +data handle_support_center_from_edge_um:Int = 13000 +data handle_support_depth_um:Int = 28000 +fn donor_joint_overhang_um() -> Int {lap_center_offset_um+lap_along_joint_um/2} +fn side_rail_overhang_um() -> Int { + let foot=rail_foot_width_um/2-rail_foot_inset_um + let wall=rail_wall_width_um/2+rail_wall_offset_um + if foot>wall {foot} else {wall} +} +fn front_handle_overhang_um() -> Int { + let grip=handle_center_from_edge_um+handle_depth_um/2 + let support=handle_support_center_from_edge_um+handle_support_depth_um/2 + if grip>support {grip} else {support} +} + +fn side_panel_overhang_um() -> Int { + let socket=rear_socket_width_um/2-rear_socket_inset_um + let rail=side_rail_overhang_um() + if socket>rail {socket} else {rail} +} diff --git a/dag/gunbc/recurring_failure_mode/functional_print_features_confused_with_supports.dag b/dag/gunbc/recurring_failure_mode/functional_print_features_confused_with_supports.dag new file mode 100644 index 00000000000..b9734479ad7 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/functional_print_features_confused_with_supports.dag @@ -0,0 +1,12 @@ +module gunbc.recurring_failure_mode.functional_print_features_confused_with_supports +import std.types {NonEmptyStr} +import gunbc.recurring_failure_mode {RecurringFailureMode} + +data functional_print_features_confused_with_supports:RecurringFailureMode=RecurringFailureMode { + identity:"functional_print_features_confused_with_supports" as NonEmptyStr, + receipts:[ + "2026-10-04 cassette prototype: the operator removed both integral rear-arm sockets while clearing supports. Support material was trapped in the mating channels, and the functional walls resembled disposable supports.", + "Earliest unjustified boundary: panel geometry combined downward-facing arm channels with a broad elevated tray, then relied on manual support removal to preserve thin functional walls. The repair separates open, floor-down sockets from the tray and retains each socket/arm/tray stack with a through-bolt.", + "Standing: geometry and sliced support absence can be checked mechanically; human recognition, handling damage, bearing strength and creep remain external observations. A support-free slice alone does not qualify the connection structurally. Next trigger: inspect the revised physical assembly and record retention/load behavior before powered use.", + ],evidence:[], +} diff --git a/dag/gunbc/specification_citation_read_provenance.dag b/dag/gunbc/specification_citation_read_provenance.dag index ab9d7a38863..155ce6f111e 100644 --- a/dag/gunbc/specification_citation_read_provenance.dag +++ b/dag/gunbc/specification_citation_read_provenance.dag @@ -380,3 +380,14 @@ data bonterms_cloud_terms_v1_0_citation_read_receipt: SpecificationCitationReadR detail: "Fetched without credentials on 2026-09-12 as HTML; the tag-stripped visible text (40865 characters) carries the complete Bonterms Cloud Terms (Version 1.0), sections 1 through 22 (General Terms) plus Definitions, including Section 21 (Trials and Betas), and the footer '(c) 2022 Bonterms. Free to use under CC BY 4.0.' The page states no release date and no statement about whether the posted text stays fixed at its locator. An earlier attempt by this session at bonterms.com/forms/cloud-terms/ answered 404: that was a constructed path, and it is evidence about the path, not about the publisher.", }, } + +// Direct publisher-locator read, 2026-10-04. Portwell is the datasheet publisher; +// its dimensions remain reseller evidence about the FII/Ampere motherboard. +data portwell_collins_citation_read_receipt:SpecificationCitationReadReceipt = SpecificationCitationReadReceipt { + subject:DeclarationRef {module_path:"extdeps.portwell.aptsa_627211_b4",decl_name:"collins_datasheet_mechanicals",field:WholeDeclaration}, + provenance:DocumentLocatorFetchedAndTextIngested { + http_status:200,byte_count:byte_size(count:4916642), + content_digest:"3914b6dfb871b9eb4b317d658175de9346c8b2bfbcdfb1d541c6e74b05f9a285", + detail:"2026-10-04: downloaded the live Portwell PDF with TLS verification using the missing Let's Encrypt YR2 intermediate plus the system CA bundle; decoded all three pages. March 2022 p1 gives board 512 mm x 424.2 mm and 20.13 in x 16.7 in, and chassis 847.5 x 448 x 87.5 mm (L x W x H). Metric and inch dimensions are inconsistent; axis assignment is a downstream inference, not a labelled upstream dimension." + } +} diff --git a/dag/std/measure.dag b/dag/std/measure.dag index 05c9b1a6706..0c457d09f4f 100644 --- a/dag/std/measure.dag +++ b/dag/std/measure.dag @@ -59,6 +59,14 @@ type Quantity = | Volume | PlaneAngle + | Force + | Pressure + | VolumeFlow + | Density + | SpecificHeatCapacity + | Acceleration + | ResistancePerLength + | Dimensionless type Scale = @@ -1909,3 +1917,69 @@ fn compare_instants(a: ObservationInstant, b: ObservationInstant) -> InstantOrde if x < y { InstantBefore } else if x == y { InstantSame } else { InstantAfter } } } + +// MECHANICS AND HEAT-TRANSFER CARRIERS, added for the printed-chassis stack and exhaust models. +// Each is the SI coherent unit at Scale One with the decimal prefix that holds the cited figure +// exactly: a filament sheet states a modulus in megapascals, a fan sheet states airflow in cubic +// metres per hour that a floor conversion carries at millilitres per second, and standard gravity is +// 9.80665 m/s^2 which Micro holds without rounding. Nothing here asserts a relation between the +// quantities (force = mass x acceleration is a law the consumer applies, not a fact of the carrier). +type Newton = Measure +type Millinewton = Measure +fn newton(count: Nat) -> Newton { Measure { count: count } } +fn newton_count(f: Newton) -> Nat { measure_count(f) } +fn millinewton(count: Nat) -> Millinewton { Measure { count: count } } +fn millinewton_count(f: Millinewton) -> Nat { measure_count(f) } + +// Pressure and STRESS share the pascal: a material's strength and modulus are pressures. +type Pascal = Measure +type Kilopascal = Measure +type Megapascal = Measure +fn pascal(count: Nat) -> Pascal { Measure { count: count } } +fn pascal_count(p: Pascal) -> Nat { measure_count(p) } +fn kilopascal(count: Nat) -> Kilopascal { Measure { count: count } } +fn kilopascal_count(p: Kilopascal) -> Nat { measure_count(p) } +fn megapascal(count: Nat) -> Megapascal { Measure { count: count } } +fn megapascal_count(p: Megapascal) -> Nat { measure_count(p) } + +// VOLUME FLOW at the coherent cubic metre per second; Micro is the millilitre per second. +type CubicMetrePerSecond = Measure +type MillilitrePerSecond = Measure +fn millilitre_per_second(count: Nat) -> MillilitrePerSecond { Measure { count: count } } +fn millilitre_per_second_count(f: MillilitrePerSecond) -> Nat { measure_count(f) } + +// DENSITY at kilograms per cubic metre: 1.28 g/cm^3 is exactly 1280 kg/m^3. +type KilogramPerCubicMetre = Measure +fn kilogram_per_cubic_metre(count: Nat) -> KilogramPerCubicMetre { Measure { count: count } } +fn kilogram_per_cubic_metre_count(d: KilogramPerCubicMetre) -> Nat { measure_count(d) } + +// SPECIFIC HEAT CAPACITY at joules per kilogram-kelvin. +type JoulePerKilogramKelvin = Measure +fn joule_per_kilogram_kelvin(count: Nat) -> JoulePerKilogramKelvin { Measure { count: count } } +fn joule_per_kilogram_kelvin_count(c: JoulePerKilogramKelvin) -> Nat { measure_count(c) } + +// ACCELERATION at micrometres per second squared, so standard gravity 9.80665 m/s^2 is exact. +type MicrometrePerSecondSquared = Measure +fn micrometre_per_second_squared(count: Nat) -> MicrometrePerSecondSquared { Measure { count: count } } +fn micrometre_per_second_squared_count(a: MicrometrePerSecondSquared) -> Nat { measure_count(a) } + +// RESISTANCE PER LENGTH at microohms per metre: a wire-gauge table's 3.28 ohm/km is 3280 uohm/m. +type MicroohmPerMetre = Measure +fn microohm_per_metre(count: Nat) -> MicroohmPerMetre { Measure { count: count } } +fn microohm_per_metre_count(r: MicroohmPerMetre) -> Nat { measure_count(r) } + +// KILOGRAM, now that a consumer stores whole-kilogram counts: the stack-mechanics mass roster +// sums grams and reports kilograms at the floor. +type Kilogram = Measure +fn kilogram(count: Nat) -> Kilogram { Measure { count: count } } +fn kilogram_count(m: Kilogram) -> Nat { measure_count(m) } + +// MILLIAMPERE, for fan and connector ratings stated below one ampere. +type Milliampere = Measure +fn milliampere(count: Nat) -> Milliampere { Measure { count: count } } +fn milliampere_count(i: Milliampere) -> Nat { measure_count(i) } + +// GRAMS PER CUBIC METRE, so dry air at 1.204 kg/m^3 is exactly 1204. +type GramPerCubicMetre = Measure +fn gram_per_cubic_metre(count: Nat) -> GramPerCubicMetre { Measure { count: count } } +fn gram_per_cubic_metre_count(d: GramPerCubicMetre) -> Nat { measure_count(d) } diff --git a/dag/test/claim/cassette_assembly_witness_test.dag b/dag/test/claim/cassette_assembly_witness_test.dag new file mode 100644 index 00000000000..5f167284978 --- /dev/null +++ b/dag/test/claim/cassette_assembly_witness_test.dag @@ -0,0 +1,38 @@ +module test.claim.cassette_assembly_witness_test +import std.types {Bool,List} +import std.affine_space {signed_frame_component_count} +import product.printed_chassis.review_realization {bundle_for} +import gunbc.cli_wire {CliWirePrintable,CliWireUnprintable} +import std.process {ExitSuccess,ExitFailure} +import product.printed_chassis.assembly_geometry {AssemblyPart,part,block,Printed,Cassette,LayOnXZ,placed} +import product.printed_chassis.assembly_review_checks {review_model_checks,within_bed} +import product.printed_chassis.cassette_review {all_parts} +import product.printed_chassis.cassette {interfaces} + +fn displaced(p:AssemblyPart) -> AssemblyPart { + AssemblyPart {id:p.id,name:p.name,kind:p.kind,scope:p.scope,adds:p.adds,cuts:p.cuts,at:placed(x:signed_frame_component_count(c:p.at.x)+1000,y:signed_frame_component_count(c:p.at.y),z:signed_frame_component_count(c:p.at.z)),explode:p.explode,print_plane:p.print_plane,notes:p.notes} +} +test fn assembly_source_has_mates_and_fits_declared_bed() -> Bool {review_model_checks(parts:all_parts(),interfaces:interfaces())} +test fn moved_guard_is_not_admitted() -> Bool { + !review_model_checks(parts:map(all_parts(),p=>if p.id=="G2" {displaced(p:p)} else {p}),interfaces:interfaces()) +} +test fn missing_member_is_not_admitted() -> Bool { + !review_model_checks(parts:filter(all_parts(),p=>p.id!="M2"),interfaces:interfaces()) +} +test fn oversized_piece_is_not_admitted() -> Bool { + let p=part(id:"oversized",name:"negative control",kind:Printed,scope:Cassette,adds:[block(w:181000,h:8000,d:90000,x:0,y:0,z:0)],cuts:[],x:0,y:0,z:0,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"") + !within_bed(p:p) +} + +fn packaging_fixture() -> AssemblyPart { + part(id:"packaging-fixture",name:"packaging fixture",kind:Printed,scope:Cassette,adds:[block(w:10000,h:8000,d:10000,x:0,y:0,z:0)],cuts:[],x:0,y:0,z:0,ex:0,ey:0,ez:0,plane:LayOnXZ,notes:"") +} +// Full assembly admission is witnessed above; packaging exercises the same producer +// on a small model so this contract does not repeatedly serialize the whole rack. +test fn review_bundle_requires_the_same_admitted_source_manifest() -> Bool { + match bundle_for(parts:[packaging_fixture()],joints:[]) {CliWirePrintable {bytes:b,exit:e}=>match e {ExitSuccess=>string_contains(s:b,pattern:"packaging-fixture")&&string_contains(s:b,pattern:"Manifest differs from the emitted geometry")&&string_contains(s:b,pattern:"_review_shapes") ExitFailure {code:_,reason:_}=>false} CliWireUnprintable {cause:_}=>false} +} + +test fn review_bundle_refuses_duplicate_members() -> Bool { + match bundle_for(parts:[packaging_fixture(),packaging_fixture()],joints:[]) {CliWirePrintable {bytes:b,exit:e}=>match e {ExitSuccess=>false ExitFailure {code:_,reason:_}=>string_contains(s:b,pattern:"Assembly source checks refused")} CliWireUnprintable {cause:_}=>false} +} diff --git a/dag/test/claim/cassette_partition_witness_test.dag b/dag/test/claim/cassette_partition_witness_test.dag new file mode 100644 index 00000000000..1fc8fb91105 --- /dev/null +++ b/dag/test/claim/cassette_partition_witness_test.dag @@ -0,0 +1,41 @@ +module test.claim.cassette_partition_witness +import std.types {Bool} +import product.printed_chassis.cassette_profiles {mt_jade_profile,mt_collins_profile,gh200_p4261_profile,altra_profile} +import product.printed_chassis.cassette_layout {partition_profile,layout_for,current_layout,LayoutKnown,LayoutNeedsBoardOutline,InvalidEdgeMargin,proposed_panel_print_policy,panel_print_policy,TrayPartitionKnown,TrayPartitionInvalid,TrayPartitionNeedsOutline} +import product.printed_chassis.bed_partition {AxisPrintAllowance,allocate_axis,AxisAllocated,AxisAllocationInvalid,AxisAllocationNoFit} +test fn collins_uses_three_columns_without_exceeding_declared_print_envelopes() -> Bool { + match partition_profile(profile:mt_collins_profile,edge_margin_um:14000,policy:proposed_panel_print_policy) { + TrayPartitionKnown {partition:p}=>p.columns==3 && p.rows==4 && count(p.edges)==17 && all(p.cells,c=>c.print_width_um<=p.policy.along_width.bed_um && c.print_depth_um<=p.policy.along_depth.bed_um) && match layout_for(profile:mt_collins_profile,edge_margin_um:14000) {LayoutKnown {layout:l}=>fold(p.cells,init:0,f:(a,c)=>a+(c.x1_um-c.x0_um)*(c.z1_um-c.z0_um))==l.tray_width*l.tray_depth LayoutNeedsBoardOutline=>false InvalidEdgeMargin=>false} + TrayPartitionInvalid=>false TrayPartitionNeedsOutline=>false + } +} +test fn jade_uses_the_same_allocator_and_larger_brim_recomputes_collins() -> Bool { + let jade=match partition_profile(profile:mt_jade_profile,edge_margin_um:14000,policy:proposed_panel_print_policy) {TrayPartitionKnown {partition:p}=>p.columns==3 && p.rows==4 TrayPartitionInvalid=>false TrayPartitionNeedsOutline=>false} + let brim=match partition_profile(profile:mt_collins_profile,edge_margin_um:14000,policy:panel_print_policy(brim_each_side_um:6000,edge_clearance_each_side_um:500)) {TrayPartitionKnown {partition:p}=>p.columns==4 && p.rows==4 && all(p.cells,c=>c.print_width_um<=p.policy.along_width.bed_um && c.print_depth_um<=p.policy.along_depth.bed_um) TrayPartitionInvalid=>false TrayPartitionNeedsOutline=>false} + jade && brim +} +test fn altra_keeps_equal_four_cells_and_unknown_board_refuses() -> Bool { + let a=match partition_profile(profile:altra_profile,edge_margin_um:14000,policy:proposed_panel_print_policy) {TrayPartitionKnown {partition:p}=>count(p.cells)==4 && count(p.edges)==4 && all(p.cells,c=>c.x1_um-c.x0_um==current_layout().tray_width/2 && c.z1_um-c.z0_um==current_layout().tray_depth/2) TrayPartitionInvalid=>false TrayPartitionNeedsOutline=>false} + let u=match partition_profile(profile:gh200_p4261_profile,edge_margin_um:14000,policy:proposed_panel_print_policy) {TrayPartitionKnown {partition:_}=>false TrayPartitionInvalid=>false TrayPartitionNeedsOutline=>true} + let b=match partition_profile(profile:altra_profile,edge_margin_um:14000,policy:panel_print_policy(brim_each_side_um:90000,edge_clearance_each_side_um:0)) {TrayPartitionKnown {partition:_}=>false TrayPartitionInvalid=>true TrayPartitionNeedsOutline=>false} + a && u && b +} +test fn unequal_edge_capacity_can_fit_when_uniform_cells_cannot() -> Bool { + let a=AxisPrintAllowance {bed_um:100,brim_each_side_um:0,edge_clearance_each_side_um:0,outer_low_um:30,outer_high_um:10,internal_low_um:0,internal_high_um:20} + match allocate_axis(span_um:210,allowance:a) {AxisAllocated {cells:c}=>count(c)==3 && map(c,x=>x.end_um-x.start_um)==[50,80,80] && all(c,x=>x.occupied_with_process_um<=a.bed_um) AxisAllocationInvalid=>false AxisAllocationNoFit=>false} +} +test fn rounding_conserves_span_and_unused_joint_does_not_prevent_single_piece() -> Bool { + let flat=AxisPrintAllowance {bed_um:100,brim_each_side_um:0,edge_clearance_each_side_um:0,outer_low_um:0,outer_high_um:0,internal_low_um:0,internal_high_um:0} + let rounded=match allocate_axis(span_um:251,allowance:flat) {AxisAllocated {cells:c}=>map(c,x=>x.end_um-x.start_um)==[83,84,84] && map(c,x=>x.start_um)==[0,83,167] AxisAllocationInvalid=>false AxisAllocationNoFit=>false} + let unused=AxisPrintAllowance {bed_um:100,brim_each_side_um:0,edge_clearance_each_side_um:0,outer_low_um:0,outer_high_um:0,internal_low_um:200,internal_high_um:200} + let one=match allocate_axis(span_um:100,allowance:unused) {AxisAllocated {cells:c}=>count(c)==1 AxisAllocationInvalid=>false AxisAllocationNoFit=>false} + let split=match allocate_axis(span_um:101,allowance:unused) {AxisAllocated {cells:_}=>false AxisAllocationInvalid=>false AxisAllocationNoFit=>true} + rounded && one && split +} + +test fn invalid_input_and_submicrometre_only_split_are_refused() -> Bool { + let a=AxisPrintAllowance {bed_um:10,brim_each_side_um:0,edge_clearance_each_side_um:0,outer_low_um:5,outer_high_um:5,internal_low_um:0,internal_high_um:0} + let invalid=match allocate_axis(span_um:0,allowance:a) {AxisAllocationInvalid=>true AxisAllocated {cells:_}=>false AxisAllocationNoFit=>false} + let zero_width=match allocate_axis(span_um:1,allowance:a) {AxisAllocationNoFit=>true AxisAllocated {cells:_}=>false AxisAllocationInvalid=>false} + invalid && zero_width +} diff --git a/dag/test/claim/cassette_platform_projects_witness_test.dag b/dag/test/claim/cassette_platform_projects_witness_test.dag new file mode 100644 index 00000000000..b7c888e9e6f --- /dev/null +++ b/dag/test/claim/cassette_platform_projects_witness_test.dag @@ -0,0 +1,33 @@ +module test.claim.cassette_platform_projects_witness +import std.types {Bool} +import std.measure {micrometer_count} +import extdeps.portwell.aptsa_627211_b4 {collins_datasheet_mechanicals} +import v2.std.optional {Present,Absent} +import product.printed_chassis.cassette_profile {board_grid_tiles,RetainPlatformPowerAssembly,IndependentPowerCassette} +import product.printed_chassis.cassette_profiles {cassette_profiles,altra_profile,gh200_p4261_profile,mt_jade_profile,mt_collins_profile} +import product.printed_chassis.cassette_layout {layout_for,current_layout,LayoutKnown,LayoutNeedsBoardOutline,InvalidEdgeMargin} +test fn unknown_board_refuses_and_collins_uses_its_own_provisional_outline() -> Bool { + let gh=match board_grid_tiles(project:gh200_p4261_profile,usable_tile_um:180000) {Absent=>true Present {value:_}=>false} + let mc=match layout_for(profile:mt_collins_profile,edge_margin_um:0) {LayoutKnown {layout:l}=>l.board_width==micrometer_count(m:collins_datasheet_mechanicals.second_board_dimension) && l.board_depth==micrometer_count(m:collins_datasheet_mechanicals.first_board_dimension) LayoutNeedsBoardOutline=>false InvalidEdgeMargin=>false} + gh && mc +} +test fn jade_requires_segmentation_and_invalid_bed_refuses() -> Bool { + let tiles=match board_grid_tiles(project:mt_jade_profile,usable_tile_um:180000) {Present {value:n}=>n==9 Absent=>false} + let invalid=match board_grid_tiles(project:mt_jade_profile,usable_tile_um:0) {Absent=>true Present {value:_}=>false} + tiles && invalid +} +test fn transplant_projects_retain_platform_power_and_open_obligations() -> Bool { + count(cassette_profiles)==4 && all(filter(cassette_profiles,p=>p.id!="altrad8ud"),p=>count(p.open)==12 && count(p.retain)==8 && match p.power {RetainPlatformPowerAssembly=>true IndependentPowerCassette=>false}) +} + +test fn one_layout_kernel_preserves_altra_and_derives_jade() -> Bool { + let old=current_layout() + let a=match layout_for(profile:altra_profile,edge_margin_um:14000) {LayoutKnown {layout:l}=>l.tray_width==old.tray_width && l.tray_depth==old.tray_depth _=>false} + let j=match layout_for(profile:mt_jade_profile,edge_margin_um:14000) {LayoutKnown {layout:l}=>l.tray_width==456000 && l.tray_depth==507360 _=>false} + a && j +} +test fn unknown_profile_and_invalid_allowance_cannot_emit_a_layout() -> Bool { + let gh=match layout_for(profile:gh200_p4261_profile,edge_margin_um:14000) {LayoutNeedsBoardOutline=>true _=>false} + let invalid=match layout_for(profile:altra_profile,edge_margin_um: -1) {InvalidEdgeMargin=>true _=>false} + gh && invalid +} diff --git a/dag/test/claim/cassette_provisional_mechanicals_witness_test.dag b/dag/test/claim/cassette_provisional_mechanicals_witness_test.dag new file mode 100644 index 00000000000..26111a8b3ea --- /dev/null +++ b/dag/test/claim/cassette_provisional_mechanicals_witness_test.dag @@ -0,0 +1,66 @@ +module test.claim.cassette_provisional_mechanicals_witness +import std.types {Bool} +import std.measure {micrometer,micrometer_count} +import product.printed_chassis.cassette_profiles {mt_collins_profile,mt_jade_profile,altra_profile,gh200_p4261_profile} +import product.printed_chassis.cassette_profile {CassetteProfile,BoardOutlineKnown,BoardOutlinePending} +import product.printed_chassis.cassette_layout {partition_layout,layout_from_outline,proposed_panel_print_policy,TrayPartitionKnown,TrayPartitionInvalid,TrayPartitionNeedsOutline} +import product.printed_chassis.board_mechanical_reference {jade_image_mount_reading,jade_raster_calibration,CandidateMountPixel,image_from_left_um,image_from_front_um,placement_admitted,mounting_reference_json,MountingReferencePending,ImageLocationReference,StandardLocationReference} +import product.printed_chassis.platform_projects_review {platform_projects_review} +import extdeps.portwell.aptsa_627211_b4 {collins_datasheet_mechanicals} +import extdeps.external_authority {CitedToAuthority,TranscribedUncited} +import extdeps.boards.asrock_rack {HoleCorrespondenceInferred,HolePositionEstimatedFromImage,HolePhysicallyConfirmedPresent,standoff_placement,StandoffAdmitted,StandoffProhibited} +import extdeps.languages.json.emit {serialize_json,JsonValue,JsonArray,JsonObject,JsonNull,JsonBool,JsonNumber,JsonString} +import extdeps.languages.json.parse {json_field,json_field_string,json_field_bool,FieldRead,FieldAbsent,FieldMalformed} + +test fn collins_outline_joins_upstream_dimensions_and_evidence() -> Bool { + let r=collins_datasheet_mechanicals + match mt_collins_profile.outline { + BoardOutlinePending=>false + BoardOutlineKnown {width:w,depth:d,standing:s}=>w==r.second_board_dimension && d==r.first_board_dimension && s==r.standing && micrometer_count(m:w)micrometer_count(m:r.chassis_width) + } +} +test fn unverified_image_position_refuses_while_known_correspondence_admits() -> Bool { + !placement_admitted(standing:jade_image_mount_reading.standing) && !placement_admitted(standing:HolePositionEstimatedFromImage) && placement_admitted(standing:HoleCorrespondenceInferred) && placement_admitted(standing:HolePhysicallyConfirmedPresent) +} +test fn calibration_maps_its_datums_to_upstream_outline_without_claiming_board_containment() -> Bool { + let c=jade_raster_calibration + let front=CandidateMountPixel {id:"front-datum",x:c.left_px,y:c.front_px} + let rear=CandidateMountPixel {id:"rear-datum",x:c.right_px,y:c.rear_px} + image_from_left_um(c:c,p:front)==0 && image_from_front_um(c:c,p:front)==0 && image_from_left_um(c:c,p:rear)==micrometer_count(m:c.width) && image_from_front_um(c:c,p:rear)==micrometer_count(m:c.depth) +} +test fn renamed_profile_retains_its_mounting_reference() -> Bool { + let p=mt_jade_profile + let renamed=CassetteProfile {id:"new-display-id",requested_hardware:p.requested_hardware,outline:p.outline,mounting:p.mounting,power:p.power,retain:p.retain,open:p.open} + serialize_json(v:mounting_reference_json(reference:renamed.mounting))==serialize_json(v:mounting_reference_json(reference:p.mounting)) +} +fn kind_is(v:JsonValue,wanted:String) -> Bool { + match json_field_string(obj:v,key:"kind") {FieldRead {value:s}=>s==wanted FieldAbsent=>false FieldMalformed {cause:_}=>false} +} +test fn reference_emitter_distinguishes_altra_jade_and_unresolved_profiles() -> Bool { + kind_is(v:mounting_reference_json(reference:altra_profile.mounting),wanted:"standard-location-correspondence") && kind_is(v:mounting_reference_json(reference:mt_jade_profile.mounting),wanted:"image-derived-candidates") && kind_is(v:mounting_reference_json(reference:mt_collins_profile.mounting),wanted:"pending") && kind_is(v:mounting_reference_json(reference:gh200_p4261_profile.mounting),wanted:"pending") +} +fn has_reference(v:JsonValue) -> Bool { + match json_field(obj:v,key:"mounting_reference") {FieldRead {value:m}=>match json_field_string(obj:m,key:"kind") {FieldRead {value:_}=>true FieldAbsent=>false FieldMalformed {cause:_}=>false} FieldAbsent=>false FieldMalformed {cause:_}=>false} +} +test fn whole_review_emits_mounting_evidence_for_every_profile() -> Bool { + match platform_projects_review() {JsonArray {elements:e}=>count(e)>0 && all(e,p=>has_reference(v:p)) JsonObject {members:_}=>false JsonNull=>false JsonBool {value:_}=>false JsonNumber {lexeme:_}=>false JsonString {value:_}=>false} +} +fn columns_for(width:Int) -> Int { + match partition_layout(layout:layout_from_outline(width_um:width,depth_um:200000,edge_margin_um:14000),policy:proposed_panel_print_policy) {TrayPartitionKnown {partition:p}=>p.columns TrayPartitionInvalid=>0 TrayPartitionNeedsOutline=>0} +} +test fn partition_threshold_is_visible_at_one_micrometre_not_hidden_by_rounding() -> Bool { + let a=proposed_panel_print_policy.along_width + let process=2*(a.brim_each_side_um+a.edge_clearance_each_side_um) + let boundary=3*(a.bed_um-process)-a.outer_low_um-a.outer_high_um-2*a.internal_low_um-2*a.internal_high_um-2*14000 + columns_for(width:boundary)==3 && columns_for(width:boundary+1)==4 +} + +fn refuses_in_wire(v:JsonValue) -> Bool { + match json_field_bool(obj:v,key:"standoff_admitted") {FieldRead {value:b}=>!b FieldAbsent=>false FieldMalformed {cause:_}=>false} +} +test fn review_cannot_silently_promote_image_candidates_to_standoffs() -> Bool { + match json_field(obj:mounting_reference_json(reference:mt_jade_profile.mounting),key:"candidates") { + FieldRead {value:v}=>match v {JsonArray {elements:e}=>count(e)>0 && all(e,p=>refuses_in_wire(v:p)) JsonObject {members:_}=>false JsonNull=>false JsonBool {value:_}=>false JsonNumber {lexeme:_}=>false JsonString {value:_}=>false} + FieldAbsent=>false FieldMalformed {cause:_}=>false + } +} diff --git a/dag/test/claim/cassette_rear_cooling_witness_test.dag b/dag/test/claim/cassette_rear_cooling_witness_test.dag new file mode 100644 index 00000000000..6c228218104 --- /dev/null +++ b/dag/test/claim/cassette_rear_cooling_witness_test.dag @@ -0,0 +1,22 @@ +module test.claim.cassette_rear_cooling_witness +import std.types {Bool} +import std.measure {micrometer_count} +import product.printed_chassis.rear_cooling {RearOpening,RearFanGeometry,RearCoolingInvalidInput,RearCoolingNoFit,select_rear_fans,rear_fan_candidates,rear_mount} +import product.printed_chassis.fan_mount {FanMountAdmitted,FanMountBoreReachesMountingHoles,FanMountBoreNotEnclosedByPlate,FanMountPlateSmallerThanPattern} +test fn tall_narrow_opening_selects_one_140mm_without_widening_tray() -> Bool { + match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:231840,height_um:190000,depth_um:60000},bed_um:170000) { + RearFanGeometry {bank:b}=>b.count==1 && micrometer_count(m:b.fan.pattern.frame_edge)==140000 && b.centres_x_um==[0] && match rear_mount(bank:b) {FanMountAdmitted {geometry:_}=>true FanMountBoreReachesMountingHoles {bore_radius:_,hole_center_distance:_}=>false FanMountBoreNotEnclosedByPlate {bore_diameter:_,plate_edge:_}=>false FanMountPlateSmallerThanPattern {plate_edge:_,required_edge:_}=>false} + RearCoolingInvalidInput=>false RearCoolingNoFit=>false + } +} +test fn height_and_width_both_constrain_standard_fan_selection() -> Bool { + let short=match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:310000,height_um:132000,depth_um:60000},bed_um:170000) {RearFanGeometry {bank:b}=>b.count==2 && micrometer_count(m:b.fan.pattern.frame_edge)==120000 RearCoolingInvalidInput=>false RearCoolingNoFit=>false} + let narrow=match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:104000,height_um:190000,depth_um:60000},bed_um:170000) {RearFanGeometry {bank:b}=>b.count==1 && micrometer_count(m:b.fan.pattern.frame_edge)==92000 RearCoolingInvalidInput=>false RearCoolingNoFit=>false} + short && narrow +} +test fn inadequate_depth_and_bed_refuse_and_bad_opening_is_distinct() -> Bool { + let depth=match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:310000,height_um:190000,depth_um:59999},bed_um:170000) {RearCoolingNoFit=>true RearFanGeometry {bank:_}=>false RearCoolingInvalidInput=>false} + let bed=match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:310000,height_um:190000,depth_um:60000},bed_um:91999) {RearCoolingNoFit=>true RearFanGeometry {bank:_}=>false RearCoolingInvalidInput=>false} + let invalid=match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:0,height_um:190000,depth_um:60000},bed_um:170000) {RearCoolingInvalidInput=>true RearCoolingNoFit=>false RearFanGeometry {bank:_}=>false} + depth && bed && invalid +} diff --git a/dag/test/claim/cassette_residential_joinery_witness_test.dag b/dag/test/claim/cassette_residential_joinery_witness_test.dag new file mode 100644 index 00000000000..a77be9ad5d6 --- /dev/null +++ b/dag/test/claim/cassette_residential_joinery_witness_test.dag @@ -0,0 +1,39 @@ +module test.claim.cassette_residential_joinery_witness +import std.types {Bool} +import std.affine_space {signed_frame_component_count} +import product.printed_chassis.cassette {printed_parts,tray_w} +import product.printed_chassis.rear_cooling {select_rear_fans,rear_fan_candidates,RearOpening,RearFanGeometry,RearCoolingInvalidInput,RearCoolingNoFit} +import product.printed_chassis.rear_fan_assembly {rear_assembly,RearAssemblyGeometry,RearAssemblyUnsupportedBank,RearAssemblyMountRefused} +import product.printed_chassis.stack_joinery {stack_joinery,StackJoineryGeometry,StackJoineryHeightUnsupported,stack_service,TopUnitMayLift,UpperUnitsNeedIndependentSupport,InvalidStackService,post_wall_area_mm2,shoe_bearing_area_um2} +import product.printed_chassis.assembly_review_checks {within_bed,primitive_positive,review_model_checks,primitive_bounds} +import product.printed_chassis.assembly_geometry {Printed,Purchased,Metal,Clearance} +test fn rear_bank_has_bed_sized_interfaces_and_separate_socket_retention() -> Bool { + match select_rear_fans(candidates:rear_fan_candidates,opening:RearOpening {width_um:tray_w-40000,height_um:190000,depth_um:60000},bed_um:170000) { + RearCoolingInvalidInput=>false RearCoolingNoFit=>false + RearFanGeometry {bank:b}=>match rear_assembly(bank:b) { + RearAssemblyUnsupportedBank=>false RearAssemblyMountRefused=>false + RearAssemblyGeometry {parts:p,interfaces:i}=>{ + let tray=filter(printed_parts(),p=>p.id=="P1"||p.id=="P2"||p.id=="P3"||p.id=="P4") + count(p)==13 && review_model_checks(parts:concat(tray,p),interfaces:i) && any(i,j=>j.id=="fan-and-guard-140"&&j.quantity==2) && any(i,j=>j.id=="rear-arm-sockets"&&j.quantity==2) && any(p,q=>q.id=="S1") && any(p,q=>q.id=="S2") && !any(p,q=>q.id=="K1"||q.id=="K2") + } + } + } +} +test fn posts_fit_bed_and_invalid_height_cannot_produce_solids() -> Bool { + let valid=match stack_joinery(pitch_um:230000) {StackJoineryHeightUnsupported=>false StackJoineryGeometry {parts:p}=>count(p)==12 && all(p,q=>within_bed(p:q) && all(concat(q.adds,q.cuts),a=>primitive_positive(p:a)))} + let low=match stack_joinery(pitch_um:189999) {StackJoineryHeightUnsupported=>true StackJoineryGeometry {parts:_}=>false} + let high=match stack_joinery(pitch_um:280001) {StackJoineryHeightUnsupported=>true StackJoineryGeometry {parts:_}=>false} + valid && low && high && post_wall_area_mm2()==256 && shoe_bearing_area_um2()==127750000 +} +test fn middle_pop_needs_support_and_top_pop_does_not_borrow_it() -> Bool { + let middle=match stack_service(node_count:4,index_from_bottom:1) {UpperUnitsNeedIndependentSupport {count:n}=>n==2 TopUnitMayLift=>false InvalidStackService=>false} + let top=match stack_service(node_count:4,index_from_bottom:3) {TopUnitMayLift=>true UpperUnitsNeedIndependentSupport {count:_}=>false InvalidStackService=>false} + let invalid=match stack_service(node_count:4,index_from_bottom:4) {InvalidStackService=>true TopUnitMayLift=>false UpperUnitsNeedIndependentSupport {count:_}=>false} + middle && top && invalid +} + +// Actual panel producers must no longer grow the support-generating hanging socket. +test fn rear_panels_have_no_geometry_below_the_rail_feet() -> Bool { + let panels=filter(printed_parts(),p=>p.id=="P1"||p.id=="P2") + count(panels)==2 && all(panels,p=>all(p.adds,q=>primitive_bounds(p:q).lo_y>= -8000)) +} diff --git a/dag/test/claim/fit_gauges_witness_test.dag b/dag/test/claim/fit_gauges_witness_test.dag new file mode 100644 index 00000000000..05f96d247a2 --- /dev/null +++ b/dag/test/claim/fit_gauges_witness_test.dag @@ -0,0 +1,35 @@ +module test.claim.fit_gauges_witness_test + +import std.types { Bool } +import product.printed_chassis.fit_gauges { follow_on_fit_gauge_pairs, fit_gauge_admission, fit_gauge_source, FitGaugeSourceReady, FitGaugeSourceRefused } +import product.printed_chassis.standoff_gauge { + GaugeAdmitted, GaugeLocationNotEnumerated, GaugeLocationDuplicated, + GaugeLocationsCoincide, GaugeExceedsBuildEnvelope, +} +import extdeps.standards.micro_atx { LocJ, LocS } + +test fn w_every_follow_on_gauge_fits_the_actual_printer_envelope() -> Bool { + (follow_on_fit_gauge_pairs |> length) > 0 && + fold(follow_on_fit_gauge_pairs, init: true, f: fn(ok, pair) { + ok && match fit_gauge_admission(pair: pair) { + GaugeAdmitted { geometry: _ } => true, + GaugeLocationNotEnumerated { requested: _ } => false, + GaugeLocationDuplicated { requested: _, count: _ } => false, + GaugeLocationsCoincide { requested: _ } => false, + GaugeExceedsBuildEnvelope { fit: _ } => false, + } + }) +} + +test fn w_no_follow_on_gauge_asks_for_an_excluded_or_unobserved_board_hole() -> Bool { + fold(follow_on_fit_gauge_pairs, init: true, f: fn(ok, pair) { + ok && pair.a != LocJ && pair.b != LocJ && pair.a != LocS && pair.b != LocS + }) +} + +test fn w_unknown_pair_cannot_emit_a_default_print() -> Bool { + match fit_gauge_source(pair: "C-J") { + FitGaugeSourceRefused { cause: _ } => true, + FitGaugeSourceReady { source: _ } => false, + } +} diff --git a/dag/test/claim/gigabyte_mp72_board_profile_witness_test.dag b/dag/test/claim/gigabyte_mp72_board_profile_witness_test.dag index 21a7a7901a8..d669aab1481 100644 --- a/dag/test/claim/gigabyte_mp72_board_profile_witness_test.dag +++ b/dag/test/claim/gigabyte_mp72_board_profile_witness_test.dag @@ -2,15 +2,19 @@ module test.claim.gigabyte_mp72_board_profile_witness import std.nat { Nat } import std.measure { square_millimeter_count, watt_count } +import extdeps.boards.types { + BaseboardPhysicalProfile, + PowerInputPopulationStanding, PowerInputPopulationSpecified, PowerInputMinimumOneStated, PowerInputPopulationUnresolved, + board_profile_power_headers_match, board_profile_area, board_profile_max_socket_power, +} import extdeps.boards.gigabyte { - BaseboardPhysicalProfile, BoardProcurementStanding, + BoardProcurementStanding, gigabyte_mp72_hb0_catalog, gigabyte_mp72_hb0_physical_profile, - asrock_altrad8ud_1l2t_physical_profile, gigabyte_mp72_hb0_procurement_standing, gigabyte_mp72_hb0_purchase_gate, - PowerInputPopulationStanding, PowerInputPopulationSpecified, PowerInputPopulationUnresolved, +} +import extdeps.boards.asrock_rack { + asrock_altrad8ud_1l2t_physical_profile, asrock_altrad8ud_required_input_population, - board_profile_power_headers_match, board_profile_area, - board_profile_max_socket_power, } // Three witnesses over a CANDIDATE board row that no fleet asset cites. They exist to keep a @@ -41,15 +45,18 @@ test fn witness_board_power_interfaces_differ() -> Bool { ) } -// How many of the three 8-pin inputs must be populated is UNRESOLVED, and the model must keep -// saying so. A cable diagram that draws a specific number of connected inputs before a vendor -// statement or fixture measurement closes this would repeat the exact mistake above: a plausible -// number rendered as a fact. +// How many of the three 8-pin inputs must be populated was UNRESOLVED until the manual was read +// first-party on 2026-10-04. Section 2.8 states that under the lowest power supply any ONE of +// ATX12V1/2/3 may be used, and states no rule above that. So the standing is a stated minimum of +// one with the higher-power population left to a connector-rating derivation downstream, and the +// witness pins exactly that grade: not Specified, which the manual does not say, and no longer +// Unresolved, which would discard what it does say. -test fn witness_required_input_population_stays_unresolved() -> Bool { +test fn witness_required_input_population_is_a_stated_minimum_of_one() -> Bool { match asrock_altrad8ud_required_input_population { PowerInputPopulationSpecified { required_inputs } => false, - PowerInputPopulationUnresolved { available_inputs } => available_inputs == 3, + PowerInputMinimumOneStated { available_inputs, stated_condition } => available_inputs == 3, + PowerInputPopulationUnresolved { available_inputs } => false, } } diff --git a/dag/test/claim/printed_chassis_component_mass_witness_test.dag b/dag/test/claim/printed_chassis_component_mass_witness_test.dag new file mode 100644 index 00000000000..de2f9caceb3 --- /dev/null +++ b/dag/test/claim/printed_chassis_component_mass_witness_test.dag @@ -0,0 +1,67 @@ +module test.claim.printed_chassis_component_mass_witness +import std.types {Bool,Int,List} +import std.measure {gram,gram_count} +import extdeps.memory.types {HostMemoryPopulation,MemoryPopulationRow} +import extdeps.memory.samsung {m393a8g40d40_crb0q_catalog} +import product.printed_chassis.assembly_geometry {placed} +import product.printed_chassis.component_mass { + MassComponent,MassEvidence,MassNeeded,BoardWithSocket,BoundedMass,VendorNominal, + lower_mass,upper_mass,MassMeasurement,MassLedger,MassComplete,MassIncomplete,MassIssue,DuplicateMeasurement, + UnknownMeasurement,MissingMass,EnvelopeCentre,PlanningMassRange,component,mass_ledger,node_components,dimm_count, +} +import product.printed_chassis.placed_mass_model {PlacedMass,SupportRectangle,LoadScenario,LoadScreen,RigidBodyScreen,InvalidLoadScenario,placed_load_screen,load_from_ledger,LowerMassEndpoint} +fn population() -> HostMemoryPopulation {HostMemoryPopulation {rows:[MemoryPopulationRow {module:m393a8g40d40_crb0q_catalog,stick_count:8}]}} +fn a() -> MassComponent {component(id:"A",quantity:8,mass:MassNeeded {reason:BoardWithSocket},x:0,y:1000,z:0,position:EnvelopeCentre)} +fn measurement() -> MassMeasurement {MassMeasurement {id:"A",lower:gram(count:10),upper:gram(count:12),basis:"analytic fixture; not a server mass"}} +test fn quantities_and_mass_bounds_fold_without_zeroing_missing_components() -> Bool { + let missing=match mass_ledger(components:[a()],measurements:[]) {MassIncomplete {issues:i}=>count(i)==1 MassComplete {total:_,rows:_}=>false} + let total=match mass_ledger(components:[a()],measurements:[measurement()]) {MassComplete {total:t,rows:_}=>gram_count(g:t.lower)==80 && gram_count(g:t.upper)==96 && t.components==8 && t.provisional_positions==1 MassIncomplete {issues:_}=>false} + missing && total +} +test fn duplicate_and_stale_measurements_refuse() -> Bool { + let duplicate=match mass_ledger(components:[a()],measurements:[measurement(),measurement()]) {MassIncomplete {issues:i}=>any(i,e=>match e {DuplicateMeasurement {id:_}=>true _=>false}) _=>false} + let stale=match mass_ledger(components:[a()],measurements:[MassMeasurement {id:"REMOVED_PART",lower:gram(count:1),upper:gram(count:1),basis:"fixture"}]) {MassIncomplete {issues:i}=>any(i,e=>match e {UnknownMeasurement {id:_}=>true _=>false}) _=>false} + duplicate && stale +} +test fn complete_inventory_includes_previously_omitted_loads_and_uses_population() -> Bool { + let rows=node_components(population:population()) + dimm_count(population:population())==8 && count(filter(rows,r=>r.id=="COOLER"))==1 + && !any(rows,r=>r.id=="CPU" || r.id=="RAM1" || r.id=="RAM2") + && any(rows,r=>r.id=="PROCESSOR") && any(rows,r=>r.id=="SSD") + && any(rows,r=>r.id=="DIMMS" && r.quantity==8) + && any(rows,r=>r.id=="NODE_HARNESS") && any(rows,r=>r.id=="BOARD_MOUNTING") + && any(rows,r=>r.id=="CORNER_STRUCTURE") + && (match mass_ledger(components:rows,measurements:[]) {MassIncomplete {issues:i}=>count(i)>3 _=>false}) +} +fn scenario(x:Int,height:Int,push_height:Int) -> LoadScenario { + LoadScenario {components:[PlacedMass {id:"fixture",mass:gram(count:1000),at:placed(x:x,y:height,z:0)}],support:SupportRectangle {half_x_um:100000,half_z_um:100000},push_height_um:push_height} +} +test fn top_push_and_base_acceleration_have_different_lever_arms() -> Bool { + match placed_load_screen(case:scenario(x:0,height:100000,push_height:200000)) { + RigidBodyScreen {mass_g:m,cg_x_um:_,cg_y_um:_,cg_z_um:_,restoring_x_positive_mn_mm:_,restoring_x_negative_mn_mm:_,restoring_z_positive_mn_mm:_,restoring_z_negative_mn_mm:_,tip_push_mn:f,tip_base_acceleration_um_s2:a,corner_reactions_mn:c,boundary:_}=>m==1000 && f==4903 && a==9806650 && all(c,n=>n==2451) + _=>false} +} +test fn offcentre_load_changes_corner_reactions_and_tipping() -> Bool { + match placed_load_screen(case:scenario(x:50000,height:100000,push_height:200000)) { + RigidBodyScreen {mass_g:_,cg_x_um:x,cg_y_um:_,cg_z_um:_,restoring_x_positive_mn_mm:p,restoring_x_negative_mn_mm:n,restoring_z_positive_mn_mm:_,restoring_z_negative_mn_mm:_,tip_push_mn:f,tip_base_acceleration_um_s2:a,corner_reactions_mn:c,boundary:_}=>x==50000 && n>p && f==2451 && a==4903325 && any(c,v=>v==1225) && any(c,v=>v==3677) + _=>false} +} +test fn outside_support_has_no_positive_tip_margin() -> Bool { + match placed_load_screen(case:scenario(x:150000,height:100000,push_height:200000)) { + RigidBodyScreen {mass_g:_,cg_x_um:_,cg_y_um:_,cg_z_um:_,restoring_x_positive_mn_mm:_,restoring_x_negative_mn_mm:_,restoring_z_positive_mn_mm:_,restoring_z_negative_mn_mm:_,tip_push_mn:f,tip_base_acceleration_um_s2:a,corner_reactions_mn:c,boundary:_}=>f<0 && a<0 && any(c,v=>v<0) + _=>false} +} + +test fn online_mass_inputs_replace_operator_weighing_requests() -> Bool { + let rows=node_components(population:population()) + let acquired=filter(rows,r=>r.id=="BOARD" || r.id=="PROCESSOR" || r.id=="DIMMS") + match mass_ledger(components:acquired,measurements:[]) { + MassComplete {total:t,rows:_}=>t.estimated_rows==2 && t.nominal_vendor_rows==1 + && gram_count(g:t.lower)==1184 && gram_count(g:t.upper)==2614 + _=>false + } +} +test fn unfinished_prints_still_refuse_without_requesting_hardware_measurements() -> Bool { + let ledger=mass_ledger(components:node_components(population:population()),measurements:[]) + match load_from_ledger(ledger:ledger,endpoint:LowerMassEndpoint,support:SupportRectangle {half_x_um:100000,half_z_um:100000},push_height_um:200000) {InvalidLoadScenario {reason:_}=>true _=>false} +} diff --git a/dag/test/claim/printed_chassis_exhaust_thermal_witness_test.dag b/dag/test/claim/printed_chassis_exhaust_thermal_witness_test.dag new file mode 100644 index 00000000000..c5852e7432d --- /dev/null +++ b/dag/test/claim/printed_chassis_exhaust_thermal_witness_test.dag @@ -0,0 +1,47 @@ +module test.claim.printed_chassis_exhaust_thermal_witness +import product.printed_chassis.exhaust_thermal {measured_mount_screen,InstalledThermalUnresolved,MeasuredMountExceedsScreen,MeasuredMountBelowScreenOnly,installed_thermal_without_measurement,fan_inlet_screen,FanInletExceedsCatalogAmbient,FanInletWithinCatalogAmbient} +import extdeps.printing.sunlu_filament {sunlu_high_speed_matte_petg,sunlu_hdt_tolerance} +import std.types { Bool } +import std.measure { watt, percent, celsius, celsius_count, celsius_delta, celsius_delta_count } +import product.printed_chassis.exhaust_thermal { ExhaustCase, assumed_air_margin, ThermalMargin, AssumedAirBelowHdt, AssumedAirMarginInsufficient, NoAirflow, worst_inlet, processor_junction_limit } +import extdeps.printing.bambu_lab_filament { bambu_pla_basic_tds, bambu_petg_hf_tds } + +fn case(heat_w: Nat, fans: Nat, free_air_fraction: Nat) -> ExhaustCase { + ExhaustCase { node_heat: watt(count: heat_w), fans: fans, free_air_fraction: percent(count: free_air_fraction), inlet: worst_inlet(), margin_required: celsius_delta(count: 10) } +} + +// Hypothetical catalog free-air flow, not installed flow: single-digit rise, PLA Basic's +// 57 C HDT stays more than ten kelvin away, and the exhaust is nowhere near the 100 C die +// limit the question was framed around. +test fn assumed_free_air_case_has_bulk_air_margin_only() -> Bool { + match assumed_air_margin(case: case(heat_w: 300, fans: 3, free_air_fraction: 100), sheet: bambu_pla_basic_tds) { + AssumedAirBelowHdt { exhaust: e, hdt: _, margin: m } => celsius_count(c: e) < 45 && celsius_delta_count(delta: m) >= 10 && celsius_count(c: e) < celsius_count(c: processor_junction_limit()) + AssumedAirMarginInsufficient { exhaust: _, hdt: _, excess: _ } => false + NoAirflow => false + } +} + +// The same heat at twenty percent free_air_fraction is a different answer: the rise is tens of kelvin and +// PLA reaches its HDT. The fan floor is a safety input, and the instrument says so by refusing. +test fn low_free_air_fraction_with_full_load_reaches_the_hdt() -> Bool { + match assumed_air_margin(case: case(heat_w: 300, fans: 3, free_air_fraction: 20), sheet: bambu_pla_basic_tds) { + AssumedAirMarginInsufficient { exhaust: e, hdt: h, excess: x } => celsius_count(c: e) + 10 > celsius_count(c: h) && celsius_delta_count(delta: x) > 0 + AssumedAirBelowHdt { exhaust: _, hdt: _, margin: _ } => false + NoAirflow => false + } +} + +test fn petg_carries_more_margin_than_pla_and_no_fans_is_a_refusal() -> Bool { + let pla = match assumed_air_margin(case: case(heat_w: 300, fans: 3, free_air_fraction: 70), sheet: bambu_pla_basic_tds) { AssumedAirBelowHdt { exhaust: _, hdt: _, margin: m } => celsius_delta_count(delta: m) AssumedAirMarginInsufficient { exhaust: _, hdt: _, excess: _ } => 0 NoAirflow => 0 } + let petg = match assumed_air_margin(case: case(heat_w: 300, fans: 3, free_air_fraction: 70), sheet: bambu_petg_hf_tds) { AssumedAirBelowHdt { exhaust: _, hdt: _, margin: m } => celsius_delta_count(delta: m) AssumedAirMarginInsufficient { exhaust: _, hdt: _, excess: _ } => 0 NoAirflow => 0 } + petg > pla && pla > 0 + && (match assumed_air_margin(case: case(heat_w: 300, fans: 0, free_air_fraction: 100), sheet: bambu_pla_basic_tds) { NoAirflow => true AssumedAirBelowHdt { exhaust: _, hdt: _, margin: _ } => false AssumedAirMarginInsufficient { exhaust: _, hdt: _, excess: _ } => false }) +} + + +test fn unmeasured_mount_cannot_pass_and_100c_exceeds_petg_screen() -> Bool { + let unknown=match installed_thermal_without_measurement() {InstalledThermalUnresolved {required:_}=>true _=>false} + let hot=match measured_mount_screen(mount:celsius(count:100),lower_hdt:celsius(count:celsius_count(c:sunlu_high_speed_matte_petg.heat_deflection_at_0_45_mpa)-celsius_delta_count(delta:sunlu_hdt_tolerance)),margin:celsius_delta(count:10)) {MeasuredMountExceedsScreen {mount:_,screen:_}=>true _=>false} + unknown && hot && fan_inlet_screen(measured:celsius(count:43))==FanInletExceedsCatalogAmbient + && fan_inlet_screen(measured:celsius(count:35))==FanInletWithinCatalogAmbient +} diff --git a/dag/test/claim/printed_chassis_power_facts_witness_test.dag b/dag/test/claim/printed_chassis_power_facts_witness_test.dag new file mode 100644 index 00000000000..a569808a84e --- /dev/null +++ b/dag/test/claim/printed_chassis_power_facts_witness_test.dag @@ -0,0 +1,65 @@ +module test.claim.printed_chassis_power_facts_witness +import std.types { Bool, List } +import std.nat { Nat } +import std.measure { + celsius_count, millivolt_count, microohm_per_metre_count, millilitre_per_second_count, gram_count, ampere_count, +} +import v2.std.optional { Present, Absent } +import extdeps.boards.asrock_rack { + asrock_altrad8ud_power_input_requirement, BoardPowerInputMode, DcIn12V, AtxPsu, + BoardPowerConnectorKind, Atx12V8Pin, MicroFitAtx4PinSignal, + PowerInputRequirement, InputRequired, InputRequiredViaBundledConverter, InputNotUsed, + asrock_altrad8ud_atx12v_twelve_volt_pins, asrock_altrad8ud_atx4pin_signal, MicroFit4PinSignal, PsOnActiveLow, AtxPwrOk, SignalGround, Atx5Vsb, + asrock_altrad8ud_connector_edge, BoardEdge, RearIoEdge, FrontEdge, SideEdgeNearDimmBankA, SideEdgeNearPcieSlots, + asrock_altrad8ud_manual_held, asrock_altrad8ud_operating_ambient_max, +} +import extdeps.standards.awg { awg_copper_resistance } +import extdeps.standards.atx12v_psdg { atx12v_twelve_volt_regulation } +import extdeps.printing.bambu_lab_filament { bambu_pla_basic_tds, bambu_petg_hf_tds } +import extdeps.fans.arctic { arctic_p8_pwm_sharing, PwmSharingChain, PstPassThrough, NoPassThrough, arctic_p8_published_airflow, arctic_p8_mass } +import extdeps.cooling.dynatron { dynatron_w1_mass_active, dynatron_w1_mass_passive, dynatron_w1_fan_duty_points } +import extdeps.connector.molex_mini_fit_jr { mini_fit_jr_standard_terminal_rating, mini_fit_jr_hcs_terminal_rating } +import extdeps.physics.dry_air { dry_air_at_20c } + +// The manual's 2.8 table, cell by cell: DC-IN needs the 8-pin and not the 4-pin; ATX needs both, +// the 4-pin through the bundled converter. +test fn dc_in_uses_the_8_pin_and_not_the_4_pin_signal() -> Bool { + (match asrock_altrad8ud_power_input_requirement(mode: DcIn12V, kind: Atx12V8Pin) { InputRequired => true InputRequiredViaBundledConverter => false InputNotUsed => false }) + && (match asrock_altrad8ud_power_input_requirement(mode: DcIn12V, kind: MicroFitAtx4PinSignal) { InputRequired => false InputRequiredViaBundledConverter => false InputNotUsed => true }) + && (match asrock_altrad8ud_power_input_requirement(mode: AtxPsu, kind: MicroFitAtx4PinSignal) { InputRequired => false InputRequiredViaBundledConverter => true InputNotUsed => false }) +} + +// Four 12 V contacts per 8-pin input, pins 5-8; PSON# is pin 4 and pin 5 is not a signal pin. +test fn eight_pin_carries_four_twelve_volt_contacts_and_the_signal_pinout_is_total() -> Bool { + count(asrock_altrad8ud_atx12v_twelve_volt_pins) == 4 + && (match asrock_altrad8ud_atx4pin_signal(pin: 4) { Present { value: v } => match v { PsOnActiveLow => true AtxPwrOk => false SignalGround => false Atx5Vsb => false } Absent => false }) + && (match asrock_altrad8ud_atx4pin_signal(pin: 5) { Present { value: _ } => false Absent => true }) +} + +// The fact the harness inherits: power and fan headers are on the FRONT edge, PANEL1 is not. +test fn power_and_fan_headers_sit_on_the_front_edge() -> Bool { + let front = fn(d) { match asrock_altrad8ud_connector_edge(designator: d) { Present { value: e } => match e { FrontEdge => true RearIoEdge => false SideEdgeNearDimmBankA => false SideEdgeNearPcieSlots => false } Absent => false } } + front("ATX12V1") && front("ATX12V2") && front("ATX12V3") && front("FAN1") && front("FAN5") + && !front("PANEL1") + && (match asrock_altrad8ud_connector_edge(designator: "NOPE") { Present { value: _ } => false Absent => true }) +} + +test fn wire_table_answers_listed_gauges_and_refuses_others() -> Bool { + (match awg_copper_resistance(gauge: 10) { Present { value: r } => microohm_per_metre_count(r: r) == 3280 Absent => false }) + && (match awg_copper_resistance(gauge: 18) { Present { value: r } => microohm_per_metre_count(r: r) > 20000 Absent => false }) + && (match awg_copper_resistance(gauge: 11) { Present { value: _ } => false Absent => true }) +} + +test fn cited_rows_are_internally_ordered() -> Bool { + millivolt_count(v: atx12v_twelve_volt_regulation.minimum) < millivolt_count(v: atx12v_twelve_volt_regulation.nominal) + && millivolt_count(v: atx12v_twelve_volt_regulation.nominal) < millivolt_count(v: atx12v_twelve_volt_regulation.maximum) + && celsius_count(c: bambu_petg_hf_tds.heat_deflection_at_0_45_mpa) > celsius_count(c: bambu_pla_basic_tds.heat_deflection_at_0_45_mpa) + && celsius_count(c: bambu_pla_basic_tds.heat_deflection_at_0_45_mpa) > celsius_count(c: asrock_altrad8ud_operating_ambient_max) + && gram_count(g: dynatron_w1_mass_active) > gram_count(g: dynatron_w1_mass_passive) + && gram_count(g: dynatron_w1_mass_active) > gram_count(g: arctic_p8_mass) + && count(dynatron_w1_fan_duty_points) == 3 + && ampere_count(a: mini_fit_jr_hcs_terminal_rating.rated_current_per_circuit) > ampere_count(a: mini_fit_jr_standard_terminal_rating.rated_current_per_circuit) + && millilitre_per_second_count(f: arctic_p8_published_airflow) > 0 + && asrock_altrad8ud_manual_held.page_count == 95 + && (match arctic_p8_pwm_sharing { PstPassThrough { max_chained_standing: _ } => true NoPassThrough => false }) +} diff --git a/dag/test/claim/printed_chassis_server_power_interface_witness_test.dag b/dag/test/claim/printed_chassis_server_power_interface_witness_test.dag new file mode 100644 index 00000000000..dce15b761ea --- /dev/null +++ b/dag/test/claim/printed_chassis_server_power_interface_witness_test.dag @@ -0,0 +1,84 @@ +module test.claim.printed_chassis_server_power_interface_witness +import std.types {Bool,Int,NonEmptyStr} +import product.printed_chassis.server_power_interface {PowerCassetteBudget,NodePowerAssignment,PairingCapacityOnly,PairingRefused,pairing_screen,pairing_example} +import extdeps.cooling.noctua_ampere {nh_u14s_amp_4926} +import product.printed_chassis.cooler_configuration {DynatronW1,NoctuaU14sAmp4926,noctua_envelope} +import product.printed_chassis.stack_geometry {stack_pitch,stack_pitch_for_cooler,part_top,part_bottom} +import product.printed_chassis.component_mass {node_components,node_components_for_cooler,VendorNominal} +import gunbc.fleet_intent {srv3_memory_population} +import std.measure {watt,watt_count,volt,percent,gram_count,micrometer_count} +import v2.std.optional {Present,Absent} +import extdeps.power.fsp_crps {fsp800_50fs_reference,fsp1200_50fs_reference} +import product.printed_chassis.server_power_interface {reference_main_power,required_station_power,available_main_power,AllModulesPresent,LargestModuleUnavailable,integration_standing,ReferenceArchitectureOnly} + +test fn main_rail_excludes_other_rails() -> Bool { + match reference_main_power(reference:fsp800_50fs_reference,input:volt(count:115)) {Present {value:w}=>watt_count(w:w)==780 Absent=>false} +} +test fn low_line_derating_is_preserved() -> Bool { + let low=match reference_main_power(reference:fsp1200_50fs_reference,input:volt(count:115)) {Present {value:w}=>watt_count(w:w)==966 Absent=>false} + let high=match reference_main_power(reference:fsp1200_50fs_reference,input:volt(count:230)) {Present {value:w}=>watt_count(w:w)==1164 Absent=>false} + low && high +} +test fn undocumented_input_range_refuses() -> Bool { + match reference_main_power(reference:fsp1200_50fs_reference,input:volt(count:150)) {Present {value:_}=>false Absent=>true} +} +test fn two_nodes_require_combined_load_and_declared_reserve() -> Bool { + watt_count(w:required_station_power(node_loads:[watt(count:300),watt(count:300)],overhead:watt(count:20),reserve:percent(count:20)))==744 + && watt_count(w:required_station_power(node_loads:[watt(count:400),watt(count:400)],overhead:watt(count:20),reserve:percent(count:20)))==984 + && watt_count(w:required_station_power(node_loads:[watt(count:300),watt(count:300)],overhead:watt(count:0),reserve:percent(count:0)))>550 +} +test fn redundancy_is_not_double_capacity() -> Bool { + watt_count(w:available_main_power(modules:[watt(count:550),watt(count:550)],path_limit:watt(count:1100),state:LargestModuleUnavailable))==550 + && watt_count(w:available_main_power(modules:[watt(count:550)],path_limit:watt(count:1100),state:LargestModuleUnavailable))==0 +} +test fn largest_loss_and_distribution_limit_both_bind() -> Bool { + watt_count(w:available_main_power(modules:[watt(count:500),watt(count:800)],path_limit:watt(count:1000),state:LargestModuleUnavailable))==500 + && watt_count(w:available_main_power(modules:[watt(count:500),watt(count:800)],path_limit:watt(count:400),state:AllModulesPresent))==400 + && watt_count(w:available_main_power(modules:[],path_limit:watt(count:400),state:LargestModuleUnavailable))==0 +} +test fn capacity_does_not_discharge_integration_obligations() -> Bool { + match integration_standing() {ReferenceArchitectureOnly {open:o}=>count(o)==11} +} + +test fn residential_envelope_uses_both_fans_and_contact_plane() -> Bool { + let p=noctua_envelope(contact_plane_um:30000) + part_top(p:p)==195000 && part_bottom(p:p)==30000 + && micrometer_count(m:nh_u14s_amp_4926.width)==150000 + && micrometer_count(m:nh_u14s_amp_4926.depth)==111000 +} +test fn cooler_variant_changes_pitch_without_rewriting_w1() -> Bool { + stack_pitch_for_cooler(cooler:DynatronW1,contact_plane_um:30000)==stack_pitch() + && stack_pitch_for_cooler(cooler:NoctuaU14sAmp4926,contact_plane_um:30000)>stack_pitch() + && stack_pitch_for_cooler(cooler:NoctuaU14sAmp4926,contact_plane_um:40000)-stack_pitch_for_cooler(cooler:NoctuaU14sAmp4926,contact_plane_um:30000)==10000 +} +test fn cooler_replacement_does_not_double_count_its_fans() -> Bool { + let old=node_components(population:srv3_memory_population) + let rows=node_components_for_cooler(population:srv3_memory_population,cooler:NoctuaU14sAmp4926,contact_plane_um:30000) + count(rows)==count(old) && count(filter(rows,r=>r.id=="COOLER"))==1 + && all(filter(rows,r=>r.id=="COOLER"),r=>match r.mass {VendorNominal {mass:g}=>gram_count(g:g)==1136 _=>false}) + && count(filter(rows,r=>r.id=="F1" || r.id=="F2" || r.id=="F3"))==3 +} + +fn test_supply() -> PowerCassetteBudget {PowerCassetteBudget {id:"power",interface_id:"12v-a",usable_main:watt(count:780),branch_limits:[watt(count:400),watt(count:400)],overhead:watt(count:20),reserve:percent(count:20)}} +fn assigned(node:NonEmptyStr,source:NonEmptyStr,interface_id:NonEmptyStr,branch:Int,watts:Int) -> NodePowerAssignment {NodePowerAssignment {node:node,power_cassette:source,interface_id:interface_id,branch:branch,load:watt(count:watts)}} +test fn mixed_node_wattages_share_one_power_cassette() -> Bool {match pairing_example() {PairingCapacityOnly=>true PairingRefused=>false}} +test fn pairing_rejects_missing_source_and_interface_mismatch() -> Bool { + let a=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"n",source:"missing",interface_id:"12v-a",branch:0,watts:100)]) {PairingRefused=>true _=>false} + let b=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"n",source:"power",interface_id:"wrong",branch:0,watts:100)]) {PairingRefused=>true _=>false} + a && b +} +test fn pairing_rejects_branch_and_aggregate_overload() -> Bool { + let a=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"n",source:"power",interface_id:"12v-a",branch:0,watts:401)]) {PairingRefused=>true _=>false} + let b=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"a",source:"power",interface_id:"12v-a",branch:0,watts:400),assigned(node:"b",source:"power",interface_id:"12v-a",branch:1,watts:400)]) {PairingRefused=>true _=>false} + a && b +} +test fn pairing_rejects_reused_ports_and_multiple_feeds_to_one_node() -> Bool { + let a=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"a",source:"power",interface_id:"12v-a",branch:0,watts:100),assigned(node:"b",source:"power",interface_id:"12v-a",branch:0,watts:100)]) {PairingRefused=>true _=>false} + let b=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"a",source:"power",interface_id:"12v-a",branch:0,watts:100),assigned(node:"a",source:"power",interface_id:"12v-a",branch:1,watts:100)]) {PairingRefused=>true _=>false} + a && b +} +test fn pairing_rejects_duplicate_supplies_and_missing_ports() -> Bool { + let a=match pairing_screen(supplies:[test_supply(),test_supply()],assignments:[]) {PairingRefused=>true _=>false} + let b=match pairing_screen(supplies:[test_supply()],assignments:[assigned(node:"a",source:"power",interface_id:"12v-a",branch:2,watts:100)]) {PairingRefused=>true _=>false} + a && b +} diff --git a/dag/test/claim/printed_chassis_stack_feed_witness_test.dag b/dag/test/claim/printed_chassis_stack_feed_witness_test.dag new file mode 100644 index 00000000000..779a3d37c13 --- /dev/null +++ b/dag/test/claim/printed_chassis_stack_feed_witness_test.dag @@ -0,0 +1,125 @@ +module test.claim.printed_chassis_stack_feed_witness +import std.types { Bool, List } +import std.nat { Nat } +import std.measure { watt, millivolt, millivolt_count, millimeter, millimeter_count, milliampere_count, percent, gram, watt_count } +import v2.std.optional { Present, Absent } +import product.printed_chassis.node_power_feed { + NodeFeedInputs, declared_connector_derating, node_feed, NodeFeedReading, NodeFeedScreeningOnly, NodeFeedRefused, + FeedRefusalCause, LoadExceedsBoardInputs, PopulationRuleUnresolved, BranchGaugeUnknown, ConnectorVoltageBelowBand, SetpointAboveBand, + fan_bank_header_load, FanHeaderLoad, FanHeaderLoadAgainstUnreadLimit, FanBankNeedsOneHeaderPerFan, +} +import product.printed_chassis.power_cassette { + PowerCassette, TopOfColumn, BottomOfColumn, BesideColumn, station_adequacy, units_lifted_to_pop, station_wall_draw, +} +import product.node_power_envelope { Gold, PsuAdequacy, PsuAdequate, PsuUndersized, PsuUndecidable } +import product.printed_chassis.harness_census { HarnessCensus, HarnessPlan, InletAtRear, InletAtFront, harness_census, disconnect_sets_to_pop, CableRole, DcBranch, Atx12vPigtail, FanHeaderCable, FanPstLink, EthernetPatch } +import extdeps.connector.molex_mini_fit_jr { mini_fit_jr_standard_terminal_rating } +import extdeps.standards.atx12v_psdg { atx12v_twelve_volt_regulation } + +fn inputs(load_w: Nat, gauge: Nat, length_mm: Nat, setpoint_mv: Nat) -> NodeFeedInputs { + NodeFeedInputs { + node_dc_load: watt(count: load_w), + connector_rating: mini_fit_jr_standard_terminal_rating, + derating: declared_connector_derating(retained: percent(count: 60)), + branch_gauge_awg: gauge, + branch_length: millimeter(count: length_mm), + station_setpoint: millivolt(count: setpoint_mv), + regulation: atx12v_twelve_volt_regulation, + } +} + +// 300 W over 10 AWG at 1.5 m from a 12.2 V setpoint: 26.316 A at the 11.4 V floor, one input at 60 percent of 24 A +// cannot carry it, two can; the round-trip drop is a few hundred millivolts and the connector +// stays inside the band. +test fn a_300_watt_node_takes_two_inputs_and_stays_in_band() -> Bool { + match node_feed(inputs: inputs(load_w: 300, gauge: 10, length_mm: 1500, setpoint_mv: 12200)) { + NodeFeedScreeningOnly { feed: f } => f.inputs_populated == 2 && milliampere_count(i: f.node_current) == 26316 + && millivolt_count(v: f.at_connector) >= millivolt_count(v: atx12v_twelve_volt_regulation.minimum) + && millivolt_count(v: f.branch_round_trip_drop) > 0 + NodeFeedRefused { cause: _ } => false + } +} + +// The same node over 18 AWG at 3 m lands below 11.40 V at the connector: a typed refusal +// naming the drop, not a quietly undervolted board. +test fn a_thin_long_branch_refuses_below_the_band() -> Bool { + match node_feed(inputs: inputs(load_w: 300, gauge: 18, length_mm: 3000, setpoint_mv: 12000)) { + NodeFeedScreeningOnly { feed: _ } => false + NodeFeedRefused { cause: c } => match c { ConnectorVoltageBelowBand { at_connector: v, band_minimum: m, drop: _ } => millivolt_count(v: v) < millivolt_count(v: m) LoadExceedsBoardInputs { required_inputs: _, available_inputs: _, per_input_limit: _ } => false PopulationRuleUnresolved => false BranchGaugeUnknown { gauge: _ } => false SetpointAboveBand { setpoint: _, band_maximum: _ } => false } + } +} + +test fn a_load_beyond_three_inputs_and_an_unknown_gauge_each_refuse_by_name() -> Bool { + let over = match node_feed(inputs: inputs(load_w: 1500, gauge: 10, length_mm: 500, setpoint_mv: 12000)) { + NodeFeedScreeningOnly { feed: _ } => false + NodeFeedRefused { cause: c } => match c { LoadExceedsBoardInputs { required_inputs: r, available_inputs: a, per_input_limit: _ } => r > a && a == 3 ConnectorVoltageBelowBand { at_connector: _, band_minimum: _, drop: _ } => false PopulationRuleUnresolved => false BranchGaugeUnknown { gauge: _ } => false SetpointAboveBand { setpoint: _, band_maximum: _ } => false } + } + let gauge = match node_feed(inputs: inputs(load_w: 300, gauge: 11, length_mm: 500, setpoint_mv: 12000)) { + NodeFeedScreeningOnly { feed: _ } => false + NodeFeedRefused { cause: c } => match c { BranchGaugeUnknown { gauge: g } => g == 11 LoadExceedsBoardInputs { required_inputs: _, available_inputs: _, per_input_limit: _ } => false ConnectorVoltageBelowBand { at_connector: _, band_minimum: _, drop: _ } => false PopulationRuleUnresolved => false SetpointAboveBand { setpoint: _, band_maximum: _ } => false } + } + let high = match node_feed(inputs: inputs(load_w: 300, gauge: 10, length_mm: 500, setpoint_mv: 12700)) { + NodeFeedScreeningOnly { feed: _ } => false + NodeFeedRefused { cause: c } => match c { SetpointAboveBand { setpoint: _, band_maximum: _ } => true LoadExceedsBoardInputs { required_inputs: _, available_inputs: _, per_input_limit: _ } => false ConnectorVoltageBelowBand { at_connector: _, band_minimum: _, drop: _ } => false PopulationRuleUnresolved => false BranchGaugeUnknown { gauge: _ } => false } + } + over && gauge && high +} + +test fn the_fan_bank_reaches_one_header_against_an_unread_limit() -> Bool { + match fan_bank_header_load(fans: 3) { + FanHeaderLoadAgainstUnreadLimit { header_current: i, fans: n } => n == 3 && milliampere_count(i: i) == 270 + FanBankNeedsOneHeaderPerFan { fans: _, headers_available: _ } => false + } +} + +fn station(placement_top: Bool) -> PowerCassette { + PowerCassette { nodes_fed: 4, placement: if placement_top { TopOfColumn } else { BottomOfColumn }, supply_rated_output: watt(count: 1600), supply_efficiency: Gold, mass: gram(count: 4000) } +} + +test fn a_station_is_adequate_for_four_nodes_and_not_for_eight() -> Bool { + let four = match station_adequacy(station: station(placement_top: false), node_dc_loads: [watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300)]) { PsuAdequate { dc_load: _, rated: _, headroom: h } => watt_count(w: h) == 400 PsuUndersized { dc_load: _, rated: _, shortfall: _ } => false PsuUndecidable { cause: _ } => false } + let eight = match station_adequacy(station: station(placement_top: false), node_dc_loads: [watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300), watt(count: 300)]) { PsuAdequate { dc_load: _, rated: _, headroom: _ } => false PsuUndersized { dc_load: _, rated: _, shortfall: s } => watt_count(w: s) == 800 PsuUndecidable { cause: _ } => false } + four && eight && watt_count(w: station_wall_draw(station: station(placement_top: false), node_dc_loads: [watt(count: 900)])) == 1000 +} + +// A station on top is lifted by every pop; at the bottom by none. Popping the bottom node of +// four lifts three nodes either way. +test fn station_placement_decides_what_a_pop_lifts() -> Bool { + units_lifted_to_pop(station: station(placement_top: true), node_count: 4, node_index_from_bottom: 0) == 4 + && units_lifted_to_pop(station: station(placement_top: false), node_count: 4, node_index_from_bottom: 0) == 3 + && units_lifted_to_pop(station: station(placement_top: false), node_count: 4, node_index_from_bottom: 3) == 0 +} + +fn plan(top: Bool, front: Bool, allowance: Nat) -> HarnessPlan { + HarnessPlan { node_count: 4, station: station(placement_top: top), inlet: if front { InletAtFront } else { InletAtRear }, inputs_populated: 2, fans_per_node: 3, ethernet_per_node: 2, switch_level_from_bottom: 4, service_allowance: millimeter(count: allowance) } +} + +// Four nodes: per node one branch, two pigtails, two patch leads, one fan header cable and two +// PST links, so five runs and eight cables per node, 20 runs and 32 cables in all. The rear fan offset makes the lead too short with either 100 mm +// or 150 mm allowance, and the census says which. +test fn the_census_counts_every_cable_and_prices_the_fan_lead() -> Bool { + let c = harness_census(plan: plan(top: false, front: false, allowance: 100)) + let tight = harness_census(plan: plan(top: false, front: false, allowance: 150)) + c.cable_count == 32 && count(c.runs) == 20 && millimeter_count(m: c.total_length) > 0 + && c.fan_header_extension_needed && tight.fan_header_extension_needed + && disconnect_sets_to_pop(plan: plan(top: true, front: false, allowance: 100), node: 0) == 5 + && disconnect_sets_to_pop(plan: plan(top: false, front: false, allowance: 100), node: 0) == 4 +} + +// Moving the inlet to the front moves the tray crossing from the pigtail to the branch; the +// total copper does not vanish, it moves, and the census shows the move. +test fn inlet_placement_moves_copper_rather_than_removing_it() -> Bool { + let rear = harness_census(plan: plan(top: false, front: false, allowance: 100)) + let front = harness_census(plan: plan(top: false, front: true, allowance: 100)) + role_length(c: rear, pigtail: true) > role_length(c: front, pigtail: true) && role_length(c: front, pigtail: false) > role_length(c: rear, pigtail: false) +} + +fn role_length(c: HarnessCensus, pigtail: Bool) -> Nat { + fold(filter(c.runs, r => match r.role { Atx12vPigtail => pigtail DcBranch => !pigtail FanHeaderCable => false FanPstLink => false EthernetPatch => false }), init: 0, f: (acc, r) => acc + r.count * millimeter_count(m: r.each)) +} + +test fn zero_connector_derating_cannot_admit_a_feed() -> Bool { + let i=inputs(load_w:300,gauge:10,length_mm:1500,setpoint_mv:12200) + let invalid=NodeFeedInputs {node_dc_load:i.node_dc_load,connector_rating:i.connector_rating,derating:declared_connector_derating(retained:percent(count:0)),branch_gauge_awg:i.branch_gauge_awg,branch_length:i.branch_length,station_setpoint:i.station_setpoint,regulation:i.regulation} + match node_feed(inputs:invalid) {NodeFeedRefused {cause:PopulationRuleUnresolved}=>true _=>false} +} diff --git a/dag/test/claim/printed_chassis_stack_mechanics_witness_test.dag b/dag/test/claim/printed_chassis_stack_mechanics_witness_test.dag new file mode 100644 index 00000000000..8fb549db42f --- /dev/null +++ b/dag/test/claim/printed_chassis_stack_mechanics_witness_test.dag @@ -0,0 +1,108 @@ +module test.claim.printed_chassis_stack_mechanics_witness +import std.types { Bool, List, String } +import std.nat { Nat } +import std.measure { gram, gram_count, watt, newton, newton_count, percent, millinewton_count, kilopascal_count, micrometre_per_second_squared, micrometre_per_second_squared_count } +import v2.std.optional { Present, Absent } +import product.printed_chassis.stack_mechanics { + DeclaredMass, StackPolicy, tip_acceleration, tip_force, stack_screening, StackAdmission, StackScreeningOnly, StackLimited, StackRefusedUnweighed, + StackLimit, PillarBucklingExceeded, TipsUnderServicePush, TipsUnderServiceAcceleration, max_screened_height, mass_roster, MassRosterReading, MassRosterComplete, MassRosterIncomplete, +} +import product.printed_chassis.stack_geometry { stack_pitch, pillar_height, tallest_cassette_part_top, lowest_cassette_part_bottom } +import product.printed_chassis.power_cassette { PowerCassette, TopOfColumn, BottomOfColumn, BesideColumn } +import product.node_power_envelope { Gold } +import extdeps.printing.bambu_lab_filament { bambu_pla_basic_tds, bambu_petg_hf_tds } + +fn station(top: Bool) -> PowerCassette { + PowerCassette { nodes_fed: 4, placement: if top { TopOfColumn } else { BottomOfColumn }, supply_rated_output: watt(count: 1600), supply_efficiency: Gold, mass: gram(count: 4000) } +} + +// DECLARED, not measured: the board and DIMM masses no vendor publishes. These are the +// witness's hypotheses and the admission reports that it rests on three of them. +fn declared() -> List { + [DeclaredMass { part_id: "BOARD", mass: gram(count: 1200), basis: "witness hypothesis: typical deep micro-ATX server board; weigh the held board to retire" }, + DeclaredMass { part_id: "RAM1", mass: gram(count: 200), basis: "witness hypothesis: four RDIMMs at 50 g; weigh to retire" }, + DeclaredMass { part_id: "RAM2", mass: gram(count: 200), basis: "witness hypothesis: four RDIMMs at 50 g; weigh to retire" }] +} + +// A 0.02 g bump criterion is lax enough that only the push criterion can bind in the tests +// below; policy_g names the bump criterion in hundredths of g for the height question. +fn policy(push_n: Nat) -> StackPolicy { + StackPolicy { effective_length_factor_percent: 100, service_push: newton(count: push_n), service_acceleration: micrometre_per_second_squared(count: 196133) } +} +fn policy_g(hundredths_of_g: Nat) -> StackPolicy { + StackPolicy { effective_length_factor_percent: 100, service_push: newton(count: 1), service_acceleration: micrometre_per_second_squared(count: 9806650 * hundredths_of_g / 100) } +} + +// The pitch is derived, not the frame proposal's 158 mm: it is the tallest part over the lowest +// hanging part plus the clearance, and both ends are real cassette parts. +test fn the_pitch_is_derived_from_the_cassette_parts() -> Bool { + tallest_cassette_part_top() > 90000 && lowest_cassette_part_bottom() < 0 + && stack_pitch() == tallest_cassette_part_top() - lowest_cassette_part_bottom() + 20000 + && pillar_height() < stack_pitch() +} + +// Without a board mass the admission refuses and names what was not weighed; it never weighs +// the board at zero. +test fn an_unweighed_board_refuses_the_admission_by_name() -> Bool { + match stack_screening(sheet: bambu_petg_hf_tds, declared: [], station: station(top: false), policy: policy(push_n: 1), node_count: 4) { + StackRefusedUnweighed { unweighed: u } => any(u, id => id == "BOARD") && any(u, id => id == "RAM1") + StackScreeningOnly { verdict: _, creep_unqualified: _ } => false + StackLimited { first_limit: _, verdict: _ } => false + } +} + +// With the hypotheses declared, four nodes on a bottom station are screened and the verdict +// reports three declared masses. The reported stress is not compared against +// a fabricated compressive allowable; ideal Euler buckling is only a screen. +test fn four_nodes_screen_on_declared_masses_and_report_the_dependence() -> Bool { + match stack_screening(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: false), policy: policy(push_n: 1), node_count: 4) { + StackScreeningOnly { verdict: v, creep_unqualified: _ } => v.rests_on_declared_masses == 3 && v.node_count == 4 + && gram_count(g: v.unit_mass) > 3000 && gram_count(g: v.unit_mass) < 8000 + && millinewton_count(f: v.bottom_pillar_load) > 0 && kilopascal_count(p: v.bottom_bearing_stress) > 0 + && millinewton_count(f: v.pillar_critical_load) > millinewton_count(f: v.bottom_pillar_load) + && newton_count(f: v.tip_force) > 1 + StackRefusedUnweighed { unweighed: _ } => false + StackLimited { first_limit: _, verdict: _ } => false + } +} + +// Tipping is the limit that binds first: a push the stack cannot survive is a typed limit, and +// a heavier station at the bottom improves base-acceleration tolerance, but not +// the top-applied push threshold for equal total mass and height. +test fn tipping_binds_first_and_a_top_station_tips_sooner() -> Bool { + let limited = match stack_screening(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: true), policy: policy(push_n: 100000), node_count: 8) { + StackLimited { first_limit: l, verdict: _ } => match l { TipsUnderServicePush { tip_force: _, push: _ } => true PillarBucklingExceeded { level_from_bottom: _, pillar_load: _, critical: _ } => false TipsUnderServiceAcceleration { tip_acceleration: _, required: _ } => false } + StackScreeningOnly { verdict: _, creep_unqualified: _ } => false + StackRefusedUnweighed { unweighed: _ } => false + } + let top = micrometre_per_second_squared_count(a:tip_acceleration(unit:gram(count:2000),station:station(top:true),node_count:8)) + let bottom = micrometre_per_second_squared_count(a:tip_acceleration(unit:gram(count:2000),station:station(top:false),node_count:8)) + limited && bottom > top && top > 0 + && newton_count(f:tip_force(unit:gram(count:2000),station:station(top:true),node_count:8))==newton_count(f:tip_force(unit:gram(count:2000),station:station(top:false),node_count:8)) + +} + +// Sensitivity of this explicit uniform-mass fixture, not physical height admission. +// The ideal Euler/material inputs do not capture printed-joint strength or creep. +test fn the_screened_height_answers_the_operator_question() -> Bool { + let bottom = max_screened_height(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: false), policy: policy_g(hundredths_of_g: 20), ceiling: 16) + let top = max_screened_height(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: true), policy: policy_g(hundredths_of_g: 20), ceiling: 16) + let pla = max_screened_height(sheet: bambu_pla_basic_tds, declared: declared(), station: station(top: false), policy: policy_g(hundredths_of_g: 20), ceiling: 16) + let lax = max_screened_height(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: false), policy: policy_g(hundredths_of_g: 5), ceiling: 16) + bottom >= 4 && bottom < 16 && bottom >= top && pla == bottom && lax > bottom + && max_screened_height(sheet: bambu_petg_hf_tds, declared: [], station: station(top: false), policy: policy_g(hundredths_of_g: 20), ceiling: 16) == 0 +} + +// The instrument's answer under the witness's declared masses, exposed so a reader can run it: +// the column height a 0.2 g bump screens with the station at the bottom. +fn screened_height_at_0_2g_bottom_station() -> Nat { + max_screened_height(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: false), policy: policy_g(hundredths_of_g: 20), ceiling: 16) +} +test fn the_0_2g_height_is_reported_not_guessed() -> Bool { + let n = screened_height_at_0_2g_bottom_station() + match stack_screening(sheet: bambu_petg_hf_tds, declared: declared(), station: station(top: false), policy: policy_g(hundredths_of_g: 20), node_count: n + 1) { + StackLimited { first_limit: l, verdict: _ } => match l { TipsUnderServiceAcceleration { tip_acceleration: _, required: _ } => true TipsUnderServicePush { tip_force: _, push: _ } => false PillarBucklingExceeded { level_from_bottom: _, pillar_load: _, critical: _ } => false } + StackScreeningOnly { verdict: _, creep_unqualified: _ } => false + StackRefusedUnweighed { unweighed: _ } => false + } +} diff --git a/docs/extdeps/asrock_rack/ALTRAD8UD_standoff_grid.png b/docs/extdeps/asrock_rack/ALTRAD8UD_standoff_grid.png new file mode 100644 index 00000000000..1fc86f89410 Binary files /dev/null and b/docs/extdeps/asrock_rack/ALTRAD8UD_standoff_grid.png differ diff --git a/docs/plans/cassette-board-mechanical-references.md b/docs/plans/cassette-board-mechanical-references.md new file mode 100644 index 00000000000..a1aba2c3140 --- /dev/null +++ b/docs/plans/cassette-board-mechanical-references.md @@ -0,0 +1,55 @@ +# Provisional Jade and Collins mechanical references + +Accepted for planning on 2026-10-04, following the operator-supplied research summary. Double-check board revision, outline axes, every mounting feature and underside keep-outs before committing standoff positions. These references unblock layout; they do not establish a complete mounting map. + +## Sources and standing + +- Jade: [OCP specification rev 1.0](https://www.opencompute.org/documents/open-compute-specification-mt-jade-rev-1-0-pdf-1), 2021-09-17, §7.2 p13: 428 mm wide × 479.36 mm deep. Retrieved from the [archived publisher document](https://web.archive.org/web/20220603132323id_/https://www.opencompute.org/documents/open-compute-specification-mt-jade-rev-1-0-pdf-1). PDF SHA256 `1ca9adc18543ef4686537aaafa386fa09075f75612dd3ea57c03b7d8d438f6ae`. +- Jade's p9 Figure 1 provides a cleaner top-down rendering than the annotated p13 Figure 3. The latter visibly has a different aspect ratio; neither is a dimensioned mechanical drawing. Candidate fasteners below were manually identified in Figure 1, not copied from a standard form factor. CPU socket fasteners are excluded. Feature identity and completeness remain unverified. +- Collins: [Portwell APTSA-627211-B4 datasheet](https://portwell.com/pdf/server/APTSA-627211-B4.pdf), as supplied in the operator's research: 512 × 424.2 mm. The live PDF was fetched and all three pages decoded on 2026-10-04 after supplying the missing Let’s Encrypt YR2 intermediate with TLS verification retained. The read receipt is `gunbc.specification_citation_read_provenance.portwell_collins_citation_read_receipt` (4,916,642 bytes, SHA-256 `3914b6dfb871b9eb4b317d658175de9346c8b2bfbcdfb1d541c6e74b05f9a285`). This is a direct read of Portwell’s publication, still reseller evidence about the board. The product derives 424.2 mm across / 512 mm deep because only the shorter dimension fits the sheet’s 448 mm chassis width. The metric/inch disagreement is retained: 20.13 inches converts to 511.302 mm, 0.698 mm below 512 mm; 16.7 inches converts to 424.18 mm. Revision and dimensional tolerances remain unconfirmed. The reported unrelated copy/paste error reduces confidence in the sheet. +- The held Ampere Collins DVT/PVT/MP Getting Started Guide v1.05 p36 Figure 12 is cropped and obscured by cables/assemblies; it cannot establish full-board datums. Collins candidate coordinates therefore remain empty, rather than borrowing Jade's pattern. [Ampere document catalogue](https://amperecomputing.com/customer-connect/products/mt-collins) is the follow-up reference. + +The reported dimensions make Collins **32.64 mm deeper and 3.8 mm narrower** than Jade under that axis assignment, not 84 mm longer. Public Collins OCP collateral was not located; this does not prove that none exists. Neither OCP NIC support nor an exhibition proves a published motherboard mechanical specification. + +## Jade provisional candidate coordinates + +Datum: left/front corner of the board's bounding rectangle; front is the fan edge at the top of Figure 1. Positive X goes right; positive depth goes toward rear I/O. The PCB is nonrectangular: the rectangle below is a scaling datum, not the actual routed outline. + +Raster: full PDF page rendered at 2 pixels/point (1224 × 1584), PCB bounds x=394..830, y=594..1083, excluding connector projections. Each axis is scaled independently to the published outline. Coordinates are reproducible planning estimates; submillimetre display precision is not accuracy. No ±1–2 mm accuracy bound has been established, and hole diameters/threads are unknown. A peer review found roughly 0.4–1.7 mm differences in front offsets using different edge picks; treating edge candidates as better than about 2 mm would be unjustified, not a newly established ±2 mm bound. Three further similar symbols were reported near connector-strip ends; they remain unidentified and are not silently added as mounting holes. + +| Candidate | From left, um | From front, um | +| --- | ---: | ---: | +| J01 | 100128 | 9802 | +| J02 | 302348 | 9802 | +| J03 | 10798 | 230367 | +| J04 | 419165 | 232327 | +| J05 | 94238 | 259775 | +| J06 | 264064 | 259775 | +| J07 | 348486 | 269578 | +| J08 | 143321 | 282322 | +| J09 | 103073 | 303888 | +| J10 | 82458 | 447010 | +| J11 | 102091 | 444069 | +| J12 | 167862 | 458773 | +| J13 | 338669 | 444069 | + + +The table is generated by `board_mechanical_reference.image_candidate_table_markdown(reading: jade_image_mount_reading)` and emitted as `mounting_reference.coordinate_table_markdown` by `stack_mass_review`; regenerate it from that output after changing calibration or candidates. + +## Model integration and later verification + +`BoardOutlineKnown {width, depth, standing}` uses the existing `CitedFigureStanding`; there is no parallel Published/Provisional type. Portwell owns the cited raw dimensions in `extdeps.portwell.aptsa_627211_b4`; the product owns the chassis-width-based axis inference. Collins uses it through the same layout/partition functions as the other boards: with 14 mm perimeter margin its tray planning envelope is 452.2 × 540 mm, allocated as 3 × 4 planning cells by the feature-aware bed allocator, with a proposed 2 mm brim and 0.5 mm bed-edge clearance per side. This does not generate drilling coordinates. + +`board_mechanical_reference` owns the image-derived Jade observation with typed raster calibration, datum, incomplete coverage, unestablished accuracy and the existing citation-read receipt. Width/depth and citation come from the OCP authority. Each `CassetteProfile.mounting` carries a closed reference: Altra’s standard correspondence, Jade’s image reading, or a named pending obligation for Collins/GH200. `platform_projects_review` matches these values rather than dispatching on profile names. Jade’s `HolePositionEstimatedFromImage` is refused by the existing total `standoff_placement` function; its JSON candidates each carry `standoff_admitted: false`. The separate mounting-coordinate and board-revision obligations remain open. + +Later: reconcile every visible candidate against original mechanical CAD or a clear, calibrated board drawing; distinguish board fasteners from nearby hardware; account for obscured holes, nonrectangular outline, socket load paths and underside components. For Collins obtain a full top-down reference before deriving any coordinates. No operator measurement or disassembly is requested now. + +## Partition sensitivity and CI scope + +With 14 mm margins, Collins is 452.2 mm across. A blanket 30 mm deduction made three cells hold only 450 mm, causing a fourth column over a 2.2 mm difference (6 mm for Jade). That blanket policy has been replaced by `bed_partition.allocate_axis`: each cell pays only for its outer or joining-side protrusions plus explicit brim and edge clearance. + +The shared panel dimensions give 20 mm donor-joint protrusion, 6 mm rails, 6.5 mm rear sockets (the governing side allowance), and 27 mm front handles. With the proposed 2 mm brim and 0.5 mm bed-edge clearance per side, width capacities are 148.5 mm first, 155 mm interior and 168.5 mm last. Three columns can cover 472 mm, leaving 19.8 mm aggregate capacity for Collins and 16 mm for Jade. The allocator balances the actual cuts subject to each capacity: Collins uses 148.5 / 151.85 / 151.85 mm; Jade uses 148.5 / 153.75 / 153.75 mm. Both need four depth rows. A 6 mm brim recomputes Collins to four columns; it is never silently reduced to force a fit. + +These are envelope plans under declared slicing allowances, not completed larger-board solids. Exact solid bounds, feature placement, underside keep-outs and slicer-generated footprint still need to agree before printing. The existing Altra panels remain the compatibility fixture. + +The broad `test.claim.cassette_` row was removed from `required_gate_prefixes` at the operator’s request. It is not replaced by exact required-gate entries. Focused cassette witnesses remain available and are run as scoped validation; the existing changed-witness policy is unchanged. diff --git a/docs/plans/cassette-completion-tasks.md b/docs/plans/cassette-completion-tasks.md new file mode 100644 index 00000000000..030bf0ce6be --- /dev/null +++ b/docs/plans/cassette-completion-tasks.md @@ -0,0 +1,92 @@ +# Cassette completion task queue + +Owner: cassette worktree / PR #13295. Target: main. User-approved sequence, 4 October 2026. +This is the current queue; dated gauge and rack-frame plans are historical. No new gauge sequence, weighing request or printer start is scheduled here. + +| ID | Task | Depends on | State | +| --- | --- | --- | --- | +| CAS-1 | Restore green required CI | — | Known declaration/catch-all failures corrected; validation pending | +| CAS-2 | Finish shared cassette generation | CAS-1 | Shared profiles and feature-aware minimum-cell allocation implemented; solid feature generalization open | +| CAS-3 | Size rear fans from cassette opening | CAS-2 | Selector, keyed 140 mm rear mount and guard modeled; geometry checks pass; cooling/retention qualification open | +| CAS-4 | Complete integrated stacking geometry | CAS-2, CAS-3 | Cassette-owned clip shoes, hollow posts and stack keys modeled; one/two-unit printed collision checks pass; load/retention qualification open | +| CAS-5 | Complete power cassette and harness layout | CAS-2, CAS-4 | Electrical/interface planning exists; physical placement open | +| CAS-6 | Validate final loads, airflow and materials | CAS-3, CAS-4, CAS-5 | Preliminary screens exist; final assembly qualification open | +| CAS-7 | Publish combined review and prepare remaining prints | CAS-6 | Tall-cassette joinery review updated; power layout and fabrication release remain open | +| CAS-8 | Desired-set print orchestration and plate packing | Existing CAD/slicing/start routes | Planned; BATCH-1 through BATCH-6 defined | + +## CAS-1 — Restore green CI + +Correct the source reference in the Bambu filament scope and explicitly enumerate the closed variants in component mass, platform review and PSU capacity screening. Run the required floor and focused witnesses, fix subsequent findings, and require all GitHub checks on the pushed head. Do not weaken checks, register avoidable catch-all debt, or claim old-head results validate new code. + +## CAS-2 — Shared cassette generation + +One profile-driven generator supports ALTRAD8UD, GH200 P4261 / 694-24261-000-100 K1, Mt. Collins and Mt. Jade. Profiles own component/mounting evidence and retained power/management assemblies. The shared partition graph divides the board-plus-margin footprint into cells with one edge per adjacent pair, accounting for joint, rail/socket, handle, brim and bed-edge envelopes separately. The allocator derives minimum axis counts and balances cuts, allowing edge cells to be narrower. With the proposed 2 mm brim and 0.5 mm bed-edge clearance per side, Altra remains four equal cells; Jade and Collins use 3 × 4 planning cells. The old blanket 30 mm deduction produced 4 × 4 and is superseded. + +Remaining: make the existing panel/joinery solid generator consume this graph, then parameterize supports, underside keep-outs, perimeter handles and sockets. Preserve the exact r06 Altra geometry as the printed compatibility fixture. The missing GH200 outline, provisional Collins outline and unverified mounting coordinates must not borrow Altra geometry or become printable through defaults. Accept when the same generator realizes both the Altra fixture and a larger synthetic profile, with bed/collision/mate checks and refusal controls. + +## CAS-3 — Height-related rear cooling + +User-selected policy: largest fitting standard fan, evaluating 140/120/92/80 mm candidates. Derive a single unobstructed opening after corner structure, connector and service reservations; do not add disconnected free areas. Check height, width, depth and individual-part print bed. Derive count and centered positions from width. Use vendor-specific hole patterns; never scale the 80 mm pattern. + +The current selector retains the r06 80 mm assembly unchanged and reports an alternative rear bank in the stack review. ARCTIC publisher drawings were visually read: P14 125 mm pitch, P12 105 mm, F9 82.5 mm, all nominal 4.3 mm holes. These are manufacturer geometry, not printed-clearance compensation. Mount edge uses a proposed 6 mm margin each side. A conservative 40 mm fan-body reservation plus 20 mm mounting/service depth is a design allowance, not a vendor depth claim. Noctua variant height remains derived, not forced to 177.8 mm. + +Remaining: generate removable, guarded rear modules and their attachment/airflow guides through the existing CAD path. A 231.84 × 190 mm unobstructed opening selects one 140 mm fan; a 310 × 132 mm opening selects two 120 mm fans. Geometric preference does not prove either bank is thermally adequate or quiet. Accept with fit-boundary, obstruction, depth/bed refusal and mount-solid checks, then airflow and header-current qualification in CAS-6. + +## CAS-4 — Integrated milk-crate structure + +Generate corner supports and stack keys as cassette-owned pieces, with no enclosing rack frame. Resolve interference with existing rear-arm sockets. Support interfaces and load paths must match before unlike profiles can stack. Minimize material and threaded joints; count assembly actions and fasteners in review output. + +Model removal and cable swept volumes. A column carried by the removed cassette cannot remain supported by geometry that is leaving with it. Default service procedure: de-energize/disconnect the target and support or lift the units above before a middle pop. Do not claim independent drawer removal until a modeled alternate load path exists. Accept with mating/collision checks for two stacked units, a removal sequence and explicit supported/unsupported service states. + +## CAS-5 — Modular power and harness placement + +Keep a common stack attachment with PSU-specific replaceable carriers/backplanes. Default one PSU; retain the exact PSU/backplane/pinout/enable compatibility obligation. Reuse existing branch and aggregate budget checks. Native GH200/Collins/Jade power assemblies remain attached to their node profile where needed. + +Place PSU, backplane, protected DC branches, mains enclosure/earth connection, connectors and strain relief. Cable census must follow actual endpoints, bends and service paths; front-edge board power and fan headers cannot disappear behind a rear-only route. Accept with cable/connector/service collisions, accessible disconnects, branch-load failures and retained-assembly envelopes accounted for. + +## CAS-6 — Final physics and thermal qualification + +Fold all installed components and generated/sliced part masses at their placements; retain researched ranges and missing-evidence standings. Include power cassette, wiring, joints and cooler transport loads. Re-evaluate corner reactions, buckling, tipping, interface shear and creep against final geometry and printed material/orientation. Preliminary Euler or rigid-body screens are not a certified stack limit. + +Use selected fans' operating data with restriction/fan-failure cases, header-load limits and ambient/exhaust temperatures. CPU die temperature is not mount temperature. Generic PETG or Bambu PETG figures do not qualify SUNLU High Speed PETG. Keep material acceptance open until product-specific temperature/load evidence supports it. Accept with adverse controls and explicit remaining validation requirements; no invented measurements or operator weighing request. + +## CAS-7 — Review and print release + +Publish assembly and exploded per-piece rotation using the shared emitted model, including power/cable/clearance geometry. Show actual height, fan selection, BOM, assembly steps and qualification status. Compare with P1–P4 and the fitted fan mount; list reusable and superseded parts explicitly. + +Only after geometry/solid/bed checks and appropriate material selection: export and slice remaining pieces for both A1 minis, record digests, mass and durations, and schedule a two-printer wave plan. Physical starts still require fresh bed-clear evidence and the reviewed authorization workflow. No new starts are implied by creating this queue. + +## 140 mm joinery checkpoint — 4 October 2026 + +`rear_fan_assembly` now realizes the retained tray arms, two bed-sized crossmember halves, a keyed cradle, an integral-spacer guard and six broad-grip retaining pins. The P14 body uses its vendor 27 mm depth. Two diagonal M4 bolts capture plate, fan and guard; the modeled stack is 40 mm before washers/nuts, so final bolt length remains unresolved. The guard's nominal 7 mm openings are proposed geometry, not certified finger protection. + +`stack_joinery` adds four cassette-owned clip shoes, hollow 20/12 mm posts split into lower/upper pieces, four middle-joint pins and keyed stack seats. No separate enclosing frame is present. The post wall area is 256 mm²; the shoe bearing area is only 127.75 mm². These must not be substituted for each other or for the old solid-pillar mechanics screen. Rear shoe relief clears the existing rear-arm pin heads. A middle pop explicitly requires independent support above it. + +Validation: three new joinery witnesses passed. The shared CadQuery emitter produced 34 valid positive solids, including 28 connected printed solids, each under the 180 mm bed bound. Exact printed-solid intersection checks found no intersections above 0.01 mm³ within one cassette or between two at 230 mm pitch. These were manual realization checks, not a new durable CAD witness. P1–P4 are imported unchanged. The interactive review preserves piece isolation/rotation and adds a two-unit view. These results do not establish support-free slicing, clipping force, vibration retention, loaded lifting, creep, thermal suitability or a safe stack count. + +Remaining before fabrication release: complete power and cable reservations, connect actual hollow/jointed support geometry to the load screen, qualify material/retention and cooling obligations, and slice selected parts. Shared larger-board panel-solid generation remains CAS-2 work; the profiles and partition graph alone do not make those boards printable. Required CI is still pending on the PR; focused local results are not a green merge gate. + +## Provisional board references — 4 October 2026 + +The operator supplied a mechanical-source survey and authorized provisional inference. [Reference ledger](cassette-board-mechanical-references.md) records 13 Jade candidate mount positions derived from OCP Figure 1 and a separate provisional Collins outline (424.2 mm width × 512 mm depth). Collins now gets a 3 × 4 planning partition from the shared feature-aware allocator under the proposed process allowances. Hole-map completeness, feature identity and later verification remain explicit; no Jade pattern is substituted for Collins. + +## Feature-aware cell allocation — 4 October 2026 + +`bed_partition` owns the axis allocation calculation, used both for board-only counts and actual panel-envelope planning. For two or more cells, capacity is first + last + (count − 2) × interior; this gives the minimum count directly. A logarithmic balancing search chooses cut widths, saturating constrained edge cells first. Integer remainders preserve exact coverage, and zero-width cells refuse. No repeated growing-list concatenation is used. + +`tray_panel_features` owns the existing joint/rail/socket/handle dimensions; the retained Altra CAD and the allocator both read these values. This preserves the existing solids while removing the separate feature-budget estimate. `platform_projects_review` emits every cell's panel span and print envelope, including the proposed brim and bed-edge clearances. + +| Profile | Planning grid | Column spans, mm | Row span, mm | Adjacent joins | +| --- | --- | --- | --- | --- | +| Altra | 2 × 2 | 135.92 / 135.92 | 147.35 | 4 | +| Collins | 3 × 4 | 148.5 / 151.85 / 151.85 | 135 | 17 | +| Jade | 3 × 4 | 148.5 / 153.75 / 153.75 | 126.84 | 17 | + +This is 12 planned panels instead of 16 for Collins/Jade, and 17 adjacent joins instead of 24. It is the minimum axis-aligned grid under the declared side envelopes, not an unrestricted nesting optimum. A different brim, bed, feature envelope or board outline recomputes the count. Multi-cell solid realization remains open; no change to printer jobs or to fabrication admission follows from this plan. + +Validation of this allocator change: six allocation checks, eight mechanical-reference/emitter checks and five platform checks passed. The combined review emitter succeeded. All 34 Altra part descriptions and its emitted CAD program are identical to the prior reviewed bundle, including P1–P4. Full required CI remains separate and pending. + +## CAS-8 — Desired-set print orchestration + +Requested next workflow milestone: [batch planning, dense plate packing and durable execution](print-batch-orchestration.md). BATCH-1 through BATCH-6 define the implementation and release checks. This replaces manual per-file queueing; it is planned, not implemented. + diff --git a/docs/plans/cassette-joinery-review.md b/docs/plans/cassette-joinery-review.md new file mode 100644 index 00000000000..cbf4fba74a3 --- /dev/null +++ b/docs/plans/cassette-joinery-review.md @@ -0,0 +1,56 @@ +# Cassette joinery revision + +Operator direction, 4 October 2026: fewer screws, fewer small attachment points, and shorter +assembly time. Replace the r05 screw-and-splice construction before scheduling cassette prints. + +Proposed r06 assembly: four keyed half-lap tray panels with integral rails and split front handle; +two slide-in rear support arms; three interlocking fan-frame sections (outer guides integrated); +three guards with integral spacers/locating pins; five large removable retaining pins. Target +17 printed instances, four M3 panel-clamp screws and six M4 fan/guard screws, excluding motherboard +standoffs. The r05 design had 49 printed instances and 56 fastener axes. These are design targets, +not measured assembly times or confirmed fit. + +Assembly order: mate the four panels and fit their four clamp screws; slide the two support arms +into the integral underside sockets and drop in two retainers; slide the center rear section into +the two outer sections and retain it with one pin; seat that bank on the arms and insert two pins; +fit fans and guards with two diagonal through-bolts per fan. The other two fan holes receive locating +pins. The keyed joints carry registration/shear; the four tray screws clamp the laps. No small +underside splice plates, separate rail splices, separate fan feet or loose guard spacers. + +Nominal joint clearance: 0.25 mm per mating side (proposal, not measured printer compensation). +Board supports, loaded joint behavior and final hardware engagement remain explicit open interfaces. +Review one complete assembly; do not restart individual gauge printing. + +Implementation notes for the revision: +- Preserve nominal board/tray outline, rear exhaust, shared fixed-frame power and custom bay pitch. +- Use 8 mm panels with 4 mm keyed laps. Panel print envelopes must include tongue, rail and handle. +- Rear arms slide in integral panel sockets; fixed rear bank remains split for the 180 mm bed. +- Derive both sides of each joint from one joint declaration; witness alignment and deliberate + clearance mutations in `.dag`, alongside actual CAD readback. +- Extend the existing CadQuery realization path; remove the separate product-level Python + geometric interpreter. Kernel observations are evidence, with validation policy modeled in DAG. + +Validation performed for r06: CadQuery 2.8.0 realized all 17 printed pieces as single valid solids; +STL closure and volume/bounds checks, STEP round-trip, 180 mm bed fit, and printed-pair collision +checks passed. The review uses the shared cadquery_realization exact-dimension emitter. The source +manifest is bound to the emitted solid program. `.dag` witnesses cover source-model bed/mate/member +checks and negative controls. Geometry checks do not establish support-free printing, retained-pin +vibration performance, loaded joints, thermal adequacy or board support. + +Remaining review work: kernel-observation policy still lives in the review packaging adapter; +move it behind a DAG-owned observed-result judgment and execute that pipeline in CI. The new source +checks and shared emitter remove the duplicate solid interpreter, but do not close that obligation. +The assembly remains review-only, with no fabrication capability. Assembly time must be measured +on the first full unpowered build; no invented time saving is claimed. + +CI cost validation (2026-10-04): the closed-variant residue gate reports zero unrostered +cases after explicit constructor handling. Source admission indexes world-space bore lines and +part IDs instead of rescanning all primitives for every mounting hole. The map remains bounded +by this small assembly; persistent-map insertion is not claimed to be globally linear. +The whole-assembly and moved/missing/oversized controls remain full-model witnesses. Packaging +uses the same parameterized producer on a small fixture and includes duplicate-member refusal. +Direct claim_batch measurements put all six witnesses below the new-witness enrollment margin; +no ceiling, debt roster or exemption was changed. A complete bundle emission matched the r06 +manifest and Python program exactly. Hosted floor validation must still confirm the committed +revision. Local full-floor execution is not a green receipt: this session host lacks the declared +browser-fixture runner premise. diff --git a/docs/plans/cassette-platforms.md b/docs/plans/cassette-platforms.md new file mode 100644 index 00000000000..a85251246be --- /dev/null +++ b/docs/plans/cassette-platforms.md @@ -0,0 +1,81 @@ +# One cassette design, multiple platform profiles + +Operator direction, 4 October 2026: consolidate the design on main; support the NVIDIA P4261 GH200 baseboard and Mt. Collins/Mt. Jade transplants without forking cassette geometry. “Baseboard” here means a mechanical support cassette, not a replacement electrical PCB. + +## Shared architecture + +```mermaid +flowchart LR + A[ALTRAD8UD profile] --> L[Shared layout and interface model] + G[GH200 P4261 profile] --> L + J[Mt. Jade profile] --> L + C[Mt. Collins profile] --> L + L --> T[Tray segmentation and joinery] + L --> M[Mounts and underside keep-outs] + L --> P[Power and management assembly placement] + L --> F[Cooling and harness routing] + T --> R[One realization and review pipeline] + M --> R + P --> R + F --> R +``` + +`cassette_profile` owns the shared input shapes; `cassette_profiles` supplies configuration rows. `cassette_layout` owns board-to-tray dimension derivation and refuses missing outlines. The existing `cassette` consumes that same derivation, preserving its printed Altra dimensions. `stack_mass_review` emits all profile standings. Board-specific upstream facts remain with their existing vendor/specification owners. + +The common design does not require identical outer dimensions. A profile selects footprint, mounting coordinates, retained assemblies, cooler envelope, cable corridors and power architecture. Cross-profile stacking requires compatible support interfaces and load paths; width changes cannot be hidden by simply extending unsupported tray material. + +## Profiles and evidence + +| Profile | Established input | Still needed | +| --- | --- | --- | +| ASRock ALTRAD8UD | Existing board dimensions, printed tray/joinery and fan-pattern observations | Existing standoff/underside/load/thermal obligations | +| NVIDIA P4261 / 694-24261-000-100 K1 | Exact operator-requested identity; matching marketplace listing located | Manufacturer drawing/revision, board outline, hole map, underside keep-outs, functional module/carrier/management BOM, power connector and sequencing, cooling assembly | +| Mt. Jade | Public OCP rev 1.0 board outline 428 × 479.36 mm, reused from extdeps | Mounting map and retained assembly placement; revision-specific power/management/harness details | +| Mt. Collins | Existing product briefs and getting-started guide references, dual-socket platform facts | Exact board/chassis revision, motherboard drawing and hole map, original power/management assembly placement | + +Sources: [Mt. Jade OCP specification](https://www.opencompute.org/documents/open-compute-specification-mt-jade-rev-1-0-pdf-1), existing `extdeps.ampere.mt_collins_getting_started_guide.subject`, and [Ampere reference-design document catalog](https://amperecomputing.com/home/customer-reference-boards). The [P4261 marketplace listing](https://www.ebay.com/itm/157373033242) identifies the requested item but is not authority for pinouts, hole coordinates, or complete-system compatibility. No MGX system enclosure dimensions or Jetson carrier drawings are substituted for this board. Some Ampere design packages require Customer Connect access; publicly modeled sources are reused first. + +A board-only rectangular 180 mm grid needs 3 × 3 cells for Jade. That is an envelope calculation, not a print plan: margin, joint overlap, supports, retained hardware and structural members must be included before choosing actual pieces. With the current shared 14 mm edge allowance, the board-only tray reservation is 456 × 507.36 mm. It is not a released tray or final whole-system footprint. + +## PVT/DVT guide inspection — 4 October 2026 + +Both publisher PDFs were downloaded and their SHA-256 digests matched the existing subject rows. Local review copies are under `/home/briansrls/print-prep-2026-10-04/milk-crate/manuals/`; the repository retains publisher locators and document identities rather than adding PDF binaries. + +| Guide | Pages checked for physical layout | Result for cassette design | +| --- | --- | --- | +| Mt. Collins DVT/PVT/MP GSG, issue 1.05, 43 pages | p. 6 Table 7 connector/riser routing; p. 32 Figure 11 board overview; pp. 36–37 revision identification | Useful placement and harness evidence; no dimensioned mounting-hole map found. Table 15 distinguishes DVT BX/DX/EX, PVT IX/KX and MP 0B, so a future drawing must be matched to revision. | +| Mt. Jade PVT/DVT NVMe GSG, issue 1.00, 48 pages | p. 4 rear interfaces; p. 35 Figure 11 board overview | Useful connector and board photographs; no dimensioned mounting-hole map found. | + +Publisher locators and hashes are owned by `extdeps.ampere.mt_collins_getting_started_guide.subject` and `extdeps.ampere.mt_jade_getting_started_guide.subject`. The Jade outline already modeled from OCP §7.2 is 428 × 479.36 mm; it is not a hole pattern. The OCP specification also lists a §12 Mechanical section, but its figures could not be retrieved during that initial pass (publisher download returned HTTP 403). The later provisional pass recovered the archived PDF and read Figures 1 and 3. That section and the revision-matched mechanical design package remain the next drawing sources to inspect. + +Neither guide inspection closes the support-coordinate obligation. Visible screw rings in perspective photographs are discovery evidence, not fabrication coordinates. A later operator-approved provisional pass is recorded in [the mechanical reference ledger](cassette-board-mechanical-references.md): Jade has image-derived candidate positions, and Collins uses a secondary-source planning outline. Complete verified mounting maps remain unresolved for both. + +## Preserve the functional platform assembly + +For the new transplant profiles, prefer retaining the platform's original power distribution and PSU cage with the node, rather than requiring the separate generic power cassette. This is design intent, not proof that a loose baseboard includes every required part. Model and place: + +- Compute board/module and original socket backplates/load-bearing mounts. +- Matched PSU, PDB/backplane, enable/sense/standby harness and protective-earth path. +- BMC/management or other sequencing hardware, front-panel connections and required interlocks. +- Original cooling hardware, fan power/control and airflow ducts; liquid plumbing if the selected assembly needs it. +- Risers, NIC/storage connections, retention and cable insertion/removal swept volumes. + +The printed cassette replaces chassis support around those assemblies; it does not automatically replace their electrical interfaces, bonding or cooling. The Altra three-by-80-mm fan arrangement is not inherited as adequate cooling for GH200 or a dual-socket board. The mass fold must include all retained items at their positions, and the larger span changes joint loads and creep requirements. + +## Shared-generator implementation sequence + +1. **Completed:** a common profile/layout authority, existing Altra dimension consumer, explicit missing-outline refusals and review output for all four configurations. +2. Generalize the current fixed four-panel topology into a bed-constrained tile/edge graph. Generate each joint once from adjacent cells; make rails, handles, corner interfaces and rear attachments consumers of that graph. Preserve existing Altra geometry as the regression case. +3. Replace board-specific support placement with a profile-supplied mounting/keep-out map. Unknown holes refuse support generation; never scale one board's pattern to another. +4. Place retained power/management/cooling assemblies through shared mount interfaces and derive cable routes and service volumes. Each profile supplies actual parts; it does not author another generator. +5. Run common assembly/collision/bed-fit witnesses, mass/force and thermal screens; realize through the existing CadQuery path. New platform fabrication waits on its unresolved inputs. No motherboard removal or new print is requested by this planning change. + +The geometry is not fully generalized yet: the current realization remains the four-panel Altra assembly. The new profiles are executable design inputs and tracked obligations, not fabricated GH200/Collins/Jade models. + +## Consolidation + +PR #13295 becomes the main-targeted cassette design PR and includes #13222's joinery ancestry. #13283's extra-frame design is superseded by the integrated-column direction and is not merged back. The print-day preparation/physical receipts are retained in this consolidated branch. Slicing (#13223) and printer authorization (#13149 and its approval-client dependency) remain separate operational changes, preserving the earlier review's requested separation from mechanical design. + +### Feature-aware planning cells + +The shared allocator now accounts separately for outer edges, donor joints, rail/socket protrusions, front handles, brim and bed-edge clearance. Under the proposed process allowances it yields 3 × 4 for Jade/Collins, while preserving the Altra 2 × 2 split. See the current [completion queue](cassette-completion-tasks.md) for spans and validation boundaries. These counts replace the old blanket feature-budget estimate; larger-board solids are still required. diff --git a/docs/plans/cassette-print-day.md b/docs/plans/cassette-print-day.md new file mode 100644 index 00000000000..437bed6989a --- /dev/null +++ b/docs/plans/cassette-print-day.md @@ -0,0 +1,309 @@ +# First complete cassette: two-printer day plan + +Planning source: cassette r06, source commit 389bb0588ee62f8f9de4276a6d72d36c23fe33c3. +Both A1 mini printers are available all day, per the operator. Use the previously confirmed +PLA in AMS slot 1 on each. Availability is not fresh bed-clear or idle evidence for a start. +This is a queue for one complete unpowered assembly, not a dispatch or print-admission receipt. +Keep the existing approval, fresh idle observation and durable start claim for every job. + +## Historical r06 queue + +Waves 3–5 below are superseded by the 140 mm continuation at the end of this document. +Do not dispatch the old three-80-mm-fan parts for the residential assembly. + +| Wave | Printer-01 | Printer-02 | Assembly work after removal | +| --- | --- | --- | --- | +| 1 | P1: rear tray half | P2: other rear tray half | Check the real keyed seam and clamp access; retain both parts for the cassette. | +| 2 | P3: front tray half | P4: other front tray half | Join the four panels, check rails and handle; fit four M3 clamps when lengths are established. | +| 3 | M1: outer rear fan frame | M3: opposite outer rear fan frame | Clean supports from keys and airflow guides; inspect the mating surfaces. | +| 4 | M2 plus E1 and E2: center fan frame and both support arms | G1 plus K1–K5: first guard and all five retainers | Assemble the rear bank and slide the arms into the tray; check retention. | +| 5 | G2: second guard | G3: third guard | Fit three fans and guards with six M4 through-bolts once length/engagement is checked. | + +Ten provisional plates cover all 17 instances exactly once. P1–P4 and M1–M3 are distinct; +E1/E2 share geometry, G1/G2/G3 share geometry, K1/K2 share geometry, and K3/K4 share geometry. +Do not substitute K5 for the other pins. No extra gauges or speculative spare sets are queued. + +These are priorities, not synchronization barriers: advance either printer as soon as its bed is +cleared and its next admitted file is ready. If the other printer is still running, it need not +finish before this printer advances. Keep the remaining day as reprint/fit-correction capacity. +The real P1/P2 seam is the first assembly checkpoint; a failed seam pauses related tray parts for +correction, while independent fan/guard preparation can continue. + +## Plate preparation + +All placements below are candidates until checked in the slicer, including support and brim extent. +The modeled bed is 180 × 180 × 180 mm. Exported STLs already have their modeled print orientation; +import at 100% scale and do not apply the same orientation transform twice. + +- P1/P2/P3/P4 each get a dedicated plate. Their modeled oriented footprints are approximately + 162.22 × 167.15, 142.22 × 167.15, 161.72 × 174.15, and 141.72 × 174.15 mm. + P3/P4 plus a 2 mm brim occupy 178.15 mm along the long axis. The old 5 mm brim does not fit. + Review the integral underside sockets and half-lap overhangs; support placement cannot be + inherited from the gauge preset, which explicitly disables supports. +- M1/M3 each get a dedicated plate: approximately 112 × 122 mm, 105 mm tall in the modeled + XZ print orientation. Review the fan-opening bridge, guides and keyed foot, with removable + support that does not trap material in a mating socket. Allow support spread before admission. +- M2 is approximately 92 × 40 mm, 107 mm tall in its modeled orientation. Each arm is + 20 × 112 mm, 8 mm tall. A candidate arrangement places the two arms beside M2 with 6 mm + object gaps; its unbrimmed bounding rectangle is approximately 144 × 112 mm. + Split this plate if support extent or stability requires it; that adds one short job. +- G1/G2/G3 have 92 × 92 mm footprints and are 19 mm tall. Do not assume two guards fit on one + 180 mm plate: two axis-aligned footprints already span 184 mm before gaps or brims. +- Put the five retainers alongside G1, with sufficient separation for brims. Evaluate printing + retainers grip-down so the broad head anchors the part, rather than balancing on a 4.1 mm shaft. + This is an explicit candidate orientation change requiring slice preview, not an already + verified alternative. If grouping compromises support or adhesion, split the retainers out. + +Use the pinned Orca workflow and the existing A1 mini/material profiles. Establish a cassette +process profile explicitly: wall count, infill, top/bottom shells, supports and brim are still +preparation decisions. A successful slicer exit alone is insufficient. Read back the resulting +project/G-code for machine, nozzle, material/temperatures, AMS mapping, scale, bounds, supports, +time and material usage. Do not dispatch a gauge-profile slice of these parts by default. + +## Timing and operator visits + +The present evidence contains no sliced cassette project, so there is no defensible numerical +completion time or filament total yet. Two available printers do not establish that all ten +plates finish within one day. After slicing, assign longer jobs to the less-loaded printer +within the above assembly priorities, and publish each printer's cumulative ETA and filament +requirement. Add cooling, removal, support cleanup and bed-reset time between jobs; those are +operator-dependent and not included in slicer estimates. + +Plan for five removal/reload rounds if the two queues stay roughly aligned; independently +finishing jobs can require up to ten visits. Leave a contingency window for a failed print or +joinery correction instead of filling the last available hour with optional duplicates. + +## Ready-to-start boundary + +The complete model and all 17 solid exports exist; source witnesses and CAD readback pass. +Current source-floor CI passed. The print queue itself has not been sliced or admitted. +Before the first dispatch: finish orientation/support review, generate and inspect the sliced +projects, complete the modeled review-to-print admission, and use the approved printer workflow. +Merging the CAD PR alone does not perform those steps or merge the separate printing/auth PRs. + +Today's physical objective is one assembled, unpowered cassette. Board standoff thread/height, +underside clearances, fastener engagement, loaded-joint behavior and thermal suitability remain +unresolved. The fixed rack, shared PSU hardware, power distribution and board-support hardware +are outside this 17-piece printed set. Record actual assembly time and troublesome joints on +this first assembly so the next revision can reduce handling and fasteners further. + +## Wave 1 preparation receipt + +P1 and P2 were sliced through the digest-verified Orca 2.4.2 DAG route, using the +existing A1 mini 0.4 mm / textured PEI / PLA profiles and explicit cassette process +settings now in `slice_run.cassette_settings_members`. Four walls, 20% gyroid, five +top/bottom layers, 0.20 mm layer setting, 2 mm outer brim, normal automatic supports. +Readback confirms 220 C nozzle / 65 C textured bed, filament index 0 for AMS slot 1, +25 mm total model height, support use, proper unit-scale in-plane placement and in-bed +footprints. Orca's temperature advisory is retained (the existing bed profile is 65 C); +traditional timelapse warnings are retained and the start route does not request it. +Malformed `first_layer_time` metadata is not used for timing. + +| Part | Estimated runtime | Estimated PLA | Project SHA-256 | +| --- | --- | --- | --- | +| P1 | 4 h 51 m 40 s | 156.51 g | f34eb44a56d7de5e5c1bb7e6617aac367aa4ec0bd53c33f7d75ac67786bf0720 | +| P2 | 4 h 35 m 50 s | 147.82 g | f10363214c47ae527d7e3055416868c011efe642fcba015e1beb492db1ba5cfc | + +These are slicer estimates, not observed finish times. The first wave alone is almost +five hours; the complete ten-plate set is not promised for this day. Output files and +readback are in `/home/briansrls/print-prep-2026-10-04/cassette-wave-1` on srv2, staged +to `/home/briansrls/print-run-2026-10-04/cassette-wave-1` on srv1. +The live start uses reviewed printer source `6da087d2e30f789c59adf11287fe179fc41b0276`, +with a private persistent `/var/lib/gunbc/printer-starts` directory provisioned on srv1. +Physical start/finish standing must come from the corresponding workflow receipt and telemetry. + +The operator launch environment also needs the workflow's three key-path bindings: +`GUNBC_APPROVAL_SUBMISSION_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-submission-mac-key`, +`GUNBC_APPROVAL_CAPABILITY_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-mac-key`, and +`GUNBC_APPROVAL_STORE_RECEIPT_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-store-receipt-mac-key`. +Set `RUNNER_TEMP` to a mode-700 per-printer temporary directory for the signed request body. +Check those paths and the private operator token file before launch. Missing bindings caused +pre-filing refusals on the initial wave-1 attempts; neither had created a start claim or +contacted a printer to upload/start. Do not label a pre-filing attempt as awaiting approval. + +Live wave-1 update: printer-02 was observed RUNNING the exact P2 digest at 1%, 270 min +remaining, with print_error 0. Its MQTT publisher lost the connection after dispatch; the +physical start was reconciled by the report, not replayed, and its durable claim is retained. +Printer-01 was withheld before upload on 83935249 / 0500C011. Bambu Studio's shipped table +has an empty description, and Bambu collaborator walterwongbbl identifies it as a non-existent +error in https://github.com/bambulab/BambuStudio/issues/4495#issuecomment-2275068828. +Printer source 54bd96e7e9 classifies that exact code, accepts IDLE/FINISH job readiness, and +retains fresh bed-clear approval, other-error refusals and no-replay claims. All six readiness/ +approval witnesses passed. The P1 retry uses a new approval attempt under this revision. +FINISH is not a cooling state or proof of a part still being present: Bambu Studio's can_print +accepts IDLE and FINISH. Asking for a reboot solely to leave FINISH was unnecessary. + +## P4 preparation — 4 October 2026 + +The operator reports P2 finished and printer-02's bed cleared, and authorizes the next segment. P4 is the next tray piece on that printer. It was sliced with the same pinned Orca workflow and machine/material profile. The initial 2 mm brim/support footprint exceeded the bed; removing the outer brim alone was insufficient. The accepted profile retains normal supports and sets `raft_first_layer_expansion=0` (this setting also affects the support base with zero raft layers). + +The final project's SHA-256 is `14cebf1762970c67626859d9d5341f23ef322be37cff663a0c48ac08b7f74bab`. Estimated time: 3 h 33 m 42 s; PLA: 105.34 g. The 14 mm height agrees with source STL bounds. Independent deposited-path coordinates span X 0.811–179.190 mm and Y 18.073–160.650 mm; slicer bounding metadata is inconsistent and is not the sole bed-fit evidence. Unit scale, machine/nozzle, temperatures, supports, checksum and AMS index 0 passed readback. The previous thermal/timelapse warnings retain their documented disposition. + +Artifacts are under `print-prep-2026-10-04/cassette-wave-2` locally and `print-run-2026-10-04/cassette-wave-2` on srv1. The reviewed source remains `source-readiness` from wave 1; attempt `cassette-r06-wave2-20261004-P4` preserves approval and durable no-replay guards. The approval and fresh readiness checks passed, the durable claim was written, and the project uploaded. The MQTT start publication lost its acknowledgement (exit 7); it was not replayed. The operator confirms printer-02 heating or calibrating P4. Read-only telemetry subsequently confirmed RUNNING, print_error 0, the exact P4 digest filename, 2% and 208 minutes remaining (layer_num 0). Printer-01 is reported finished and cooling; P3 preparation is authorized, but its bed-clear confirmation is still pending. + +P3 is also sliced and checked with the same front-tray profile. Project SHA-256: `cd0e5071dd1f61ea99a1817eadf9cf7def2f1f5869f8730b14bbe0073bf1fc67`; estimated 3 h 43 m 47 s / 110.67 g PLA. Independent modal extrusion-path bounds are X 6.565–170.650 mm, Y 1.078–179.190 mm, including line starts/endpoints; no deposited arc moves occur in these slices. A 0.25 mm edge allowance is retained. The operator subsequently confirmed printer-01's bed clear and requested P3 start; attempt `cassette-r06-wave2-20261004-P3` was launched through the same guarded route. + +P3 passed approval and fresh readiness, uploaded, and received one start publication. Independent telemetry confirms RUNNING the exact P3 digest at 0%, 223 minutes remaining, layer_num 0. Its print_error remains the vendor-documented non-existent 83935249 code admitted by the reviewed readiness classification; this is not a claim that the raw error field is zero. No start was replayed. + +## 140 mm continuation after P3/P4 + +The operator reports P4 removed from printer-02 and P3 finished on printer-01. +All four tray panels have therefore been reported finished; assembly fit is not yet recorded. +Printer-01 cooling/removal confirmation remains pending. Next jobs are the two original +rear support arms E1/E2 on printer-01 and split crossmember B140L/B140R on printer-02, +from the reviewed residential 140 mm assembly. These supersede the old M1/M3 wave. + +Both plates retain the assembly's unit-scale geometry. XZ print orientation puts their +8 mm thickness upright and the retaining holes vertical; the two arms occupy 50 × 112 mm +and the two crossmember halves 141.72 × 90 mm before brim. The arms have 10 mm separation, +as do the crossmember halves. Files are prepared in `cassette-wave-3` under the existing +local preparation root, through the pinned Orca DAG route and the supported cassette PLA +profile (four walls, 20% gyroid, five top/bottom layers, 2 mm brim). These are parts for +the unpowered fit assembly, not qualified structural or thermal components. + +The subsequent queue before the socket revision was the M140 cradle, G140 guard, K1/K2/K3/K4/K6/K7 retainers, and +the four lower/upper corner-post pairs with four post retaining pins. Plate grouping, +orientation, material and slice readback for those later parts remain preparation work. +No old 80 mm fan frames or guards should be printed for this variant. + +Both next plates passed archive/G-code checksum, unit-scale transform, A1 mini 0.4 mm, +PLA 220 C / textured-bed 65 C, AMS index 0, 8 mm height, and independent deposited-path +bounds checks. The existing 65 C bed-temperature advisory is retained for the unpowered +prototype. Automatic supports are enabled and reported in the slice. + +| Printer | Plate | Slicer estimate | PLA | Project SHA-256 | +| --- | --- | --- | --- | --- | +| printer-01 | E1 + E2 | 56 m 27 s | 23.63 g | f1e5bcf8b2eb5574436ccd460f62ca15d43097829deb5163e6b3f30f27d21a3f | +| printer-02 | B140L + B140R | 1 h 52 m 46 s | 52.38 g | acaf7b25a4c49c3412043c2c20ae35e790d6f4ae3c9f0f6fb6ca70d55b7f7244 | + +Files and unexecuted launch scripts are staged at +`/home/briansrls/print-run-2026-10-04/cassette-wave-3` on srv1. Printer PR #13149 +remains open; the prior private token file is absent. A fresh private token file and +printer-01 removal confirmation have been requested. No approval request, upload or +start has been performed for these two jobs. + +### Wave 3 dispatch and assembly hardware + +The operator subsequently confirmed both beds clear, supplied fresh operator authentication, +approved both ntfy requests, and confirmed both printers active. Each workflow passed +approval/readiness and uploaded its exact staged digest. Both start publishers lost their +acknowledgements; neither was replayed. Independent telemetry confirms both exact digest filenames RUNNING: printer-01 +9%, 51 minutes remaining, vendor-documented non-existent error 83935249; printer-02 +4%, 107 minutes remaining, error 0. The private one-hour token file was removed. Durable start claims must remain in place. + +Suggested first-assembly hardware, calculated from the existing geometry rather than +fit-verified: four M3 × 16 mm machine screws with nuts and two flat washers each for the +8 mm tray seams; two M4 × 50 mm machine screws with nuts and two flat washers each for +the modeled 40 mm 140-mm fan/cradle/guard sandwich. Use non-countersunk heads and tighten +only snug against the plastic. Nut/washer selection and engagement still require assembly +review. These suggestions do not resolve motherboard standoff thread/height. Rear arms, +crossmember and corner posts retain their modeled printed pins. + +Future operator launches should use printer workflow revision `a96a2fd8b5` from PR #13149, +staged on srv1 at `/home/briansrls/print-run-2026-10-04/source-reconciled-a96a2fd8b5`. +It integrates bounded exact-filename RUNNING reconciliation after both acknowledged and +uncertain publishes, with durable raw reports and final receipt. The previous wave-three +scripts remain historical receipts and must not be rerun. The new workflow passed 35 local +checks; its full live start path has not yet been exercised. Use fresh attempts and the same +approval/bed-clear/claim requirements for future jobs. + + +The operator subsequently reports printer-01's E1/E2 plate finished. Record both arms as +reported completed, awaiting removal/inspection; do not schedule them again merely because +packing is replanned. Bed clearance for another plate has not been confirmed. Printer-02's +crossmember job has no new completion report in this update. + +Future batching requirement: use the [desired-set print orchestration plan](print-batch-orchestration.md) +to pack compatible outstanding items densely and reconcile quantities across both printers. +The sparse arms plate is a scheduling limitation of the manual queue, not a constraint that +each part family requires its own plate. The batch controller is not implemented yet. + +## Rear socket redesign after cleanup damage + +The operator reports both integral rear sockets were torn off during support removal. +P1/P2 are rejected for the original rear-arm connection; this creates exactly two +replacement panel demands. P3/P4, E1/E2 and B140L/B140R are not replacement demands. + +`cassette.rear_socket` now produces separate S1/S2 open channels, floor-down. +The rear panels contain no integral socket. Existing arms and their hole locations +are retained. One M4 through-bolt, washers and nut per corner replaces K1/K2; +M4 x 35 is the proposed length, pending actual engagement. Do not print K1/K2 for +this revision. Four tray seam clamps and two fan bolts remain unchanged. + +The side walls stop at the existing rail underside rather than overlapping it as +an integral union did. CAD must check separate-solid clearance, not merely validity. +S1/S2 can print without supports; the panel print orientation is a separate process +choice. Upright panels are experimental unpowered-fit prints, not qualified structural +parts. Compare sliced support/material and time against flat panels before release. + +The candidate overnight plate is replacement P1/P2 upright plus S1/S2 floor-down, +with individual named objects, all printed layer-by-layer. Slower wall speeds and +acceleration plus a brim address adhesion/motion risk; they do not establish loaded +joint strength. The durable arbitrary-set batch controller remains the separate +BATCH-1 through BATCH-6 milestone; a prepared multi-object plate is not that controller. + +Final preparation receipt: `artifacts/printing/socket-redesign-2026-10-04/` records +all compared slices, full project digests, named membership and model checks. +`overnight-batch` places P1/P2 on their rear edges (long vents vertical), S1/S2 +floor-down and K3/K4/K6/K7 head-down. The deposited-path readback confirms no +support extrusion attributed to either socket or any of the four pins. All eight +objects remain separately named in the 3MF. This is a single layer-by-layer plate. + +Selected under the operator's preference for fewer handling visits and longer +prints; the flat alternative remains cheaper in machine time and total material. +Sliced estimates are in `readback.json`; none is a measured physical print result. +The selected file is `cassette-socket-redesign/overnight-batch.3mf` in the local +2026-10-04 print-prep directory. No print was started by this preparation. Fresh +bed clearance and the reviewed approval/start route still apply at dispatch. + +The preparation's independent G-code reader was corrected to include leading-dot +numbers such as `E.0225`; previous P1–P4 and arms/beam slices were rechecked and +remain inside bed bounds. Slicer `first_layer_time` XML values were malformed; +time comparison uses total prediction and the readable G-code header instead. + +## Overnight plate started — 5 October 2026 + +Printer-01 is running `overnight-batch`: replacement P1/P2, separate S1/S2, +and K3/K4/K6/K7. These eight instances are now reserved by an active physical +print; do not schedule duplicates. The operator confirmed both beds clear and +approved the printer-01 ntfy request. Printer-02 was not started by this run. + +`artifacts/printing/socket-redesign-2026-10-04/start-receipt.json` binds the exact +project to the observation. The publisher hit its 20-second acknowledgement bound; +the automatic reconciliation's first read confirmed the matching filename and +RUNNING. The workflow exited successfully without republishing. The temporary +operator-token file was deleted after completion of the start workflow. Physical +completion, removal and acceptance of individual parts remain pending. + +Printer-02 preparation uses M140, G140, CU-1-1/CU-11/CU1-1/CU11 and +CP-1-1/CP-11/CP1-1/CP11: ten distinct parts, none in printer-01's active plate. +The four lower shoes remain unprinted; their clip-channel support removal needs a +better orientation. The rear shoes now carry matching open notches in the upper +and lower lips so the separate socket's through-bolt hardware can pass. CAD checked +a 10 mm diameter, 6 mm deep underside hardware envelope against both rear shoes. +The four residential-joinery witnesses still pass. The new batch prints upper posts +inverted and pins head-down; build-plate-only supports avoid the enclosed post bores. + +Printer-02's preparation receipt is `artifacts/printing/rear-posts-2026-10-05/`. +The local pinned-slicer workflow produced one ten-object plate, and the preparation +readback checked its checksum, material/machine settings, rigid transforms, complete +membership, deposited XY bounds and support attribution. This receipt is a manually +invoked preparation result. CAD kernel checks and sliced-toolpath readback are not +yet mandatory gates in the printer-start workflow; BATCH-3 must bind them to the +source/process/project before the start route can enforce that preparation evidence. + +## Printer-02 started — 5 October 2026 + +Printer-02 is now running the ten-piece rear-and-upper-posts plate. The workflow +accepted ntfy approval, checked current readiness and confirmed the exact filename +in its first post-start observation. `artifacts/printing/rear-posts-2026-10-05/start-receipt.json` +is the execution receipt. The publisher acknowledgement was uncertain; no start was +republished. The temporary token file was deleted. Reserve these ten instances along +with printer-01's eight; physical completion and per-piece acceptance remain pending. + +The final four lower posts were trialed lying on their sides. Their CAD source fits +the nominal placement, but Orca auto-arrangement emitted two plates and the one-plate +readback refused. This trial is not dispatchable. Resolve packing/process allowances +or prepare explicitly separate plates before offering it as ready; no start was sent. +The existing slicer command auto-arranges objects, so it does not preserve the supplied +packing as a binding placement. BATCH-2/3 must represent and enforce that distinction. diff --git a/docs/plans/cassette-server-psu-interface.md b/docs/plans/cassette-server-psu-interface.md new file mode 100644 index 00000000000..07593df3bc1 --- /dev/null +++ b/docs/plans/cassette-server-psu-interface.md @@ -0,0 +1,78 @@ +# Interchangeable server-PSU power cassette + +Investigation and design intent, 4 October 2026; PR #13295. This is a reference architecture, not a selected electrical assembly or released mount CAD. + +## Power availability preference + +The operator selects a single PSU as the default; redundancy is optional only if the incremental cost is worthwhile. FSP remains a reference, not a required supplier. Do not require a two-module redundant set for the first cassette. A redundant-capable PDB must have documented single-module operation, enable behavior and unused-slot airflow handling before being used with one module. + +FSP separately catalogs PDBs including FSP-PH51A and FSP-FC210CE. The [PH51A product page](https://www.fsp-group.com/en/product/crps/1745379392-1427.html) advertises cable customization, and its [datasheet](https://www.fsp-group.com/download/pro/FSP-PH51A_Datasheet.pdf) states that the wire harness can be customized. This establishes a vendor offering, not an off-the-shelf multi-node harness SKU or included cable set. Exact module compatibility, small-quantity availability, harness part numbers, price and single-module behavior remain procurement obligations. The protected per-node distribution remains our system requirement. + +## Independent power-cassette pairing + +Power is its own cassette type, independently selectable from the server cassette. There is no fixed one-PSU-per-board or two-boards-per-PSU ratio. A named power cassette advertises usable output at the selected AC input, output-interface identity, independently protected branch limits, overhead and reserve. Each node names one power cassette and one branch and declares its required load. Different node wattages can share a cassette; multiple power cassettes can serve separate node groups in one installation. Their outputs are not implicitly paralleled. + +`server_power_interface.pairing_screen` refuses duplicate supply identities, multiply assigned nodes, reused ports, absent supplies/ports, incompatible interface identities, branch overload and aggregate overload. Passing is capacity screening only: it does not retire the existing wiring, protection, startup, thermal, geometry or cable-reach obligations. The executable report includes a heterogeneous 300 + 200 + 100 W example against 780 W usable output with 20 W overhead and 20% reserve. Port ratings are declared example inputs, not selected connector ratings. Changing assignments also requires rerunning harness routing and placed-mass/stack analysis. + +## Boundaries + +The cassette should accommodate different power assemblies through **replaceable inserts**, rather than assuming every server PSU has the same connector. Keep the column attachment and node-facing DC interface consistent. Each insert owns its PSU/cage envelope, attachment, insertion/removal clearance, ventilation, cord retention and strain relief. Each electrical adapter owns the exact mating PDB/backplane, enable/sense/standby behavior, output harness and management interface. A change of PSU family requires review of both adapters. + +```mermaid +flowchart TD + AC[Mains cord and protective earth] --> PSU[Vendor metal PSU modules and matched cage/PDB] + PSU --> BUS[Rated main DC distribution] + BUS --> A[Node A protected and isolatable branch] + BUS --> B[Node B protected and isolatable branch] + A --> CA[Keyed locking DC connector] + B --> CB[Keyed locking DC connector] + CA --> HA[Short internal harness to board 12 V inputs] + CB --> HB[Short internal harness to board 12 V inputs] + CTRL[Station enable and PMBus controller] --- PSU +``` + +The initial node service interface is one accessible, shrouded, keyed locking DC connector, with local isolation and status, and two Ethernet connections. Select its current/temperature rating, wire/crimp system and mating life from an actual family. Do not equate a similar connector shell with EPS/PCIe electrical compatibility. Shut down and isolate a node before disconnecting it. A PSU module being hot-swappable does not qualify the board connection for live mating. + +Blind mating can follow: it needs guides/floating alignment, contact sequencing, inrush/precharge/discharge behavior and an interlock. Do not make a printed latch perform electrical sequencing implicitly. Keep connector access outside the corner-column joints and the temporary support/extraction swept volume. Use replaceable clips and captive inserts, minimizing small assembly screws. + +Keep the vendor metal PSU enclosure/cage and its earthing intact. Printed parts locate and retain that assembly; they do not replace its mains enclosure. The preferred station position remains the bottom of the column, outside node extraction. Its actual mass and position must enter the component fold once a BOM is chosen. + +## Candidate references, not an interchangeability list + +| Reference | Established | Unresolved | +| --- | --- | --- | +| Existing Mt. Collins PSU | Repo product brief: two redundant units, Platinum, up to 2000 W, PMBus 1.2 | Actual installed model, main-rail ratings, pinout and matching cage/PDB | +| Chicony S15-550P1A / S550E004L listing | Seller identifies 550 W | Primary mating-interface documentation and compatible PDB; do not substitute another Chicony model's pinout | +| FSP800-50FS complete redundant assembly | Main 12 V, 65 A = 780 W at listed 100–240 Vac | Exact purchased variant/drawing, output harness and per-node protection | +| FSP1200-50FS complete redundant assembly | Main 12 V: 80.5 A = 966 W at 100–127 Vac; 97 A = 1164 W at 180–264 Vac | Same integration obligations; no inferred rating in the undocumented input gap | + +Primary sources: [FSP800-50FS datasheet](https://www.fsp-group.com/download/pro/FSP800-50FS_Datasheet.pdf), [FSP1200-50FS datasheet](https://www.fsp-group.com/download/pro/FSP1200-50FS_Datasheet.pdf), [FSP catalog](https://www.fsp-group.com/download/catalog/IPCPSU.pdf). The current catalog gives a 265 × 76 × 84 mm assembly envelope, larger than the older sheets' 250 × 76 × 83.8 mm. Reserve the larger body for planning; reconcile the exact drawing before creating mount geometry. Body fit alone excludes cord bends, airflow and withdrawal clearance. + +[Chicony's CRPS family table](https://www.chiconypower.com/en/item/view?item_id=580) illustrates why labels alone are insufficient: its R550AV02P lists 42.6 A on main 12 V (511.2 W), with standby separate. This is **not** evidence for the seller's S550E004L. PMBus specifies management, not a universal mechanical/power pinout. Prefer a complete vendor-matched assembly, or an explicitly documented module/backplane pair. + +## Capacity screen + +The executable report in `stack_mass_review` includes the following examples. The 300/400 W loads are declared node scenarios, not measured fleet loads; the 20 W overhead and 20% reserve are planning inputs, not an electrical-code rule. + +| Two nodes | Required including overhead/reserve | Reference main rail | Result | +| --- | --- | --- | --- | +| 300 W each | 744 W | 800-class assembly: 780 W | Capacity screen passes only | +| 400 W each | 984 W | 800-class assembly: 780 W | Insufficient | +| 400 W each | 984 W | 1200-class, 115 Vac: 966 W | Insufficient with this reserve | +| 400 W each | 984 W | 1200-class, 230 Vac: 1164 W | Capacity screen passes only | + +Even granting all 550 W to the main rail, two 300 W nodes exceed it before losses. Lower capped nodes may fit, but 550 W is not the unrestricted two-node default. Two 550 W modules in 1+1 redundancy still provide at most one module's relevant rating after a failure. The model removes the largest module and caps the remainder by the distribution-path rating; arithmetic never grants permission to parallel unmatched supplies. + +## Electrical adapter obligations + +Select exact module/PDB part numbers and pinout; main/standby rails and minimum loads; enable/sense behavior; sharing and ORing; PMBus addresses and one management owner; coordinated per-node fault isolation; branch connector and wire ratings; inrush/discharge; earthing/strain relief; cooling and insertion envelope; and board input population. These remain typed open obligations even when a capacity example passes. + +Use a station controller for shared supply enable. Do not wire multiple hosts' PS_ON outputs together. The ASRock DC-IN route and its front-edge 8-pin inputs remain the board interface modeled by `node_power_feed`; do not connect an unverified PSU standby rail to its signal header. A matched PDB does not by itself establish independent branch protection: a fault on one node must be evaluated against upstream shutdown behavior. + +## Next implementation + +1. Select a documented complete 800–1200 W-class reference assembly against node load scenarios, AC input and required redundancy; obtain its exact mechanical drawing and output harness specification. +2. Implement the common cassette attachment and removable mechanical insert, keeping the chosen vendor cage intact. Reserve routing and service clearances, not just a rectangular PSU body. +3. Select and model the protected branch assembly and common node connector; bind each supported PSU adapter to its compatibility evidence and limits. +4. Fold the selected assembly, harness and printed insert masses/positions into stack statics; evaluate local thermal exposure before selecting printed material. +5. Generate/review adapter CAD and an unpowered assembly prototype. Current r06 tray printing remains a separate unpowered cassette fit exercise. diff --git a/docs/plans/milk-crate-stack-design.md b/docs/plans/milk-crate-stack-design.md new file mode 100644 index 00000000000..c58ef4195b2 --- /dev/null +++ b/docs/plans/milk-crate-stack-design.md @@ -0,0 +1,215 @@ +# Integrated milk-crate cassette: review and evidence + +PR [#13295](https://github.com/gunb-ai/gunbc/pull/13295), based on +`session/cassette-joinery-review` (#13222). This is an architecture and calculation review, +not a release to print a populated stack. The earlier separate-frame proposal #13283 is superseded. + +## Assembly and service topology + +The cassette carries its own four corner columns. There is no permanent outer rack, duplicate +runner system, or extra set of trays. Column feet register on the next cassette; broad bearing +faces carry compression, while removable keys retain alignment. Retaining pins must not carry +the stack's gravity load. Prefer open ribs/L-sections and local gussets over solid 20 mm posts; +final section, layer direction, joint stiffness and creep still require qualification. + +![End-to-end topology and supported middle-node removal](milk-crate-topology.svg) + +```mermaid +flowchart TB + AC[Site mains] --> PSU[Bottom power cassette: enclosed supply and protective earth] + PSU --> PDB[Protected distribution: one isolatable branch per node] + PDB --> D1[Node 1 front DC disconnect] + PDB --> D2[Node 2 front DC disconnect] + PDB --> DN[Node N front DC disconnect] + D2 --> EPS[Internal pigtails to front-edge board 8-pin inputs] + SW[Independent network switch] --> N1[Node 1 rear data / management] + SW --> N2[Node 2 rear data / management] + SW --> NN[Node N rear data / management] + FAN[Node 2 rear fan bank] --> EXT[Side route / extension] + EXT --> HDR[Node 2 front fan header] +``` + +A direct stack cannot lose a middle structural member while the upper stack remains unsupported. +The minimal-material answer is **temporary service supports**, shared between columns: + +1. Shut down and isolate only the target node. Disconnect its external DC and network leads. +2. Install two floor-supported service trestles outside the target's extraction path. A crosshead + engages the **upper cassette's corner bearing pads**, not its motherboard tray or fan arms. +3. Lift the upper group only enough to disengage the registration keys; retain it against tipping. + Every upper branch needs that lift allowance, bend radius and strain relief. No cable is a support. +4. Withdraw the target toward the front. Upper nodes' independent cable drops stay connected. +5. Reinsert, reseat, transfer the load back, remove the service supports and reconnect the target. + +This is a proposed service sequence, not qualified live service. Until its complete load path, +retention, lift mechanism and cable sweep are checked, use an unloaded demonstration only. +Without the supports, removal remains LIFO and requires moving the nodes above. + +**Geometry to resolve before a new cassette print:** the old rear arm sockets lie under the tray +corners, and 20 mm posts centered on the current tray edge intrude into the board envelope. Move +corner bearing sections outward or redesign the socket together with the post; do not simply +extrude the old corner. A review candidate uses post centers around x = ±142 mm, z = ±159 mm +with a 20 mm envelope (about 304 × 338 mm bearing footprint). These are proposed clearances, +not released dimensions. The rear fan bank extends farther aft. One authoritative StackJoint, +actual sections and continuous extraction sweep are owed by the geometry revision, which must +also replace `rack_assembly.fixed_parts` in the assembly-review root. The existing r06 meshes +and hosted rendering are historical and do not yet show this integrated structure. + +## Cable space is part of the design + +The [ASRock manual](https://download.asrock.com/Manual/ALTRAD8UD-1L2T.pdf) places the three +8-pin power inputs and five fan headers on the edge opposite rear I/O. It permits DC-IN without +the 4-pin signal lead; high-load input population and header current limits remain unresolved. + +| Run | Routing and service consequence | +|---|---| +| Shared supply → node | Independent front-side drop; rated, keyed disconnect and strain relief at each cassette. No power daisy-chain through a removable node. | +| Disconnect → board | Short internal pigtails to the front-edge inputs. Select complete contacts, crimps, wire and protection together; do not infer compatibility from connector appearance. | +| Network / management | Independent rear-side drops. Reserve plug bodies, latch access and bend radius before the fan bank; escape around a side instead of passing through fan apertures. Link count comes from the selected networking topology, not all four RJ45 sockets automatically. | +| Rear fans → board | Route along a side to the front header. The modeled rear fan offset makes the straight route about 371 mm before service/bend allowance: a 400 mm fan lead is insufficient with 100 mm allowance. Include the extension in the harness BOM mass allowance. | +| Cooler fan | Its own internal board-header lead; do not add its airflow to the rear bank as if they were independent parallel fans. | +| Station wiring | Mains inlet/PE, distribution wiring, branch protection, disconnects and station cooling all belong to the power cassette BOM. | + +The existing `harness_census` is a **lower-bound route/count screen**, not a cut list. It still +models the old rear-raceway/LIFO alternative for comparison. Exact connector endpoints, network +selection, exterior drop clips, fan lead extension, cooler lead and station wiring must be bound +before a harness order. Middle removal cannot reuse its LIFO disconnect count as an admission. + +## Mass is a fold over components + +`component_mass.node_components` derives all printed-part occurrences and cooler/fan placements +from the cassette model, adds the board, CPU, DIMMs, SSD, mounting hardware, fasteners, internal +harness and corner structure, and takes the DIMM count from `HostMemoryPopulation`. +`stack_mass_review` consumes the actual `srv3_memory_population` and `srv4_memory_population`: +both have **eight Samsung M393A8G40D40-CRB DIMMs**, not an assumed four. + +Mass rows distinguish vendor nominal values, sourced bounds, explicit planning estimates and typed design gaps. Estimates remain tagged and counted in the fold. +The fold rejects missing mass, duplicate identities/measurements, stale measurement IDs, +invalid ranges and empty inventories. A complete mass sum still reports provisional positions +and nominal vendor inputs: it is not a complete structural qualification. + +| Component | Evidence / remaining work | +|---|---| +| W1 active cooler including its fan | [Dynatron](https://www.dynatron.co/product-page/w1): 612 ± 10 g. Do not add its fan again. | +| Three rear P8 PWM PST fans | [ARCTIC specification](https://www.arctic.de/media/84/14/68/1697784882/Spec_Sheet_P8_PWM_PST_EN.pdf): 81 g each, nominal; manufacturing tolerance not provided. | +| srv3 970 EVO Plus / srv4 970 EVO SSD | Samsung brochures give **maximum 8 g**, represented as a bound, not a measured 8 g restoring weight. | +| Board | **910–1700 g planning range**, using the operator-provided bracket. Conflicting online item/shipping weights do not establish a bare-board tolerance; this is explicitly an assumption. | +| CPU | **114 g typical Altra Max package mass**, Ampere section 4 p.17. The earlier Altra family is 112 g. These are nominal values, not tolerances. | +| DIMMs | **20–100 g each planning range**, multiplied by eight. This is a deliberately broad engineering allowance for the bare module, not a vendor mass claim. Samsung publishes its geometry; retailer shipping mass is not used as component mass. | +| Printed pieces | Use revision/profile-bound slicer or measured part mass. Positive primitive volumes include overlaps and ignore cuts/infill; they are not actual printed mass or conservative tipping input. | +| Hardware, harness, connectors | Explicit missing rows, filled from the selected BOM and routed lengths or measured sets. No silent zero. | +| Power cassette | Separate PSU, enclosure, distribution and wiring rows. Supply selection is still open, so mass and CG are open. | +| Service supports / exterior cables | Separate service-case loads and cable forces, not mass silently assigned to a node. | + +The already-generated PLA P1/P2 G-code reports **156.51 / 147.82 g of filament used**. Those are +job consumption estimates, potentially including support/purge, and belong to r06/profile/material; +they must not be copied onto a redesigned PETG corner cassette as actual part weights. + +**No operator weighing or disassembly is requested.** Source research, existing CAD/slicer output, +BOM calculations and explicit design allowances are the preparation route. The earlier request +to weigh the board/DIMM and the default `BOARD_AND_CPU` measurement placeholder are withdrawn. +The runtime report now keeps BOARD and PROCESSOR separate, supplies the available masses and +planning ranges automatically, and exposes each row's evidence class and basis. + +The resulting **hardware-only planning subtotal is 2.029–3.487 kg per node** (board, CPU, +eight DIMMs, W1, three P8 fans and SSD). It excludes prints, fasteners, standoffs, harness and the +power cassette, and is not a guaranteed interval. Those remaining design/BOM rows stay visible; +they are our modeling work, not requests for the operator to collect weights. + +Research references and interpretation (4 October 2026): + +- [Ampere Altra Max datasheet, §4 p.17](https://amperecomputing.com/assets/Altra_Max_Rev_A1_DS_v1_25_20240130_73cfcc518a_4705c00046.pdf): 114 g typical, confirmed in the primary indexed text; also present in issues 1.15 and 1.30. The direct browser fetch exceeded its size limit. +- [Samsung module datasheet, §18 p.39](https://download.semiconductor.samsung.com/resources/data-sheet/DS_64GB_TSV_DDR4_4Gb_D_die_RegisteredDIMM_Rev13.pdf): indexed primary drawing gives 133.35 × 31.25 mm and 3.9 mm maximum thickness. It supplies geometry, not the assumed 20–100 g mass range. Direct URL retrieval currently returns 404; the indexed primary excerpt remains available. +- Board retailer results reproduce 0.91 kg and 1.7 kg figures, with other conflicting values and packaging ambiguity. We retain the user's bracket as a planning assumption; none is promoted to an ASRock specification. +- DIMM seller results include explicitly labeled shipping masses (0.20 lb and 1 lb). Neither is accepted as a bare-module measurement. The mass range is a product-side assumption, kept outside Samsung's extdeps facts. + +Uncertainty remains in scenario sweeps instead of generating another physical-measurement request. + +## Force calculations and their boundary + +`placed_mass_model` folds mass and x/y/z first moments. For each supporting interface, the load +set must contain all components above it, in that interface's frame. The bottom power cassette +raises every node and contributes its own mass at the base; it cannot be added as weight without +also moving the node positions. Check every interface and the complete stack, not only the floor. + +For a rectangular support region, four directional restoring moments use each edge's distance +from the actual CG. Top push uses the **applied-force height**; base acceleration uses the **CG +height**. Four equal vertical contact stiffnesses give reactions proportional to +`1 ± x/a ± z/b`; negative reactions require a unilateral contact re-solve, not clamping to zero. +Equal quarter loads are valid only for a centered, symmetric case under that assumption. + +`load_from_ledger` refuses an incomplete mass ledger. Endpoint scenarios are sensitivity cases, +not an interval proof: putting every mass at its maximum can increase the restoring moment. +The previous uniform `stack_mechanics` model is retained as a labeled approximation, with the +push-height and bottom-station offsets corrected and bending-strength-as-compression removed. +Its result is now `StackScreeningOnly`; its search is `max_screened_height`. + +**No defensible safe stack height is established yet.** Remaining constraints include contact +footprint and print orientation, joint shear/bending, pillar buckling with real end conditions, +local compression, creep at measured temperature, friction/sliding, cable pull, chosen bump/tilt +load cases, and the temporary service-support state. Rigid-body statics does not substitute for +FEA or physical creep/joint testing. A heavy base may help; a lighter base raising heavier nodes +can make stability worse. The earlier “10 high” and “PLA/PETG admit the same height” are withdrawn. + +## Thermal and material interpretation + +The user's candidate is **SUNLU High Speed (likely Matte) PETG**; exact SKU/lot remains to match. +Its [primary TDS](https://media.sunlu.com/prod/20260330/ccf53a55-5b58-49fa-b1d9-08f94c0a4b28.pdf?filename=TDS) +reports HDT **70 ± 2 °C at 0.45 MPa**, on specified XY printed specimens. High printing speed is +not a thermal rating. HDT is neither melting temperature nor a long-duration loaded-part limit. +The model uses a vendor-neutral filament row instead of borrowing Bambu PETG values. + +A 100 °C CPU junction does not mean a 100 °C fan mount. Conversely, a **measured 100 °C mount** +is well beyond this PETG's HDT screen. Bulk exhaust rise `Q/(rho V cp)` is useful only with its +assumptions: installed airflow must come from the fan/system pressure-curve intersection or a +measurement. Catalog maximum free-air flow times PWM percentage does not establish it. +The hypothetical aggregate-flow cases are now labeled `AssumedAir...`; percentages are fractions +of free-air flow, not fan duty. Unknown local temperature stays `InstalledThermalUnresolved`. + +The P8 catalog operating ambient ceiling is **40 °C**, independent of the filament. A 35 °C +inlet plus even the hypothetical ~8 K bulk rise already needs attention at the fan inlet. +Measure inlet, fan inlet, hottest mount and column joint under sustained load and a controlled +fan-failure shutdown transient. Radiation, conduction, hot recirculation and creep remain open. +Do not select a BMC PWM floor or authorize PLA/PETG operation from the free-air calculation. + +## Electrical corrections + +The Molex primary 5556/5558 table varies rating with circuit count, terminal material and wire. +For an 8-circuit 18 AWG screening row the lower listed material rating is **6 A/contact**, not +the family maximum 9 A. This still does not identify the actual board header, crimp or PCB path. +The feed calculation now rounds constant-power current up at the chosen minimum voltage; +300 W at 11.4 V is 26.316 A. Zero derating refuses. `NodeFeedScreeningOnly` is not approval of +wire ampacity, parallel current sharing, connector/pigtail drop, protection, transient load or +board input population. The ATX voltage band is a screening assumption for direct DC-IN. +DC-return bonding and node bonding/ESD requirements depend on the selected supply design; +“12 V only” does not erase those obligations. + +## Execution and next geometry boundary + +Run the component-mass, stack-mechanics, stack-feed and exhaust-thermal witness entries under +`dag/test/claim/printed_chassis_*_witness_test.dag`. The executable fleet inventory report is: + +``` +gunbc run --source-root dag --source-root src/v2 \ + --entry dag/gunbc/product/printed_chassis/stack_mass_review.dag +``` + +The new mass model is consumed by that report and the placed-load refusal path. SUNLU and fan +ambient rows are consumed by thermal witnesses. New integrated corner geometry and the +support-assisted extraction mechanism are a **named frontier for the next CAD revision**, not +claimed to have been printed, strength-tested, or incorporated into the hosted r06/r07 meshes. +Their landing must replace the old fixed-rack review root, bind the mass ledger to finalized +parts, and run collision/continuous-sweep/bed-fit checks before another cassette print. + +## Interchangeable PSU assemblies + +The power cassette uses replaceable mechanical inserts and PSU-specific electrical adapters behind a common protected node-facing interface. See [the server-PSU investigation and topology](cassette-server-psu-interface.md) for documented assembly references, AC-input derating, redundancy arithmetic and unresolved interface obligations. This supersedes treating a PSU body envelope alone as a complete power-station design. + +## Residential cooler variant + +Use NH-U14S AMP-4926 as the requested quiet-cooler reference, not a fixed 4U height. [Noctua's specifications](https://www.noctua.at/en/products/nh-u14s-amp-4926/specifications) give **165 × 150 × 111 mm** (height × width × depth) and **1136 g with both fans**. The vendor lists 5U compatibility and discontinued status. The 160 mm / 797 g heatsink-only figures and packaging dimensions are not assembly inputs. Its airflow is perpendicular to the socket long axis; final placement must align that with the rear exhaust route. The three 80 mm rear fans remain separate components. + +`cooler_configuration.residential_cooler` selects this variant; `stack_geometry.stack_pitch_for_cooler` substitutes the vendor envelope, and `node_components_for_cooler` substitutes its mass once, including both fans. The existing W1 fleet observations remain intact. The review report includes the Noctua variant's derived pitch and component fold. Contact-plane height (currently a 30 mm review assumption), pose, clips, DIMM interference, connector routing and service clearance remain unresolved. The body box is a clearance reservation, not detailed heatsink CAD or proven compatibility. No rack-unit rounding is imposed. A taller/heavier variant must use its own pitch, harness lengths and placed mass in later column analysis; the W1 height result cannot qualify it. Printed corner geometry still requires implementation. + +Validation: all 10 server-power/cooler witnesses passed, and `stack_mass_review` emitted the combined report. At the current provisional 30 mm contact plane, 15 mm underside projection and 20 mm service clearance, the Noctua planning pitch is 230 mm (not a required rack size or a final minimum). The hardware-only subtotal becomes 2563–4001 g with the existing board/DIMM planning ranges; prints, connectors and the station remain additional modeled obligations. + +Power allocation is now independent of column/node count: assign each node to one compatible protected output of a named power cassette, and validate per-port and aggregate budgets. Multiple power cassettes may serve separate groups; no implicit output paralleling. See the [pairing model](cassette-server-psu-interface.md#independent-power-cassette-pairing). diff --git a/docs/plans/milk-crate-topology.svg b/docs/plans/milk-crate-topology.svg new file mode 100644 index 00000000000..7bb0267f12d --- /dev/null +++ b/docs/plans/milk-crate-topology.svg @@ -0,0 +1 @@ +Integrated cassette stack, wiring and middle-node serviceConcept schematic, not manufacturing geometry. Four cassette corner columns carry gravity loads. Independent front DC and rear network drops leave other nodes connected while temporary floor-supported trestles support the upper group during removal.One cassette, one structural layerMilk-crate architecture • independent cable drops • temporary supports for a middle-node popCONCEPT REVIEW — not scaled CAD, structural qualification, or a print-ready assemblyNormal operation · front-side perspectiveService · support the upper group firstNode 1Node 2Node 3Power cassette at baseEnclosed PSU + protected branchesMass / supply selection unresolvedNode 1Node 3Power cassette at baseEnclosed PSU + protected branchesMass / supply selection unresolvedNetwork switchFront: DCRear: networkOne branch per nodeMainsColumns belong to the cassette; no permanent outer frame.Front power headers • rear fans / I/O • side routes around fan bankTarget node outShutdown + isolate firstUpper leads stay connectedwith measured lift slackRemovable supportThe upper group's load goes to the floor through service supports.Lift / retention / extraction sweep and powered service still need qualification.Model before releaseMass: board + CPU, 8 DIMMs, cooler, SSD, fans, every print, fasteners, harness, columns, PSU and distribution.Forces: every interface, actual mass locations, corner reactions, tipping, sliding, cable pull and service-support state.Heat: local fan / mount / joint temperatures and loaded creep. PETG HDT is not a continuous operating rating. \ No newline at end of file diff --git a/docs/plans/print-batch-orchestration.md b/docs/plans/print-batch-orchestration.md new file mode 100644 index 00000000000..bd07c542a61 --- /dev/null +++ b/docs/plans/print-batch-orchestration.md @@ -0,0 +1,96 @@ +# Desired-set printing and automatic plate packing + +Requested after printer-01 completed the E1/E2 plate on 4 October 2026. This is the +next workflow milestone, not an implemented scheduler or permission to start a print. +The operator should request parts and quantities once, then clear beds when notified. + +## Contract + +Input is a desired multiset of versioned part instances, compatible available printers, +and an admitted process/material policy. Output is an explainable packed plate plan, +per-printer schedule, remaining quantities and durable execution/handling receipts. +Rerunning the same desired set reconciles existing work instead of printing duplicates. + +Use assembly part identities and content digests from the existing CAD/export path, +`manufacturing_manifest` for physical attribution, the fleet's printer identities, +and the reviewed shared start/approval/claim/reconciliation route from PR #13149. +Do not introduce another publisher or another printer inventory. `bed_partition` divides +an oversized design into manufacturable pieces; packing places those resulting pieces +on plates. Both consume the same bed/process allowances but answer different questions. + +## Packing and scheduling + +1. Expand quantities into stable required-instance slots, each bound to geometry revision, + material/process and intended orientation. Reserve slots already assigned to an active, + claimed, uncertain or completed-but-not-yet-inspected print. Accepted usable inventory + satisfies matching slots. Rejecting a failed part creates an explicit replacement need. +2. Partition only by genuinely incompatible printer, nozzle, material and process constraints. + Combine different parts and requested projects when those constraints and handling allow it. +3. Pack the remaining instance footprints deterministically. Evaluate allowed in-plane rotations; + changing the load-bearing print orientation requires separate admission. Include supports, + brim, object gaps, bed margins and printer exclusion zones. A whole-plate, layer-by-layer print + avoids introducing sequential-print toolhead swept-volume assumptions. +4. Minimize required bed-clear cycles first, then use sliced duration to balance the two printers + and respect assembly priorities. Do not make nominal area utilization the sole objective: + a dense fragile plate that fails or mixes incompatible processes is worse. +5. Start with a deterministic rectangle packer over conservative oriented envelopes, using several + orderings and scoring the resulting plans. Later contour nesting can improve density. Label + the algorithm and packing quality; do not claim a globally optimal solution from a heuristic. +6. Slice candidates and independently read back complete deposited-toolpath bounds and separation, + machine/nozzle, temperatures, material mapping, scale, supports, duration and material usage. + If generated supports/brims exceed the reserved footprint, repack or split before admission. + Preserve object/instance attribution rather than flattening unrelated parts into an anonymous STL. +7. Keep both queues prepared ahead. Reschedule only unclaimed work when a printer becomes free; + never mutate the membership or digest of an approved/claimed/running plate. + +The recent arms and crossmember plates illustrate the opportunity: their modeled envelopes +could be arranged as a 141.72 × 90 mm beam group and a rotated 112 × 50 mm arm group, with +10 mm between groups, giving approximately 141.72 × 150 mm before outer brim. This is an +unsliced packing candidate, not proof of admitted support/adhesion or a request to reprint them. + +## Durable execution and physical handling + +The batch reconciler owns stable required-instance slots. A plate attempt atomically reserves +its member slots across both printers before invoking the existing approval/start boundary. +A shared persistent lock/transaction prevents two schedulers from claiming the same instances. +Store plate membership, geometry/process/3MF digests, placement, printer, approval identity, +start claim and observations durably. A caller-selected output directory cannot reset identity. + +Separate batch demand, plan revision, plate attempt and physical print instance. Plan IDs may +change when packing improves; demand-slot reservations and completed inventory must not reset. +A new plate attempt cannot reclaim an unresolved old attempt's slots merely by using a new ID. + +Lifecycle: planned -> prepared -> awaiting bed-clear/approval -> claimed -> running-confirmed +-> completed-awaiting-removal/inspection -> accepted inventory. Upload/start uncertainty enters +reconciliation-required, retaining the claim and slot reservations. Explicit pause, failure, +partial failure and rejection have named outcomes; no automatic republish follows a timeout. +Completion alone neither proves bed clearance nor proves all objects are usable. Partial failure +can accept intact objects and replace only rejected instances, with an explicit operator record. + +Use the existing fresh bed-clear ntfy approval as the operator's combined clear-and-continue +interaction for the next immutable plate. Notify completion and present the next packed plate +there. A1 minis still need physical part removal; this design does not assume automatic unloading +or infer a clear bed from FINISH, IDLE, a timer or an old confirmation. Approving a whole batch +must not silently authorize future uncleared beds. + +## Implementation tasks + +| Task | Deliverable | Acceptance | +| --- | --- | --- | +| BATCH-1 | Versioned desired-set and inventory reconciliation in DAG | Same request/restart/plan revision cannot duplicate accepted, active, claimed or uncertain slots; changed geometry/material cannot consume incompatible inventory. | +| BATCH-2 | Deterministic multi-object plate packer and review projection | Every required unreserved instance placed exactly once or explicitly refused; rotations, overlap, exclusions, incompatible processes and brim/support boundaries tested; report unused area and algorithm limits. | +| BATCH-3 | Multi-object CAD/slicer realization and readback | Preserve per-instance IDs and unit scale; fail/repack when actual toolpaths exceed a plate's admitted envelopes; sliced time/mass feed scheduling. | +| BATCH-4 | Durable two-printer batch controller over the existing guarded start route | Concurrent schedulers reserve once; crash recovery tested before/after upload, claim, publish and completion; lost ACKs reconcile exact filenames without replay. | +| BATCH-5 | Completion, partial acceptance and bed-clear continuation | One clear-and-continue interaction per plate; distinguish finished from usable/removed; replace only rejected items; no start from stale bed-clear evidence. | +| BATCH-6 | Shadow plan and one authorized packed live plate | Compare full remaining cassette plan with manual baseline, record plate count/handling visits/estimated duration, then exercise recovery without issuing duplicate physical commands. | + +Deliver BATCH-1/2 as pure planning first; BATCH-3 produces reviewable slices; BATCH-4/5 are the +execution integration; BATCH-6 is the release gate. Do not label the feature implemented until +all six are complete. No extra gauges or speculative duplicate parts should fill a plate. + +The subsequent socket-cleanup incident adds a process objective: compare structural +orientation candidates using sliced support mass, total material, duration, trapped +support accessibility and handling risk. Do not reduce infill or rotate loaded joints +solely to maximize nominal bed occupancy. Prefer longer useful multi-object plates +when compatible, as explicitly requested by the operator. Damaged P1/P2 become explicit +replacement demands; retain all other completed/active instance reservations. diff --git a/docs/plans/printed-chassis-program.md b/docs/plans/printed-chassis-program.md index b0e3efd6fd6..a26aead8415 100644 --- a/docs/plans/printed-chassis-program.md +++ b/docs/plans/printed-chassis-program.md @@ -1,6 +1,272 @@ +**Current execution queue:** [CAS-1–CAS-7](cassette-completion-tasks.md) tracks shared profiles, height-related rear cooling, integrated stacking, power/harness placement, qualification and print release. + +**Unified multi-platform design, 4 October 2026:** [cassette profiles and consolidation](cassette-platforms.md) defines one shared design for ALTRAD8UD, GH200 P4261, Mt. Collins and Mt. Jade. New platforms retain their native power/management assemblies where appropriate; no copied board-specific tray generators. + # Printed node chassis — program plan -**Pinned plan. Updated 2026-09-02.** Printers land **2026-09-03**. +**Current review: 4 October 2026.** The operator requested fewer screws and faster assembly. +Revision 06 uses 17 printed pieces, four tray clamps, six fan/guard bolts and five broad-grip +retaining pins. See [cassette joinery review](cassette-joinery-review.md). Earlier counts below +are dated history. No further gauge sequence or cassette print is scheduled by this review. + +**Pinned plan. Progress reconciled 2026-10-03.** Printers are in use; the operator has the +printed standoff gauge in hand. The operator reports that the existing gauge aligns with numbered holes 1 and 3. + +## Current progress — 2026-10-03 + +Evidence: merged PRINT-N commit `b242faf7655` (#10680), current printed-chassis modules, +and the operator's 2026-10-03 report. Historical execution results are not tests rerun today. +This dated status supersedes the pre-arrival implementation narrative below. + +- **CAD bootstrap and measured export profile landed.** PRINT-N records CadQuery 2.8.0 running + through user-owned micromamba/conda-forge and measured tessellation observations in + `product.printed_chassis.export_profile`. The old kernel blocker is resolved. Full STEP/3MF + solid-readback acceptance is not established by that fact and remains to be audited. +- **Physical printing reached.** PRINT-N records printer-02 reporting `RUNNING` on the gauge + project after FTPS delivery and MQTT start through the modeled path. The operator now confirms + the gauge is in hand. No authenticated printer operation has been performed in this session. +- **Coupon feedback recorded.** `hole_fit_measurement.operator_ladder_reading_2026_09_04` + records M3 interference / tight / free at 3.00 / 3.10 / 3.20 mm nominal rungs. Printer attribution + remains absent; this is not universal hole compensation or qualification of both machines. +- **Initial pair fit observed, 2026-10-03.** The operator reports that both gauge holes align + perfectly with numbered holes **1 and 3**, with the gauge underneath the motherboard and no + screws installed. The supplied annotated image identifies these as **F and C**. This is an + operator-reported alignment pass on that pair; it does not establish loaded support, exact + tolerances, the other holes or the full FIT milestone. One board is sufficient for this check. + +### Numbered reference and observed pair + +The repo already cited the operator's Drive image in +`extdeps.boards.asrock_rack.asrock_altrad8ud_standoff_figure_authority`. A byte-for-byte local copy +is now [ALTRAD8UD_standoff_grid.png](../extdeps/asrock_rack/ALTRAD8UD_standoff_grid.png), retrieved +2026-10-03 from [the original Drive file](https://drive.google.com/file/d/1z36tmUWAwOwXWy6ASmaPWJNzKoZggBs8/view). +Use the image's numbers in operator instructions. Its annotated decimal coordinates are a visual +reference; they do not replace the standard's exact coordinate authority. + +| Image hole | Model location | +|---|---| +| 1 | F | +| 2 | M | +| 3 | C | +| 4 | H | +| 5 | L | +| 6 | B | +| 7 | R | + +**Observed:** 1–3 (F–C) aligns simultaneously underneath the board, without fasteners, per operator. +**Next candidate:** 3–4 (C–H). Other prepared checks are 4–5, 5–2, 3–6 and 4–7. +No new photo is required to identify the pair already reported against this reference. + +### Prepared follow-on experiments — 2026-10-03 + +`product.printed_chassis.fit_gauges` selects five additional gauges using the existing standard +location authority, gauge admission and CadQuery emitter. They are spacing experiments, not confirmed +mounting locations or qualified structural parts. Start with C–H after the existing F–C check passes; +the others extend coverage to the remaining inferred holes. Each is a separate part so it can be +identified by its file and labeled before leaving the bed. + +| Pair | Question answered | +|---|---| +| C–H | Does the rear-to-middle spacing agree on the C/H/L column? | +| H–L | Does the middle-to-front spacing agree on that column? | +| L–M | Does the front-row spacing agree? | +| C–B | Does the additional rear hole agree? | +| H–R | Does the outboard middle hole agree? | + +J remains excluded and S remains unobserved. A connected set of pair distances does not fix every +relative angle; the assembled adjustable fixture still must confirm the full map. Do not install +extra standoffs merely because the standard has a named location. + +**Measurements to gather before a mounting fixture:** + +- Identify the seven candidate holes (F, C, B, H, R, L, M) on one physical board; record any absent + or unexpected hole and pair mismatch. No need to disassemble several boards for this step. +- Actual standoff thread, height from support to board underside, and contact-pad/shoulder diameter; + identify the screws being used and their usable engagement. Do not infer these from the M3 coupon. +- Highest underside protrusion and clearance around each support; note solder joints/components + near the front overhang where a possible support would touch. +- Board edge dimensions and access around rear I/O, memory latches and power/storage connectors. + +**Measurements that can follow, before cassette geometry:** cooler model and total installed height; +PSU model, actual envelope and mounting points; fan model and intended fasteners; intended drives and +expansion cards; loaded node mass; cable connector bodies and the space needed to plug/unplug them; +available deployment width/depth/height. Missing values stay open instead of becoming defaults. The current fleet inventory identifies Dynatron W1 coolers on srv3 +and srv4; that does not identify the cooler on the board being checked, and its catalog row supplies +no measured installed envelope. + +**Recovered operator setup reports:** Creality PLA, white on printer-01 and black on printer-02, +reported September 4–5; on October 3 the operator says both setups are identical. These reports are +retained in `gunbc.fleet.printer_setup_reports`, with source timestamps and a read-only `main` +projection. The historical AMS report did not name a printer. The operator explicitly confirmed AMS physical +slot 1 on both machines on October 3; that now governs the print command. +Nozzle/plate settings still need reconciliation against retained execution evidence. Recovered files disagree: `print-n/work/gauge_petg.3mf` specifies PETG with +textured PEI; `printn-slice/gauge_container.3mf` specifies PLA with a cool plate. Preset metadata does not supersede the operator’s filament reports. Do not reuse their machine G-code blindly. Final slicing must also check +placement including brim/skirt: the longest narrow gauge nearly spans the 180 mm bed. Keep model +scale at 100%; resolve any placement conflict through explicit orientation/adhesion settings, not +by shrinking a measuring instrument. The existing fan-mount experiment +is optional after the actual fan and fasteners are identified; it does not gate motherboard FIT. + +### Preparation results — 2026-10-03 + +Generated **C–B, C–H, H–L, L–M and H–R** as individual STL and STEP files under +`/home/briansrls/print-prep-2026-10-03`. Generation ran through `fit_gauges.batch` with the +compiler built from this worktree; the generated Python contains the modeled geometry and export +settings. `geometry-readback.json` records hashes and executed results for all five parts: +valid single solids, expected dimensions and cylindrical hole locations/radii, STEP round-trip +agreement, closed manifold meshes, mesh bounds and volume agreement. These checks establish file +conformance for these gauges only, not physical fit or printer/material qualification. + +The three `test.claim.fit_gauges_witness_test` checks executed and passed: every candidate fits +the actual modeled printer envelope; none requests J/S; an unknown pair cannot emit a substitute. +`witnesses.log` retains the run. No physical observation has been inferred from those software tests; the 1–3 fit is separately +recorded from the operator above. + +**Ready for final slicing, not yet machine-ready G-code:** use C–H as the next candidate after the +existing F–C result. Use the recovered PLA reports; reconcile nozzle, plate and adhesion settings before +slicing. No upload or printer start was performed. Authenticated delivery/start was not revalidated; +the old `printer_credential_migration_run` includes destructive secret migration and is not an +appropriate routine readiness probe. A subsequent print must use an admitted credential path rather +than rerun that migration merely because it previously started a job. + +### Requested two-printer batch — 2026-10-03 + +The operator requests concurrent printing on both printers. Prepare **printer-01: holes 3–4 +(C–H)** and **printer-02: holes 4–5 (H–L)**. This checks two additional board spans; because the +parts differ, it is not a printer-to-printer calibration comparison. Source-identical STL copies and +hashes are staged under `/home/briansrls/print-prep-2026-10-03/two-printer-batch/`, one directory per +printer. Material/colour reports are now modeled: Creality PLA, white on printer-01 and black on +printer-02. Physical spool identity and current bed-clear state are not established by those reports. +The selected slicing profile is the A1 mini 0.4 mm nozzle, Generic PLA and Textured PEI plate; +this is a recorded process choice, not a claim of current printer telemetry. +The routine workload-identity entry point is implemented in the migration below; its deployment +remains pending. The operator-token batch route and live progress are recorded separately below. +No printer start is implied by staging the files. Label the finished gauges 3–4 and 4–5 before removing them. + +### Printer authentication migration to the ntfy workflow + +The operator requested migration on 2026-10-03. `gunbc.fleet.printer_federation_provision` +adds a dedicated `printer-lan` reader to the existing ntfy-approved GCP IAM estate. It requests +access only to the two rostered printer secrets; the print worker cannot create, rotate or delete +credentials or change IAM policy. The provider pins the repository, main workflow/ref, dispatch +and `printer-lan` environment through the shared OIDC claim authority. + +`fleet-converge` mode `printer` has two operations: + +1. `observe` resolves a printer's existing credential once and writes its exact version identity + to a receipt. The receipt contains no access code. It performs no printer action. +2. `print` requires the selected printer, staged project path on srv1, expected SHA-256, numeric + credential version and an explicit clear-bed report. It hashes a private copy before upload, + uses the same version for FTPS and MQTT, and refuses a workflow rerun. A successful receipt + means **start published**, not a claim that the machine reported RUNNING. Unknown outcomes + require observation, never automatic replay. + +The dedicated job runs on srv1, checks out the dispatch event SHA even if `expected_revision` is +supplied, and serializes starts per printer without cancelling an in-flight operation. It uses +`WorkloadIdentityToken`; no operator-token fallback and no destructive credential migration. +`gunbc.fleet.printer_host_tools.converge` is administrator preparation for the MQTT tool, using +the existing apt ensure and the tool's declared package. The worker checks the actual client +version before any upload. Mosquitto identifies its version through the documented `--help` +interface ([upstream manual](https://mosquitto.org/man/mosquitto_pub-1.html)). + +**Deployment standing:** source changes are not evidence of installed IAM grants or a dispatched +print. The main-pinned workflow must land before it can federate. The existing IAM bootstrap must +cover the new target resources so its observer can plan and its approved executor can apply those +resource-local bindings. Then run the shared `gcp_iam_converge` workflow and approve its concrete +diff through ntfy, prepare the srv1 MQTT tool, and run credential observation. Final sliced projects +and live printer-state verification remain required before dispatching these gauges. The operator +reported both beds clear during this session; do not carry that report to an unrelated later print. + +Validation: all seven printer workflow binding, admission, tool-version, OIDC, IAM approval and +start-replay regression tests passed. The canonical artifact gate regenerated the workflow; YAML +structure assertions and shell syntax checks passed. These are source checks, not live deployment +or printer observations. + +### Operator-token batch execution — 2026-10-03 + +The operator supplied a one-hour GCP token and asked to print before the migration lands. For this +batch, `printer_operator_approval.run` uses the existing `OperatorSuppliedToken` capability and the +shared signed ntfy approval gate. It does not relabel that token as WIF, invent an Actions run, or +change IAM/secret versions. The permanent main-pinned GitHub route remains separate. + +The approval binds printer, project SHA-256, exact credential version and batch attempt. After the +live approval it subscribes to a fresh report and requires IDLE/FINISH with zero print error, then +creates a persistent per-printer start marker before upload/start. A refused or ambiguous start is +never automatically replayed. `printer_report` reads telemetry through the same fleet identity, +exact credential and pinned CA; its report is current-state evidence, not causal acknowledgement. + +Printer-01 credential version 1 was read successfully through the modeled client. Holes 3–4 +were sliced successfully by Orca 2.4.2, with ZIP and embedded G-code checksum checks, one plate, +identity scaling, 15 layers/3 mm height, and bounds including the brim within 180 mm. Selected +settings: PLA, 220 °C nozzle, 65 °C textured bed, 0.20 mm layers, 2 mm outer brim. Estimated +19m22s and 6.99 g. Project SHA-256: +`28a8700489cf908edb9ed59c0aa871fe6903b4d558c3b64de6a7636af701580e`. + +The isolated srv1 execution directory is `/home/briansrls/print-run-2026-10-03`; credentials live +only in owner-only private staging files and are not repository artifacts. Holes 4–5 also sliced +and passed the same checks: 15 layers, 13m48s, 3.67 g, SHA-256 +`a8f434e80014b6a316d30665b372bd0df3e858a68a2ff04a5fe4f50031d68535`. + +The shared approval client now follows the declared writer cutover to the approval broker on +8085; the old roadmap endpoint refused writes as designed. Both requests were filed and reached +approval polling. Four operator-route witnesses passed, including all three writer postures. +Fresh read-only reports showed printer-01 IDLE/error 0 and printer-02 IDLE/error 83902511. +Both reported PLA in AMS tray 0 (physical slot 1), while the existing start command selects the +external spool. The initial executions were stopped before upload to resolve that mismatch. +No start marker, physical start, or RUNNING observation was claimed at that point. + +### Local preparation environment + +The isolated worktree is `/home/briansrls/.worktrees/gunbc/3d-printing-2026-10-03`. +Local preparation artifacts are in `/home/briansrls/print-prep-2026-10-03`: + +- `venv/`: CadQuery 2.8.0; resolved dependency versions in `requirements.lock`. +- `target/debug/gunbc`: compiler built from this worktree. Invocation requires an enforced cgroup + memory budget; the preparation run uses a systemd scope with `MemoryMax=24G` and no swap. +- `mounting-check-map.svg`: schematic from the checked-out standard-location rows; all physical + correspondences remain subject to the bench check. +- `verify-generated-gauges.py`: reads the emitted literal geometry instructions, runs them in + CadQuery, checks the resulting solid and STEP round-trip dimensions/hole axes/volume, and checks + STL bounds, closed manifold edges and volume. Its result only concerns generated-file conformance. +- `slicer-probe/DO_NOT_PRINT_old_settings.3mf`: tool-recovery probe using the old project settings; + deliberately excluded from the next-print candidates. + +The preserved OrcaSlicer 2.4.2 AppImage at `/tmp/orca.AppImage` matches the digest recorded in +`extdeps.printing.orca_slicer`. Its extracted CLI at `/tmp/orcax/squashfs-root/AppRun` now launches +after installing its missing OpenGL/WebKitGTK runtime dependencies. `--help` and model inspection +work. The modeled probe now uses the supported `--help` interface. `with_verified_orca` copies +and hashes the pinned AppImage in a private realization, runs probe and slice inside its callback, +and removes the realization before returning. `slice_run.run` uses it to produce the gauge project; +its success establishes slicing, not an observed physical print. + +### Gauge observations to collect today + +Record each board separately: stable label, model/revision, selected holes and gauge orientation. +The gauge checks **LocF–LocC**, modeled as offsets of **157.48 mm along the rear edge and +22.86 mm in depth**. These are model values, not measured dimensions of the printed part. +Establish the physical hole correspondence before interpreting a result; say whether checking board +holes directly or installed standoffs, since the latter also tests standoff placement. + +Report whether both locations align simultaneously with the gauge resting naturally; if not, +which end aligns and the direction of the miss. Note obstructions or warp. Do not count alignment +obtained by bending the gauge or drawing it into place with screws as a pass. Record measured +values only when actually measured, with the instrument used. A photo can supplement the result. +An F–C pass supports that pair on that board, not the full mounting map or underside clearance. + +### Next steps and dependencies + +| Order | Work | Dependency / completion evidence | +|---|---|---| +| 1 | Record each board's F–C result | Operator observations; retain disagreements and distinguish board-hole alignment from standoff fit | +| 2 | Resolve misses; extend successful checks to remaining mounting locations | For a miss, distinguish orientation, printed-gauge dimensions and board/standoff placement before changing geometry. A pass permits additional pair gauges or an adjustable fixture, not freezing the entire map | +| 3 | Audit CAD, slicing and print evidence | Can proceed during board checks: locate generated artifacts, profile/toolchain identity, printer/material/nozzle binding and outstanding solid-readback controls; close PRINT-7/8 only against their full criteria | +| 4 | Complete process observations | Attribute the existing coupon only from retained evidence; otherwise print labeled replacements. Observe both printer/spool combinations independently and repeat attempts before claiming repeatability | +| 5 | Build an unpowered adjustable fit fixture | Confirm remaining mounting positions, standoff thread/height, underside keep-outs and connector access. PLA can serve this experiment without qualifying structural or powered use | +| 6 | Measure cassette inputs and choose joints/material | Can start now: actual cooler and PSU envelopes, node mass, drive requirement, fasteners, connector access and cable service space. Joint family and structural material precede their qualification prints | +| 7 | Prototype one removable cassette and one fixed bay | Requires fit and structural-process evidence; then PSU/cables, airflow and powered thermal validation under standing requirements, followed by 2x2 service demonstration | + +The immediate milestone is a verified mounting map and an unpowered fit fixture. Full cassette +geometry depends on measured hardware envelopes and structural-process qualification. + Governing statement: @@ -30,9 +296,10 @@ whether more printers are worth buying. | Build volume | 180 x 180 x 180 mm | A1 mini spec table, operator relay | | Fan form factor | 80 mm; **count derived**, not assumed | operator + derivation | | Rack unit | 2x2 block, composable | side-chat ruling | -| Service law | fixed frame, removable cassette | operator requirement R9 | -| PSU placement | cassette-resident | side-chat ruling | -| External bundle | 2 x Ethernet + 1 x AC per node | operator | +| Operator effort | Research component masses online; use explicit model/BOM ranges where needed. Do not request weighing or hardware disassembly for mass inputs. | operator direction, 4 October 2026; [mass assumptions](milk-crate-stack-design.md#mass-is-a-fold-over-components) | +| Service law | Integrated corner-column stack; proposed temporary supports permit target-only removal, otherwise LIFO (4 October 2026, superseding R9) | operator ruling, [milk-crate stack design](milk-crate-stack-design.md) | +| PSU placement | independent power cassettes assigned to node groups by usable wattage, port count and interface compatibility; mains stays in power cassettes (4 October 2026; supersedes one-per-column allocation) | operator ruling, [milk-crate stack design](milk-crate-stack-design.md) | +| External bundle | 2 x Ethernet + 1 x protected, isolatable DC feed per node; AC at shared supply | operator acceptance, 4 October 2026 | ## Standing laws @@ -58,13 +325,13 @@ because a step without one cannot be said to be done. | **PRINT-3** | CAL | Coupon authority: ladder as modeled experimental design, rungs and plate derived | **done** | | **PRINT-4** | TOOLCHAIN | Realization contract v0: contract derives its own rungs; a wrong-step handler is caught and located | **done** | | **PRINT-5** | TOOLCHAIN | Raw transport schema admission: unknown version, missing field and unknown operation all refuse before any geometry call | **done** | -| **PRINT-6** | TOOLCHAIN | CadQuery handler + authority wall; sealed instruction plan; walls 1-3 execute, wall 4 (solid readback) is a declared boundary | handler done; materializer + export profile open | -| **PRINT-7** | TOOLCHAIN | STEP/3MF emitted and re-inspected; output conformance re-establishes envelope, holes, walls | | -| **PRINT-8** | TOOLCHAIN | Slicer profile bound or refused; per-printer manifests; ProcessQualificationIdentity minted, not branded | | -| **PRINT-9** | CAL | Coupons printed on BOTH printers; each printer+spool admitted or refused **independently** | needs printers | -| **PRINT-10** | FIT | Adjustable-standoff fixture; real board mounted unpowered; hole map measured back and frozen | needs printers + board | +| **PRINT-6** | TOOLCHAIN | CadQuery handler + authority wall; sealed instruction plan; walls 1-3 execute, wall 4 (solid readback) is a declared boundary | handler/export profile landed; full readback closure unverified | +| **PRINT-7** | TOOLCHAIN | STEP/3MF emitted and re-inspected; output conformance re-establishes envelope, holes, walls | kernel blocker resolved; full acceptance to verify | +| **PRINT-8** | TOOLCHAIN | Slicer profile bound or refused; per-printer manifests; ProcessQualificationIdentity minted, not branded | live printing exercised; qualification closure to verify | +| **PRINT-9** | CAL | Coupons printed on BOTH printers; each printer+spool admitted or refused **independently** | coupon feedback recorded; independent qualification and repeatability pending | +| **PRINT-10** | FIT | Adjustable-standoff fixture; real board mounted unpowered; hole map measured back and frozen | gauge in hand; 1–3 (F–C) alignment pass reported; full fixture/map open | | **PRINT-11** | CASSETTE | Structural cassette: rails, tray, handle, latch; carries node mass; no seam in layer-separation tension | needs PETG decision | -| **PRINT-12** | CASSETTE | PSU carrier and harnesses; every connector insertable; nothing side-loaded; **PE continuous with board, standoffs and cassette all removed** | needs PSU envelope | +| **PRINT-12** | CASSETTE | Shared fixed-frame PSU/distribution and node harnesses; every connector insertable; nothing side-loaded; **PE continuous with board, standoffs and cassette all removed** | needs PSU envelope | | **PRINT-13** | CASSETTE | 80 mm fan carrier + replaceable duct; powered thermal admitted against a bench baseline | needs cooler choice | | **PRINT-14** | RACK | One fixed bay; cassette retained in service, removable after disconnect; empty bay structurally complete | | | **PRINT-15** | RACK | Management-node mount (Raspberry-Pi class) on the rack, carried as a bay peer rather than an accessory | | @@ -92,23 +359,18 @@ nominal. The check was run because the module's own annotation asserted "changin moves that witness", and an annotation asserting a property the code does not have is exactly what #10119 had to repair four times. -PRINT-5 and PRINT-6 landed with #10119, so the pre-arrival critical path is now **PRINT-7 alone**, -and it is BLOCKED rather than merely pending: re-inspecting an emitted STEP/3MF needs a CAD kernel, -and the trigger for that is a base image with glibc >= 2.38 or an x86_64 runner. PRINT-8 is the first -step that needs the hardware itself, and it is not blocked by PRINT-7 — the coupon can be printed and -measured before any kernel readback exists, it just cannot issue an absolute process-qualification -receipt until PRINT-7 closes. +PRINT-5 and PRINT-6 landed with #10119. PRINT-N (#10680) subsequently resolved CAD bootstrap, +added measured export-profile work, and exercised physical printing. PRINT-7/8 still require an +evidence audit against their full criteria; job-start evidence alone does not close them. ### What blocks what -- **PRINT-5..6 are landed.** PRINT-7 is BLOCKED on a CAD kernel — measured, not assumed: CadQuery - installs on this arm64 container and clears `libGL.so.1`, then `casadi` requires `GLIBCXX_3.4.32` - which requires `GLIBC_2.38`, and this box is glibc 2.36. The trigger is a base image or an x86_64 - runner, NOT a pip install. +- **CAD bootstrap is resolved.** Full output-conformance and process-qualification evidence + remain to be reconciled. - **Joint family** (dovetail / tongue-and-groove / keyed slide / pin) gates the ProductionInterfaceCoupon only — a later half of PRINT-8, not the MachineProcessCoupon. -- **PETG** gates PRINT-10 onward. PLA carries PRINT-8 and PRINT-9 and stops there: no PLA-to-PETG - receipt carry. +- **Structural material qualification** gates durable/powered cassette work. PLA can carry CAL + and unpowered FIT; no PLA-to-PETG receipt carry. - **Deployment envelope** gates PRINT-13..14 layout admission, nothing earlier. - **Drive count** gates PRINT-11 completion only. @@ -117,9 +379,9 @@ receipt until PRINT-7 closes. | Project | Scope | Terminal evidence | State | |---|---|---|---| | **CONTRACT** | Goals, non-goals, authority graph, refusal law, MVP boundary | No assumption represented as a default | deferred — no consumer yet | -| **MEASURE** | Board, cooler, PSU, fan, DIMM, cable, **deployment envelope**, uncertainty | Pack sufficient to generate CAL+FIT | **partial — carrier landed, roster empty** | -| **TOOLCHAIN** | Realization contract, CadQuery handler, authority wall, STEP/3MF, slicer env, printer nodes | Deterministic generation + executed no-parallel-authority controls | not started | -| **CAL+FIT** | Coupons, qualified tolerances, adjustable standoff fixture, real-board fit, hole-map feedback | One unpowered board fits; hole authority admitted | not started | +| **MEASURE** | Board, cooler, PSU, fan, DIMM, cable, **deployment envelope**, uncertainty | Pack sufficient to generate CAL+FIT | **partial — coupon readings recorded; board-fit observations pending** | +| **TOOLCHAIN** | Realization contract, CadQuery handler, authority wall, STEP/3MF, slicer env, printer nodes | Deterministic generation + executed no-parallel-authority controls | partial — CAD/export and live printing exercised; full conformance open | +| **CAL+FIT** | Coupons, qualified tolerances, adjustable standoff fixture, real-board fit, hole-map feedback | One unpowered board fits; hole authority admitted | in progress — coupon feedback and gauge in hand | | **CASSETTE** | Structure -> PSU/cables -> airflow -> powered thermal | Removable node operates and is serviceable | not started | | **RACK+VERDICT** | One bay -> 2x2 block -> scale/economics | Middle-node service proven; buy-more-printers verdict | not started | @@ -356,7 +618,7 @@ unwalled**: a `CommitRequired` artifact whose path matches a developer ignore pa uncommittable. Its trigger is the first such artifact; walling it before one exists would be a check whose RED is unauthorable. -**Export is removed from the emitted program rather than profiled.** The +**Historical pre-PRINT-N state; the measured export profile subsequently landed.** Export was removed from the emitted program rather than profiled. The `cq.exporters.export(result, "…stl")` line chose five things the model never stated — STL over STEP or 3MF, a filename, a working-directory-relative destination, format-by-extension, and CadQuery's default tessellation policy. The last is not neutral: STL is a mesh format, so every modeled circle @@ -382,7 +644,7 @@ on the grounds that it preserves a typed place for the next obligation, but noth gate required emptiness — so the forcing did not exist, and the discharged property is carried where it executes, in the type of the accepting arm. -**Toolchain boundary, now measured rather than assumed.** Wall 4 — re-reading the produced *solid* +**Historical bootstrap probe, superseded by PRINT-N conda-forge bootstrap.** Wall 4 — re-reading the produced *solid* for plate dimensions, datum location and the eleven ladder holes — is a §4b boundary obligation, not a rung. Its trigger was written as "a runner with cadquery importable", which was too vague to act on, so it was probed to termination: @@ -396,7 +658,7 @@ on, so it was probed to termination: - Past that, `casadi` requires **`GLIBCXX_3.4.32`**, and a libstdc++ carrying it requires **`GLIBC_2.38`**. This container is **glibc 2.36**. -So the real trigger is **a base image with glibc ≥ 2.38 (or an x86_64 runner), not a pip install** — +The probe then incorrectly concluded the trigger was **a base image with glibc ≥ 2.38 (or an x86_64 runner), not a pip install** — and assembling one by hand-extracting `.deb`s onto `LD_LIBRARY_PATH` is where a probe turns into the workaround §5 names, so it was stopped there rather than pushed through. The environment wall 4 needs is a *pinned, identified* toolchain — Python version, CadQuery version, OCP closure, image digest, @@ -492,20 +754,18 @@ So the PETG decision blocks the **durable/powered CASSETTE path**, not the first unpowered FIT fixture. Qualify each printer-spool combination independently; never infer that an observed difference is the printer alone. -## Open decisions (operator) — none block the first print +## Open decisions (operator) — structural and rack work -- **Drive count** — a CASSETTE completion obligation. Blocks nothing before Thursday. +- **Drive count** — a CASSETTE completion obligation. Does not block gauge checks or the unpowered fixture. - **PETG** — blocks structural/powered parts, not CAL or unpowered FIT. - **Deployment envelope** — **not on the pre-arrival critical path.** Its absence must refuse rack-layout admission later, not block calibration now. ## Next -1. ~~`CalibrationCouponAuthority`~~ — landed as `product.printed_chassis.coupon`. -2. `RealizationContract v0` — the transport-only, schema-bound feature graph the handler consumes. -3. CadQuery handler + four-part wall executed over v0, with all four negative controls. -4. STEP/3MF generation and conformance; bound slicer profile or refusal. -5. Per-printer manifests for node 1 and node 2; measurement/admission form ready for results. +Follow the dated next-step table above. Board-specific gauge observations and remaining mounting +checks lead to the unpowered fixture; toolchain-evidence reconciliation and cassette measurements +can proceed independently. The former list predated the landed PRINT-N implementation. **Deferred as definition-only residue until each has a consumer:** `CONTRACT` (needs the generator), `DeploymentEnvelope` (needs the layout-comparison consumer). The design of both is pinned above. @@ -801,3 +1061,303 @@ node pitch (which decides upright versus flat-on-riser, and the riser makes it a aux power connector type and sustained draw against the node supply and stack distribution · whether a mixed stack is allowed at all, since a uniform GPU stack and a mixed stack have different sidewall answers. + +### Batch follow-up: AMS and printer-02 storage + +On 2026-10-03 the operator confirmed AMS physical slot 1 on both printers and identified +printer-02 error 83902511 (0x0500402f) as a microSD error. The operator was unsure whether +that card was in use. This LAN route uploads to SD storage and starts a `file:///sdcard/` +project, so the error remains a start blocker; it is not suppressed. No formatting or deletion +of printer storage is authorized or performed. The start payload now selects AMS and maps +the single project filament to tray 0; the signed operator request includes this feed choice. + +The operator subsequently reported formatting printer-02’s card locally. A fresh no-error +report is still required before its start. Remote formatting was requested as a future capability; +the current modeled workflow has no format operation, and support on these printers is unverified. + +At 22:01 UTC printer-02's AMS request was approved. Its fresh report admitted IDLE/error 0, +but FTPS upload returned curl exit 25 / server 550 before MQTT start. Its durable batch marker +is retained; there is no automatic replay. Printer-01's approved attempt received a partial +temperature-only report and correctly refused before upload. The observer now allows up to +eight fresh messages to obtain state and error together, without inventing missing fields. + +The operator confirmed that printer-02 recovered and appeared functional after formatting. +A separate after-format attempt retains the original failed upload marker and requests new +approval. No MQTT start had been sent in the failed attempt. Status observation now subscribes +before publishing a read-only `pushing.pushall` snapshot request with Mosquitto request/response; +it still requires an explicit complete state/error report and cleans the private credential files. + +Correction at 22:12 UTC: the operator reports printer-02 still shows SD card “error” in +settings despite clearing print_error. Stop its after-format attempt before actuation and +prioritize printer-01. A zero print_error is not proof of usable storage. + +### Printer-01 start observed by operator + +At approximately 22:15 UTC the full-snapshot gate admitted printer-01 and its holes 3–4 +project uploaded successfully. MQTT publication did not return PUBACK. The operator then +confirmed the machine was heating or calibrating. The publisher had already exited with +connection-lost code 7 and cleaned its credential/CA directories before termination was attempted. +The transport outcome is UNKNOWN; the batch marker is retained and no start replay is permitted. +This is operator-observed preparation, not yet a telemetry-confirmed extrusion or completed print. +The selected feed is AMS physical slot 1; slice estimate 19m22s. Printer-02 remains held for +SD-card settings error, despite its zero print_error report. + +At 22:19 UTC a fresh authenticated snapshot confirmed printer-01 `RUNNING`, `print_error=0`, +and subtask name `gunbc-28a8700489cf908edb9ed59c0aa871fe6903b4d558c3b64de6a7636af701580e.3mf`. +It reported 4%, 18 minutes remaining, layer 0/15 (preparation), nozzle 176 °C and bed 62 °C. +This reconciles the unknown MQTT transport result with observed execution of the intended gauge; +it does not establish completed extrusion or a finished part. No duplicate start was sent. + +### Printer-02 recovery retry + +The operator subsequently reported printer-02's card working and requested another attempt, +supplying a fresh one-hour token. The previously stopped after-format attempt had no start claim +and had sent no printer command. At approximately 22:31 UTC its new signed approval was observed, +the fresh full-state check admitted the printer, and the holes 4–5 file uploaded successfully. +AMS physical slot 1 remains selected. MQTT publication is awaiting reconciliation; it is not +replayed. The earlier FTP-550 attempt and its claim remain separate and retained. + +At approximately 22:36 UTC, fresh telemetry confirmed printer-02 RUNNING/error 0 with the +expected `gunbc-a8f434e80014b6a316d30665b372bd0df3e858a68a2ff04a5fe4f50031d68535.3mf` +subtask, 38%, layer 0/15, and 8 minutes remaining. The upload/start route now has physical +execution evidence after the card recovery; this is not a long-term card-health qualification. + +The operator reported printer-01 finished the holes 3–4 gauge, both holes line up well, +and the part was removed / bed cleared. Record C–H as an operator-reported alignment pass, +without inferring load-bearing qualification. Prepare holes 3–6 (C–B) as printer-01's next gauge. + +The operator authorized the next printer-01 print after confirming C–H fit and a clear bed. +Holes 3–6 (C–B) sliced through the verified Orca entry with the same selected profiles. +Readback: SHA-256 `fb6d4bd1fcb23c07087b5575e6f5b20619f7ab8da03217828148d84cfcefc09f`, +15 layers / 3 mm, identity scaling, one plate, valid embedded G-code MD5 and ZIP integrity, +brim bounds [57.211684, 80.071684, 122.788316, 99.928316] mm, 12m10s / 2.59 g PLA. +The slicer metadata carries `bed_temperature_too_high_than_filament` at the upstream Generic PLA +textured-plate setting of 65 °C; this setting matches the successfully printed/aligned prior gauge. +It is retained as an observed warning, not described as warning-free. The new request binds this +project digest and a separate holes-3-6 attempt; printer-02's running job is not touched. + +The other remaining candidates are also sliced and validated under +`/home/briansrls/print-prep-2026-10-03/next-batch/` (no start requested for either yet): + +| Gauge | SHA-256 | Estimate | PLA | +|---|---|---|---| +| 5–2 (L–M) | `607601f18602ae273472654c01b9d34ca1674b7f6f5356c7136a382af41f545b` | 19m34s | 7.10 g | +| 4–7 (H–R) | `972ec6278c3334e070d4bd31884ce781211454c1c32296652e5f9a6ae4269252` | 13m26s | 3.42 g | + +Both passed ZIP/G-code checksum, one-plate, 15-layer/3-mm, identity-transform, A1 mini/PLA/0.4-mm +profile, and 180-mm bed-bound checks. L–M including brim spans X=1.331684–178.668316 mm; no +scaling was applied. Both retain the same 65 °C bed-temperature warning described above. + +### Operator direction: stop the pair-gauge sequence + +The operator requested stopping further gauge printing and moving toward chassis/rack parts. +Do not enqueue the prepared 5–2 or 4–7 gauges. A fresh snapshot already confirmed the newly +started printer-01 3–6 job RUNNING/error 0 with its exact digest filename and 12 minutes remaining; +the operator explicitly chose to let that active print finish. No cancellation is requested. +The operator expects the remaining positions to be accurate; retain that as an expectation, not +a measured fit result. 1–3 and 3–4 have reported alignment passes. + +Repository inspection found no printable full chassis/cassette/rack-bay geometry. Existing concrete +geometry covers coupons, standoff gauges and a fan-mount plate; rack_mount models occupancy of +catalog hardware, not printable rack construction. The proposed next geometry is a segmented, +adjustable motherboard carrier for unpowered assembly/fit, forming the first chassis prototype. +Remaining mounting positions can stay adjustable rather than requiring more pair gauges. Actual +standoff thread/height and underside clearance still determine the board-support interface; +PSU/cooler envelopes, structural material and load/joint checks remain for the powered cassette. + +### Hosted concept workbench + +The operator accepted the requirements and requested a hosted site for iterating on the chassis, +including independent 3D rotation of each piece. Published privately: +[Altra Chassis Workbench](https://altra-chassis-workbench.briansrls448156.chatgpt.site). +It contains an explicitly provisional four-panel carrier, segmented rails, handle and retaining +tabs, with a 2×2 fixed-frame study. Individual isolation/orbit, explosion, withdrawal and design +controls are available. There are no print controls or manufacturing exports. +The missing hardware dimensions remain unknown; the geometry is a visual proposal, not an +admitted fabrication result. Source project details are recorded in `sites/README.md`. +Desktop/mobile rendering and the principal interactions were exercised successfully. +All temporary copies of the operator's GCP token were removed after printing work ended. + +### Cassette hardware and airflow study — 4 October 2026 + +Operator clarification: the target cooler is Dynatron (W1 in the preceding question), and PSUs +must be interchangeable rather than tied to one model. Roughly 400–600 W was an example range, +not a finalized electrical qualification. PSU compatibility must include accepted physical envelope, +mounting adapters, connector requirements, intake/exhaust clearance and service access; wattage +alone is insufficient. The inventory's unread PSU identities remain unread. + +Concept 02 adds a rear cassette-resident adjustable metal PSU cradle, a variable PSU envelope, +Dynatron W1 geometry, DIMM clearance zones, one to three front 80 mm fans with individual carrier +feet, side airflow guides, a schematic internal harness corridor and front-to-rear direction arrows. +The operator proposed cassette-mounted fans. All these components travel with the cassette in the +2×2 frame view. Three fans is a layout default, not an established cooling requirement. + +The [Dynatron manufacturer page](https://www.dynatron.co/product-page/w1), checked 4 October, +publishes active dimensions 141.1 × 84 × 72 mm and passive dimensions 116 × 84 × 72 mm. +The study depicts the active envelope with simplified fins/fan; active versus passive operation, +actual socket position/orientation and installed offset are not established by that depiction. +The P8 PWM PST uses its catalog 80 × 80 × 25 mm envelope; the existing 71.5 mm hole pitch and +4.4 mm clearance remain convention-unverified. Fan count, ducting and header/PST-chain electrical +admission still require evidence. Airflow arrows are not simulation results. + +PSU slider defaults (150 × 85 × 150 mm), DIMM zones, the 50 mm rear service gap, adjustable cradle +interfaces and harness route are design proposals. They are not measured hardware or a claim that +all 400–600 W PSUs fit. No drive or expansion-card configuration has been invented. A height check +flags simple study-pitch conflicts; it does not prove full assembly clearance or powered suitability. + +Source commit: `8c944389c0dbcb25639d4c6069587863ae0d0fd5` in the separate Sites repository. +Browser checks passed for selectable parts, PSU isolation, fan-count changes, height-conflict +feedback, airflow visibility, orbit, rack withdrawal, requirements and mobile overflow. +No additional gauge or chassis print was started. + +### Shared power accepted — supersedes cassette-resident PSU + +On 4 October 2026 the operator accepted shared power per four-node block. This replaces the earlier +cassette-resident PSU requirement and its per-node AC bundle. Eight nodes comprise two independently +powered four-node blocks. Each block has a fixed-frame 12 V supply system and four separately +protected, isolatable and disconnectable node feeds. Cassettes retain their board, cooler and fans. +The earlier roughly 400–600 W example concerned individual supplies and is not a shared-block rating. + +[ASRock manual §2.8](https://download.asrock.com/Manual/ALTRAD8UD-1L2T.pdf) documents direct +12 V +DC input and distinguishes it from the ATX signal-adapter connection. This establishes a supported +input mode, not a qualified multi-node distribution system. Required power connections, independent +startup/shutdown behaviour, aggregate load/startup demand, branch fault protection, wire/connector +ratings, voltage drop, protective-earth continuity and service isolation still require engineering. +No connector pinout, fuse size, PSU wattage, redundancy or live hot-swap capability is selected here. +A nonredundant shared supply is a four-node failure domain; redundancy remains an open decision. + +Concept 03 depicts one fixed top power shelf, one adjustable shared PSU space reservation, one +four-branch distribution placeholder and stationary branch harnesses. A DC disconnect placeholder +travels with each cassette. Withdrawal illustrates the node shut down and its branch isolated and +disconnected first. Top-shelf placement and all distribution dimensions are visual proposals. Shared +supply height is additional to the two node rows; cassette pitch is governed by cassette hardware. +Older Concept 02 notes above are retained as history and are superseded for power topology. + +Concept 03 source: `7f77b6884f23af034fd1632e5918c3a21e988e5a` in the Sites repository. +Browser checks passed: shared PSU isolation, no PSU on the cassette-only view, fan-count controls, +height feedback, airflow visibility, rack withdrawal, requirements and mobile layout. + +### Rear exhaust and first fan-mount print — 4 October 2026 + +The operator requested moving fans to the rear to keep hands at the front handle away from them, +and authorized modeling/printing a first part. Rear cassette-mounted exhaust is the new layout; +front-to-rear airflow remains. Rear I/O/cable access and a finger guard remain unresolved. Both +beds were reported clear; printer-01 is selected with the previously confirmed PLA in AMS slot 1. + +The first real part reuses `fan_mount.arctic_p8_fan_mount`: 92 × 92 × 4 mm plate, 76 mm aperture, +71.5 mm hole pitch and 4.4 mm mounting holes. This is a useful fan mounting plate for unpowered +assembly fit, not another motherboard spacing gauge and not a complete cassette. Cassette +attachment and guarding have not been fabricated. Hole-pattern evidence remains convention-unverified. +`fan_mount_print.main` carries the existing admitted geometry through the existing CadQuery emitter +and exercised STL export settings; there are no independently authored CAD coordinates. + +Generation and verification artifacts are in `/home/briansrls/print-prep-2026-10-04/rear-fan-mount`. +CadQuery 2.8.0 checks passed for one valid solid, dimensions, all five hole axes/radii, analytic volume, +STEP round-trip and a closed manifold STL. STL SHA-256: +`2b0c7df7e8cf79dc772a33ee6f62a6ccf9e5568ef07e0309545313480e35b4f8`. +Orca 2.4.2 sliced through `slice_run.run` at 0.20 mm, 20 layers, PLA/0.4 mm nozzle, 220 °C nozzle, +65 °C textured bed and 2 mm outer brim. Estimate 28m41s, 11.40 g. ZIP/G-code checksum, one plate, +identity scaling and 180 mm bed bounds passed; bounds including brim are 42.071684–137.928316 mm +on both axes. The retained bed-temperature warning matches the previously exercised 65 °C setting. +The slice_info first-layer-time metadata is malformed; G-code estimates 6m4s and that metadata is +not an admission input. Project SHA-256: +`71da943ff2ed73a6b47c54759ba288e82af7d084c269bf9a413d4d901cf5199c`. + +Concept 04 is published with rear fans and downloadable generated STL/STEP, source +`c2a49273da70f20a23e2e044fe552dceb74b773f`. Browser interaction checks passed. +The operator-token/ntfy route is being used because PR #13149 remains unmerged. The request binds +printer-01, credential version 1, exact project digest and attempt `rear-fan-mount-20261004-01`. +A start result must be observed separately; preparation is not evidence of printing. + +The first launch refused before submission because its submission-key environment variable was +missing; it filed no approval and created no start marker. Corrected operator launches on srv1 +require these existing key paths (never their contents) plus the private token-file path: + +- `GUNBC_APPROVAL_SUBMISSION_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-submission-mac-key` +- `GUNBC_APPROVAL_CAPABILITY_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-mac-key` +- `GUNBC_APPROVAL_STORE_RECEIPT_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-store-receipt-mac-key` +- `RUNNER_TEMP` pointing to an owner-only staging directory. + +The refusal log is retained as `print-01-preflight-refusal.log` under the srv1 run directory. +Retrying the same attempt before any request/start marker exists does not replay a printer start. + +The corrected run passed the signed approval gate, observed the printer ready, claimed its durable +start marker, uploaded the exact project and sent one AMS-slot-1 start. MQTT publication lost its +connection without acknowledgement after two minutes, so the effect was classified UNKNOWN and +was not replayed. A separate modeled `printer_report.run` observation then returned RUNNING, +print_error 0, exact digest filename `gunbc-71da943ff2ed73a6b47c54759ba288e82af7d084c269bf9a413d4d901cf5199c.3mf`, +19% and 23 minutes remaining. This confirms the requested job active; layer_num was 0, so it is +not yet evidence of deposited layers or completed fit. The snapshot is retained locally and on srv1. +The operator independently reported probably heating. Printer-02 was not started. Temporary copies +of the operator token were deleted from both hosts. + +### Complete logical cassette review — 4 October 2026 + +The operator reports that the printed rear fan plate fits. Record this as a physical fit +observation, not a caliper measurement or a promotion of the upstream conventional hole pattern. +The operator requests the whole cassette model now, review of all pieces together, then the first +cassette print. Stop the individual gauge sequence. No printer start is authorized by this review +artifact generation alone; the next print follows assembly review and the existing print workflow. + +Review 05 replaces browser-authored mechanical geometry with `product.printed_chassis.cassette` +and `rack_assembly`. `assembly_geometry` carries typed box/cylinder CSG, micrometre coordinates, +part kind, fixed/moving scope, print orientation and mating-interface records. `cassette_review` +emits the full manifest. `review_realization` emits the generic CadQuery host adapter; it chooses +no mechanical dimensions and grants no fabrication admission. The old Site `concept.js` is removed. +Vendor board outline, fan plate/pattern, W1 envelope and P8 depth resolve from their existing owners. + +One cassette has 49 printed instances / 29 distinct authored geometries: four vented tray panels, +four panel splices, four rails, two rail splices, a handle, two retaining stops, two rear extension +arms, two fan crossmember halves and a splice, three fitted-pattern fan plates, three feet, three +guards, two side airflow guides, four guide spacers and twelve guard spacers. The tray is +271.840 × 294.700 mm. Each piece fits the 180 mm A1 mini envelope in its declared orientation. +The two-level block retains a 140 mm bay pitch; this is a custom frame, not a claim of standard +2U/4U rack compliance. The fixed metal frame includes runner proposals. Shared 12 V supply, +four protected/isolatable feeds and distribution space remain fixed when a cassette is extracted. +Supply dimensions are explicitly space reservations; no supply wattage or circuit is selected. + +The guides initially intersected the rail flanges and outer fan feet. The corrected model raises +the guide foot to 13 mm, adds front 8 mm / rear 4 mm spacers, and drills the outer foot for the shared +rear bolt stack. The front rests on the panel; the rear rests on the outer fan foot. This clears +the rail flange by 1 mm. Guard pads enclose the four fan fastener holes; upper 7 mm / lower 2 mm +spacers account for the 5 mm fan foot. Guard protection, fan bolt length and engagement still need +assembly review. All 56 distinct modeled fastener axes have a coaxial mate in another printed part. +This is geometric alignment evidence, not a fastener strength or engagement test. + +Reproduction (from this worktree; use a memory-limited scope for the compiler): + +```sh +systemd-run --user --scope -p MemoryMax=24G -p MemorySwapMax=0 gunbc run \ + --source-root dag --source-root src/v2 \ + --entry dag/gunbc/product/printed_chassis/cassette_review.dag --function main > model.json +systemd-run --user --scope -p MemoryMax=24G -p MemorySwapMax=0 gunbc run \ + --source-root dag --source-root src/v2 \ + --entry dag/gunbc/product/printed_chassis/review_realization.dag --function main > realize-review.py +python realize-review.py model.json fresh-output-directory +``` + +Use the exercised CadQuery 2.8.0 environment. Output includes bed-oriented STL for each printed +instance, local-coordinate STEP, cassette assembly STEP, viewer meshes, a manifest with digests, +parts/print-quantity/interface CSVs and one review ZIP. Generated artifacts and run logs are retained +under `/home/briansrls/print-prep-2026-10-04/cassette-r05`; the Site carries the review package. +The adapter refuses invalid/disconnected printed solids, cuts missing material, out-of-bed parts, +nonmanifold STL, STEP/STL bounds or volume divergence, unmatched fastener bores, orphan interface +members and intersections between printed parts. Kernel bounding boxes explicitly ignore cached +triangulation, while STL bounds allow the modeled tessellation deflection. + +Remaining interfaces are deliberately represented as unresolved: board support thread/height and +underside keep-outs, loaded rail/joint/material behavior, real cooler pose and rear cable access, +fastener engagement, selected power/distribution hardware, thermal performance and unspecified +storage/expansion. No invented standoffs are provided. The first cassette is an unpowered assembly +fit, not a populated operational chassis. Review the complete parts and these interfaces together; +do not restart a gauge-by-gauge process. + +Final exercised generation passed for all 49 printed pieces, 29 unique geometries, 56 matched +fastener axes and zero printed-part intersections. Negative controls separately refused an oversized +part, a disconnected solid, an unknown primitive, a displaced mating bore and an orphan interface. +The emitted adapter was the code actually executed. Local browser checks passed with the final +package; the Site shows the same generated mesh geometry as the downloadable CAD. No slicing, +approval request or printer start was performed in this review step. Reuse the already fitted fan +plate when scheduling quantities after review. + +Review 05 published successfully at the existing private workbench, Site source `a9658fd05d27e83220c611b5fb00ac4397b74739`. diff --git a/sites/.gitignore b/sites/.gitignore new file mode 100644 index 00000000000..bb544ad8a7d --- /dev/null +++ b/sites/.gitignore @@ -0,0 +1,2 @@ +# This hosted Site has its own versioned source repository managed by Sites. +/chassis-workbench/ diff --git a/sites/README.md b/sites/README.md new file mode 100644 index 00000000000..a00804fa3ee --- /dev/null +++ b/sites/README.md @@ -0,0 +1,40 @@ +# Chassis workbench + +[Altra Chassis Workbench](https://altra-chassis-workbench.briansrls448156.chatgpt.site) is the private +assembly review site. Reuse project `appgprj_6ac187f2fe208191b1ec7ad6703d93e8` and the separate +Sites-managed checkout at `sites/chassis-workbench/`; do not create a second site. + +Review 06 consumes meshes and metadata generated from `product.printed_chassis.cassette_review`. +Mechanical geometry, placements, print orientation and interfaces belong to `.dag`. The browser +only displays those meshes; the former `concept.js` and mechanical sliders are removed. View +controls change the camera, visibility, explosion and cassette withdrawal, never printable geometry. + +- Carrier: all 17 printed instances, purchased hardware and labeled clearance reservations. +- All pieces: 12 unique authored print geometries with required quantities; isolate and rotate any. +- Rack: four cassettes in a fixed metal frame, fixed runners and one shared-power reservation. +- Downloads: individual STL/STEP, assembly STEP, logical manifest and a complete review ZIP with + parts, print quantities and interface hardware lists. +- Assembly review: five assembly steps, four tray clamps, six fan/guard bolts, five broad-grip pins, + the original fan fit and remaining hardware decisions. Motherboard standoffs are separate. + +The tray is 271.840 × 294.700 mm, split for the 180 mm A1 mini. The custom frame uses 140 mm bay +pitch; it is not a standard rack-unit compliance claim. Dynatron W1 and P8 envelopes come from +vendor authority. The original fan plate physically fit; r06 retains its hole pattern in a new integral frame/foot. +Shared 12 V power remains fixed per four-node block with four isolatable/protected feeds. Capacity, +branch circuitry, connector choice and redundancy remain unresolved. Board supports are explicitly +unresolved rather than invented geometry. See `docs/plans/printed-chassis-program.md` for evidence, +reproduction, first-assembly scope and the existing fabrication/printing workflow. + +CadQuery validation covers all printed solids, STL manifoldness/bounds/volume, STEP round-trip, +180 mm bed fit and zero positive-volume intersections between printed +parts. It does not qualify strength, engagement, thermals or electrical operation. Local browser +checks cover generated mesh loading, roster, selection/isolation/keyboard rotation, rack extraction, +all-pieces view, filters, hardware/clearance visibility, downloads, interface table and mobile layout. +There are no printer controls, credentials, external application access or persistent user data. + +Earlier visual-only source: concept 04 `c2a49273da70f20a23e2e044fe552dceb74b773f`. + +Historical review 05 Site source: `a9658fd05d27e83220c611b5fb00ac4397b74739` (owner-private audience unchanged). + +Published review 06 Site source: `8dcdc73e2ed18f6498f63204796018727e3f923f` (owner-private). +Joinery/CAD draft: https://github.com/gunb-ai/gunbc/pull/13222 .