diff --git a/design/mechanical/MECHANICAL_DESIGN.md b/design/mechanical/MECHANICAL_DESIGN.md new file mode 100644 index 0000000..bd56e8a --- /dev/null +++ b/design/mechanical/MECHANICAL_DESIGN.md @@ -0,0 +1,780 @@ +# NIGHTWATCH — Observatory Mechanical Design: Rigorous Proof-Out + +> **Generated artifact.** Every number below is computed by the `design/mechanical/calc/` +> package and regenerated by `python3 -m design.mechanical.calc.report`. The test +> `test_report.py` fails if this file drifts from the calculator, so the figures cannot +> silently disagree the way the repo's docs currently do. +> +> **Honesty labels.** Every input is tagged **S** (sourced — stated in a repo file), +> **D** (derived — computed here), or **A** (assumed — the repo is silent; an engineering +> assumption is stated). Nothing here is measured field data; the observatory has never been built. + +## 1. Why this exists + +The repo already documents a mechanical design — `NIGHTWATCH_Build_Package.md` (spec table, costed +BOM, concrete-pier spec) and a `pos/` panel of expert personas (C. Walton Musser on the harmonic +drives, Richard Hedrick on frame stiffness). But it is **assertion, not proof**: the headline +targets are stated, never calculated, and the single most load-bearing fact — which telescope — +contradicts itself. This report turns the prose into computed, self-consistent, margin-carrying +engineering, runs the selection permutations as a real weighted trade study, makes and defends the +bold choices the numbers justify, and fills the CAD and environmental-load voids. + +## 2. Resolved contradictions + +The repo carries numbers that disagree across files. This design pins each to one value in +`params.py` and records the conflict so it is fixed, not buried. + +| # | Contradiction (as found) | Resolution here | +|---|---|---| +| 1 | **OTA identity**: MN76 178 mm f/6 1068 mm **~9 kg** (Build Package) vs MN78 180 mm **f/8** 1440 mm **~14 kg** (`INTES_MICRO_HISTORY.md`, "selected") | Both carried as load cases (`MN76`, `MN78`); the torque + stiffness proofs and the trade study decide (see §Trade Study). | +| 2 | **Encoder PPR**: AMT103 "8192 PPR" everywhere vs "2048 PPR" (`HARDWARE_SETUP.md`) | Pinned to 8192 PPR motor-side; 2048 flagged as a doc error. | +| 3 | **On-axis resolution**: AS5600 "12-bit / 4096" vs `EncoderConfig.resolution=8192` | AS5600 is 12-bit → 4096 counts/rev = ~316 arcsec/count; the 8192 config default is unrelated and misleading. | +| 4 | **Horizon/altitude limit**: 10° (safety monitor) / 15° (`constants.py`) / 20° (scheduler) | Not a mechanical limit — advisory; the mount has no hard slew-altitude cutoff (a noted gap). | +| 5 | **Operating temperature**: −20…40 °C (YAML) vs 20…100 °F (code) | Reconciled to °C internally (−6.7…38 °C from the code Fahrenheit values). | +| 6 | **Site coordinates**: 38.9 / −117.4 (software) vs 39.0 / −117.0 (firmware) | Pinned to 38.9 °N / −117.4 °W, 1800 m for all load derivations. | +| 7 | **Bearings** called "angular contact" but specified as 6008/6006, which are **deep-groove** | Flagged; the bearings + stiffness proofs recommend 7008/7006 angular-contact pairs for moment stiffness. | + +## 3. The three governing voids (repo is silent — filled here, labelled ASSUMED) + +A 6000 ft, seismically-active, remote Nevada site is governed by three structural loads the repo +never states. The software encodes only *operational* interlocks (park at 25 mph, close at 35 mph +gust), which are **not** structural survival ratings. This design makes them first-class: + +| Void | Repo status | Assumed design load (A) | Governs | +|---|---|---|---| +| **Survival wind** | absent (only 25/35 mph operational) | ASCE 7 basic wind ~105 mph 3-sec gust, Risk Cat I | roof anchors, pier overturning | +| **Snow load** | absent entirely | ~25 psf ground snow, high-desert @ 6000 ft | closed-roof structure, roof slope | +| **Seismic** | absent entirely | S_DS ≈ 0.5 g (Walker Lane vicinity) | pier base shear, anchorage | + +Two more the repo flags as open and this design closes: **DGX Spark power/heat** (assumed ~170 W → +energy + enclosure-thermal budget) and the **power/autonomy budget** (solar+battery autonomy hours). + +## 4. Results at a glance + +| # | Proof | Verdict | Governing number | +|---|---|---|---| +| 1 | Axis torque budget | ✅ PASS | Intes-Micro MN78 (f/8), counterweight-FREE: RA needs 49.9 Nm vs 127 Nm rated (SF 2.5); DEC needs 26.5 Nm vs 70 Nm rated (SF 2.6). | +| 2 | Static pointing deflection (OTA horizontal, worst case) | ❌ FAIL | Intes-Micro MN78 (f/8): 8 mm baseline 43.5" (FAIL), 12 mm bold 43.3" (FAIL) vs 5" target -- bearing compliance at the 63.5/76.2 mm spans governs. | +| 3 | First natural frequency (structural first mode) | ✅ PASS | Intes-Micro MN78 (f/8): governing first mode 65 Hz (rocking 65 Hz, bounce 169-220 Hz) vs 10 Hz target -- SF 6.5, clear of the <2 Hz wind and 1-5 Hz servo bands. | +| 4 | Tracking-error (pointing) budget | ❌ FAIL | Baseline dual-encoder chain reaches only 5.02 arcsec RMS, FAILING the 1.0 arcsec target; the on-axis RESA ring reaches 0.54 arcsec RMS. An on-axis high-resolution absolute encoder is REQUIRED to reach sub-arcsecond. | +| 5 | Mass-balance proof (counterweighted & counterweight-free) | ✅ PASS | Intes-Micro MN78 (f/8): 12.5 kg balances the 18 kg payload at r_cw=288 mm on the 457 mm shaft (fit factor 1.59). Counterweight-FREE deletes 15.4 kg and 29% of RA inertia — viable per the torque proof (RA SF 2.5). | +| 6 | Axis bearing L10 fatigue + static safety | ✅ PASS | Load capacity is NOT the constraint: L10 ~ 5e+07 yr (RA) / 3e+07 yr (DEC) at ~1 rev/sidereal-day, static S0 18x / 14x. FINDING: the repo calls the 6008/6006 'angular contact' but 60xx are DEEP-GROOVE with poor moment stiffness — use matched angular-contact 7008/7006 pairs (back-to-back) for moment stiffness (see stiffness proof). | +| 7 | Wind structural loads (operational drag + survival uplift) | ❌ FAIL | Survival wind (105 mph) GOVERNS: 9.2 kN roof uplift vs 1.8 kN roof self-weight (SF 0.19) -- hold-down anchors are MANDATORY and unspecified in the repo (4x 2 klbf anchors -> SF 3.9). Operational gust drag on Intes-Micro MN78 (f/8) is only 41 N. | +| 8 | Pier / foundation (tilt, frequency, seismic, concrete) | ✅ PASS | 12"x36" concrete pier: tilt 0.70" (SF 7), f_n 152 Hz (SF 15), seismic overturning SF 7.0 (dead-weight-only 0.81 -> embedment governs), concrete SF 14. Governing SF 6.8 (PASS). | +| 9 | Thermal focus stability over the diurnal swing | ❌ FAIL | Passive focus is NOT enough: over the 22 K diurnal swing the 6061-T6 tube walks focus ~10x the f/8 depth of focus (only 2.1 K to leave best focus) -> a temperature-compensated focuser is REQUIRED (the repo focuser has a -2.5 steps/C coefficient). The Astrositall mirror adds only ~5 um (0.7% of the tube term); a ~1.3 W corrector heater beats the 2 W all-sky ring. | +| 10 | Roll-off roof drive + snow load | ✅ PASS | Drive PASS: moving the 180 kg roof against rolling + the 35 mph close gust needs 235 N (11.8 Nm @ 5 cm wheel), SF 2.1 vs a ~500 N garage-door-class drive — and wind is 62% of that load, so the drive is WIND-sized. FINDING: the closed flat roof carries 10.8 kN of snow (~6.1x its dead weight) — the snow case is the roof's structural design driver; a sloped roof sheds it, and a snow-laden roof CANNOT be driven -> snow interlock required. | +| 11 | Night-time energy budget (UPS + solar autonomy) | ❌ FAIL | Night load ~287 W, 59% of it the ASSUMED 170 W DGX Spark. The 1500 VA/900 W UPS rides shutdown with 16x margin (~31 min) but only ~5% of a night. Off-grid AUTONOMY FAIL: the specified 400 W + 100 Ah pack gives only 3.3 h at 12 V (< 10 h night) — a 48 V pack gives 13.4 h — and the panel harvests ~1.4 kWh/day vs ~2.9 kWh needed. Drop/duty-cycle the DGX or go 48 V for true autonomy. (DGX dumps ~14 K into the enclosure at 2 ACH -> ventilate or locate it outside.) | + +**6 PASS · 0 MARGINAL · 5 FAIL.** The FAILs are the value: each is a real gap in the repo's design that assertion had hidden — the bearing-limited stiffness, the sub-arcsec-incapable encoder chain, the missing survival-wind anchors, passive focus over the diurnal swing, and off-grid autonomy. Every FAIL carries a computed remedy below. + +## 5. The proofs + +### 1. Axis torque budget — ✅ PASS + +**Intes-Micro MN78 (f/8), counterweight-FREE: RA needs 49.9 Nm vs 127 Nm rated (SF 2.5); DEC needs 26.5 Nm vs 70 Nm rated (SF 2.6).** + +*Target:* Required axis torque < rated (SF>=2 continuous, peak covers goto) +*Governing safety factor:* 2.55× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Payload mass (OTA + train) | 18 | kg | | +| RA gravity imbalance torque | 35.3 | Nm | payload weight x CG offset | +| RA wind torque @ 35 mph gust | 8.13 | Nm | | +| RA goto-inertia torque | 0.105 | Nm | | +| RA friction torque | 6.35 | Nm | | +| RA total required | 49.89 | Nm | | +| RA rated / required (SF) | 2.546 | x | | +| RA peak / required (SF) | 6.875 | x | | +| DEC total required | 26.53 | Nm | | +| DEC rated / required (SF) | 2.639 | x | | +| DEC peak / required (SF) | 7.011 | x | | + +_Assumptions:_ +- RA CG offset from polar axis = 200 mm (GEM geometry estimate, DERIVED) +- Balance residual (counterweighted) = 2% (ASSUMED) +- Drive+bearing friction = 5% of rated torque (ASSUMED) +- DEC wind center-of-pressure at 34% of tube length from the DEC pivot (ASSUMED, conservative; governs the DEC total). +- Wind torque uses the 35 mph emergency-close gust (max wind while open), SOURCED. +- Air density 1.027 kg/m^3 at 1800 m (DERIVED), 17% below sea level. + +### 2. Static pointing deflection (OTA horizontal, worst case) — ❌ FAIL + +**Intes-Micro MN78 (f/8): 8 mm baseline 43.5" (FAIL), 12 mm bold 43.3" (FAIL) vs 5" target -- bearing compliance at the 63.5/76.2 mm spans governs.** + +*Target:* Summed gravity deflection < 5 arcsec (Hedrick), SF>=1.5 to PASS +*Governing safety factor:* 0.12× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Payload mass (OTA + train) | 18 | kg | | +| Gravity lever L_cg (cg + saddle) | 0.2833 | m | along-tube CG + assumed 50 mm saddle offset | +| Gravity moment at DEC axis | 50.01 | Nm | W x L_cg, tube horizontal | +| -- 8 mm baseline housings -- | 0 | | | +| DEC housing beam deflection | 0.5903 | arcsec | | +| RA housing beam deflection | 0.2871 | arcsec | | +| DEC bearing-pair tilt | 28.43 | arcsec | 6006 @ 180 N/um, 63.5 mm span | +| RA bearing-pair tilt | 14.21 | arcsec | 6008 @ 250 N/um, 76.2 mm span | +| 8 mm TOTAL deflection | 43.52 | arcsec | | +| 8 mm target / actual (SF) | 0.1149 | x | FAIL | +| -- 12 mm bold housings -- | 0 | | | +| 12 mm beam deflection (DEC+RA) | 0.6295 | arcsec | | +| 12 mm bearing tilt (DEC+RA) | 42.64 | arcsec | unchanged -- wall does not stiffen bearings | +| 12 mm TOTAL deflection | 43.27 | arcsec | | +| 12 mm target / actual (SF) | 0.1156 | x | FAIL | +| Bearing share of total (8 mm) | 97.98 | % | | + +_Assumptions:_ +- Worst case: OTA horizontal, gravity perpendicular to the optical axis (SOURCED as the design case). +- L_cg = OTA.cg_from_saddle (233 mm) + saddle offset 50 mm (saddle undimensioned in repo, ASSUMED). +- Bearing radial stiffness 6008~250 N/um, 6006~180 N/um (repo lists only load ratings, not stiffness) -- ASSUMED. +- Bearing span = housing depth (bearings at the box faces): DEC 63.5 mm, RA 76.2 mm. +- The full payload moment is applied through BOTH the DEC and RA load paths (overhung bending on the RA bearings persists even when torque-balanced) -- a bounding assumption. +- Deep-groove bearings modelled; the repo labels them 'angular contact' (flagged in params) -- a preloaded angular-contact pair would raise stiffness materially. + +### 3. First natural frequency (structural first mode) — ✅ PASS + +**Intes-Micro MN78 (f/8): governing first mode 65 Hz (rocking 65 Hz, bounce 169-220 Hz) vs 10 Hz target -- SF 6.5, clear of the <2 Hz wind and 1-5 Hz servo bands.** + +*Target:* First mode > 10 Hz (Hedrick), SF>=1.5 to PASS; must clear 1-5 Hz servo +*Governing safety factor:* 6.46× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Pier lateral stiffness | 4.133e+07 | N/m | concrete cantilever 3EI/H^3 | +| Head series stiffness k_eff | 3.440e+07 | N/m | pier+beams+bearings in series | +| Payload mass (CW-free) | 18 | kg | | +| Payload + counterweights | 30.5 | kg | | +| Bounce mode, CW-free | 220 | Hz | translational | +| Bounce mode, counterweighted | 169 | Hz | heavier -> lower f | +| Rocking mode (governing) | 64.65 | Hz | overhung on bearing angular stiffness | +| Governing first mode | 64.65 | Hz | min of all modes | +| Governing / target (SF) | 6.465 | x | PASS | +| Wind-gust excitation band | 2 | Hz | must stay below f_n | +| Servo/guide bandwidth | 5 | Hz | must stay below f_n | + +_Assumptions:_ +- Pier modelled as a solid concrete lateral cantilever (I = pi*D^4/64); its own mass and soil/footing compliance are neglected (stiffer, non-conservative for the pier term but it is not governing). +- Bearing radial stiffness 6008~250 N/um, 6006~180 N/um (ASSUMED, shared with stiffness.py); rocking uses the derived angular stiffness k*span^2/2. +- Modal mass = payload (+ 12.5 kg counterweights); head/drive masses and pier participation neglected -> the true modes are slightly lower. +- Rocking inertia J = m*L_cg^2 with L_cg the same gravity lever as stiffness.py (233 mm CG + 50 mm saddle); point-mass approximation. +- 12 mm bold housings used for the head beams (recommended build); beam terms are negligible vs pier and bearing terms either way. +- Verdict taken on the lowest mode found (rocking), not the higher bounce mode -- the honest first mode. + +### 4. Tracking-error (pointing) budget — ❌ FAIL + +**Baseline dual-encoder chain reaches only 5.02 arcsec RMS, FAILING the 1.0 arcsec target; the on-axis RESA ring reaches 0.54 arcsec RMS. An on-axis high-resolution absolute encoder is REQUIRED to reach sub-arcsecond.** + +*Target:* Tracking error < 1.0 arcsec RMS (P.TRACKING_RMS_TARGET_ARCSEC) +*Governing safety factor:* 0.20× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| AMT103 motor-side resolution (to axis) | 1.582 | arcsec/LSB | 8192 PPR x100 upstream ratio | +| AS5600 on-axis resolution | 316.4 | arcsec/LSB | 12-bit => quant RMS 91", homing-grade only | +| RESA on-axis resolution (proposed) | 0.005538 | arcsec/LSB | absolute ring, sub-arcsec | +| Baseline: motor quant RMS | 0.4567 | arcsec | res/sqrt(12) | +| Baseline: harmonic PE residual | 4 | arcsec | unseen by motor enc | +| Baseline: drivetrain/mount flexure | 3 | arcsec | unseen by motor enc | +| Baseline total tracking RMS | 5.021 | arcsec | RSS of the three | +| Baseline margin (target/achieved) | 0.1992 | x | <1 => fails | +| Proposed: RESA quant RMS | 0.001599 | arcsec | res/sqrt(12) | +| Proposed: servo following error | 0.2 | arcsec | on-axis closed loop | +| Proposed: residual OTA flexure | 0.5 | arcsec | downstream of ring | +| Proposed total tracking RMS | 0.5385 | arcsec | RSS of the three | +| Proposed margin (target/achieved) | 1.857 | x | >1 => meets | + +_Assumptions:_ +- Quantisation RMS = LSB/sqrt(12) (ideal uniform quantiser). +- Baseline servo closes on the MOTOR encoder (1.58" to axis); the AS5600's 91" quant RMS makes it a homing reference only, so harmonic PE and mount flexure leak into pointing. SOURCED chain. +- Harmonic PE residual = 4.0" RMS (ASSUMED), consistent with CSF repeatability P.RA_DRIVE=6" / P.DEC_DRIVE=8". +- Drivetrain + mount flexure = 3.0" RMS (ASSUMED, stated structural budget), entirely upstream of the axis and thus invisible to a motor encoder. +- Proposed on-axis ring corrects everything upstream of the axis; only servo following (0.2" RMS, ASSUMED) and residual OTA/focuser flexure (0.5" RMS, ASSUMED) downstream of the ring remain. +- Terms combine in RSS (independent, zero-mean error sources). + +### 5. Mass-balance proof (counterweighted & counterweight-free) — ✅ PASS + +**Intes-Micro MN78 (f/8): 12.5 kg balances the 18 kg payload at r_cw=288 mm on the 457 mm shaft (fit factor 1.59). Counterweight-FREE deletes 15.4 kg and 29% of RA inertia — viable per the torque proof (RA SF 2.5).** + +*Target:* RA balance achievable within shaft length AND counterweight-free viable (torque proof) +*Governing safety factor:* 1.59× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Payload mass (OTA + train) | 18 | kg | | +| Payload CG offset from polar axis (r_payload) | 0.2 | m | torque.RA_CG_OFFSET_M | +| Payload moment about RA axis | 3.6 | kg*m | m_payload * r_payload | +| Counterweights available | 12.5 | kg | 2x5 + 1x2.5 kg | +| Required shaft position r_cw | 0.288 | m | m_cw * r_cw = m_pay * r_pay | +| Shaft length available | 0.4572 | m | 18" 303-SS shaft | +| Fit factor (shaft_len / r_cw) | 1.587 | x | >1 => fits | +| DEC rear-stack imbalance moment | 5.884 | Nm | train weight x 150 mm; nulled by dovetail slide | +| DEC residual moment after adjust | 3.53 | Nm | residual lever 20 mm (torque.DEC_RESIDUAL_LEVER_M) | +| Counterweight shaft mass (rho*A*L) | 2.896 | kg | 303-SS solid rod | +| Total deleted mass (cw-free) | 15.4 | kg | weights + shaft | +| RA payload inertia (polar axis) | 3.007 | kg*m^2 | | +| Deleted RA inertia (weights+shaft) | 1.239 | kg*m^2 | | +| RA inertia reduction (cw-free) | 29.18 | % | | +| Counterweight-free viable (torque) | 1 | bool | torque verdict PASS | + +_Assumptions:_ +- r_payload = RA CG offset 200 mm (imported from torque.RA_CG_OFFSET_M, DERIVED). +- Counterweights modelled as a point mass; the shaft's own distributed moment is NOT credited toward balance -> conservative (pushes r_cw outward, not inward). +- Counterweight shaft mass = rho*A*L of 303-SS (rho=8000 kg/m^3, d=31.8 mm, L=457 mm) = 2.90 kg, DERIVED. +- DEC fore-aft balance is set by sliding the OTA in its dovetail saddle (no DEC counterweight); residual lever 20 mm from torque.DEC_RESIDUAL_LEVER_M, ASSUMED. +- Deleted RA inertia = weights as point mass at r_cw + shaft as a rod about the axis end (I = 1/3 m L^2), DERIVED. +- Counterweight-free viability is inherited from the torque proof (torque.evaluate('MN78', counterweight_free=True) => PASS), not re-derived here. + +### 6. Axis bearing L10 fatigue + static safety — ✅ PASS + +**Load capacity is NOT the constraint: L10 ~ 5e+07 yr (RA) / 3e+07 yr (DEC) at ~1 rev/sidereal-day, static S0 18x / 14x. FINDING: the repo calls the 6008/6006 'angular contact' but 60xx are DEEP-GROOVE with poor moment stiffness — use matched angular-contact 7008/7006 pairs (back-to-back) for moment stiffness (see stiffness proof).** + +*Target:* L10 >> service life AND static S0 >= 2 (fatigue not the constraint) +*Governing safety factor:* 13.66× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Payload mass (OTA + train) | 18 | kg | | +| RA overturning moment (W x CG offset) | 35.3 | Nm | lever = RA_CG_OFFSET_M | +| RA bearing span (housing depth) | 0.0762 | m | sets couple arm | +| RA output bearing load P | 639.8 | N | W + M/span (6008) | +| RA dynamic rating C | 1.68e+04 | N | | +| RA L10 life | 1.810e+10 | rev | (C/P)^3 x 1e6 | +| RA L10 life | 4.956e+07 | yr | @ ~1 rev/sidereal day | +| RA static safety S0 = C0/P | 18.13 | x | C0=11600 N | +| DEC overturning moment (W x lever) | 27.36 | Nm | lever = tube r + saddle stack | +| DEC bearing span (housing depth) | 0.0635 | m | | +| DEC output bearing load P | 607.4 | N | W + M/span (6006) | +| DEC dynamic rating C | 1.33e+04 | N | | +| DEC L10 life | 2.874e+07 | yr | @ ~1 rev/sidereal day | +| DEC static safety S0 = C0/P | 13.66 | x | C0=8300 N | + +_Assumptions:_ +- Governing bearing load P = W + M/span (direct transverse weight + moment couple); no load-sharing credit on the direct term -> conservative. +- RA lever = RA_CG_OFFSET_M 200 mm (from torque, DERIVED); DEC lever = tube radius 105 mm + saddle stack 50 mm (saddle stack ASSUMED). +- Bearing span = housing depth (P.RA_HOUSING.depth_m / P.DEC_HOUSING.depth_m); axis tilt at latitude would reduce the transverse component, so vertical W is conservative. +- L10 = (C/P)^3 x 1e6 rev; converted to years at 1 rev/sidereal day (~1/day). Both axes exceed 1e7 years -> rolling fatigue is NOT a lifetime constraint. +- FINDING: params labels BRG_RA_6008 / BRG_DEC_6006 'angular contact', but 60xx are DEEP-GROOVE ball bearings (params.py already flags the deep-groove C ratings). Deep-groove bearings have poor moment/tilting stiffness. +- RECOMMENDATION: replace with matched ANGULAR-CONTACT pairs — 7008 (RA) / 7006 (DEC) — in a back-to-back (O) arrangement with preload, which provides the moment stiffness that sets pointing deflection. This is a STIFFNESS decision, not a load-rating one (see stiffness proof). +- The RA output bearing additionally carries DEC-head and (if fitted) counterweight dead weight not modelled here; even at 2-3x the modelled load, S0 stays > 5 and L10 > 1e6 yr, so the conclusion is robust. + +### 7. Wind structural loads (operational drag + survival uplift) — ❌ FAIL + +**Survival wind (105 mph) GOVERNS: 9.2 kN roof uplift vs 1.8 kN roof self-weight (SF 0.19) -- hold-down anchors are MANDATORY and unspecified in the repo (4x 2 klbf anchors -> SF 3.9). Operational gust drag on Intes-Micro MN78 (f/8) is only 41 N.** + +*Target:* Survival roof uplift resisted by hold-down (self-weight insufficient); operational drag feeds torque/pier budgets +*Governing safety factor:* 0.19× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Air density @ 1800 m | 1.027 | kg/m^3 | ISA, 16% below sea level | +| q @ 25 mph park | 64.13 | Pa | | +| q @ 35 mph gust | 125.7 | Pa | | +| OTA drag @ 25 mph (park) | 20.74 | N | | +| OTA drag @ 35 mph (gust) | 40.65 | N | max wind while open | +| Wind moment at pier TOP @ gust | 20.32 | Nm | F x mount-head height | +| Wind moment at pier BASE @ gust | 57.5 | Nm | F x (pier + head height) | +| -- survival, roof closed -- | 0 | | | +| q @ 105 mph survival | 1131 | Pa | | +| Roof gross uplift | 9163 | N | q x GCp x A_roof | +| Roof self-weight | 1765 | N | | +| Roof NET uplift (anchor demand) | 7398 | N | uplift - self-weight | +| Roof self-weight / uplift (SF) | 0.1926 | x | FAIL | +| Assumed anchor capacity (4x) | 3.559e+04 | N | | +| Anchored hold-down (SF) | 3.884 | x | remediation | +| Enclosure lateral drag | 9774 | N | q x Cf x A_wall | +| Enclosure overturning moment | 1.173e+04 | Nm | about leeward base edge | +| Overturning, dead-weight only (SF) | 0.6592 | x | anchors required | +| Survival governs vs operational | 1 | bool | | + +_Assumptions:_ +- Dynamic pressure uses site air density 1.027 kg/m^3 (ISA @ 1800 m, DERIVED) — lower than sea level, so loads are honest not inflated. +- Operational max wind = 35 mph emergency-close gust (SOURCED safety monitor); the OTA is only exposed with the roof open, so it never sees survival wind. +- Survival basic wind 105 mph is ASSUMED (ASCE 7 central-NV Risk Cat I; repo is silent — a governing void). +- Roof footprint 9 m^2 and mass 180 kg are ASSUMED (params flags roof geometry as unspecified). +- Net uplift coefficient GCp = 0.9 (SOURCED param); wall force coefficient Cf = 1.2 on an assumed 2.4 m wall (ASSUMED). +- Mount-head height 500 mm (OTA axis above pier top) is ASSUMED; sets the operational overturning lever. +- Repo specifies NO roof hold-down or enclosure anchorage; the anchor scheme (4x 2 klbf, ASSUMED) is shown only as remediation, not as an existing spec. +- The pier is assumed structurally isolated from the enclosure (standard observatory practice), so roof uplift loads the enclosure foundation, not the pier. + +### 8. Pier / foundation (tilt, frequency, seismic, concrete) — ✅ PASS + +**12"x36" concrete pier: tilt 0.70" (SF 7), f_n 152 Hz (SF 15), seismic overturning SF 7.0 (dead-weight-only 0.81 -> embedment governs), concrete SF 14. Governing SF 6.8 (PASS).** + +*Target:* Tilt << 5"; f_n > 10 Hz; seismic stable (embed+weight); concrete < f'c & modulus of rupture +*Governing safety factor:* 6.83× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Pier I (pi d^4/64) | 4.237e-04 | m^4 | | +| Pier lateral stiffness k | 4.133e+07 | N/m | 3EI/L^3 cantilever | +| -- tilt (35 mph gust) -- | 0 | | | +| OTA drag @ 35 mph | 40.65 | N | from wind.py | +| Pier tip deflection | 0.9836 | um | F L^3/3EI | +| Pier pointing tilt | 0.6968 | arcsec | tip slope F L^2/2EI | +| Tilt budget / actual (SF) | 7.176 | x | vs 5" pointing budget | +| -- frequency -- | 0 | | | +| Tip mass (head+payload+CW) | 45.4 | kg | | +| Pier first mode | 151.9 | Hz | sqrt(k/m)/2pi | +| f_n / target (SF) | 15.19 | x | vs 10 Hz | +| -- seismic (S_DS=0.5) -- | 0 | | | +| Seismic weight W | 2016 | N | tip + exposed pier | +| Base shear V = S_DS W | 1008 | N | | +| Overturning moment | 673.8 | Nm | about grade | +| Soil passive resultant Pp | 6881 | N | assumed granular, Kp=3 | +| Overturning SF (embed+weight) | 7.036 | x | | +| Overturning SF (dead-weight only) | 0.811 | x | < 1 -> embedment required | +| Sliding SF (Pp / V) | 6.828 | x | | +| -- concrete stress -- | 0 | | | +| Axial bearing stress | 6101 | Pa | | +| Flexural stress (seismic) | 2.424e+05 | Pa | M c / I | +| Net compression / f'c (SF) | 111 | x | | +| Net tension / modulus rupture (SF) | 13.78 | x | cracking check | +| -- frost -- | 0 | | | +| Embedment / frost depth (SF) | 1.524 | x | 0.914 m vs assumed 0.6 m frost | +| Governing safety factor | 6.828 | x | PASS | + +_Assumptions:_ +- Pier modelled as a solid circular concrete cantilever fixed at grade (I = pi*d^4/64, k = 3EI/L^3); soil-spring base fixity and pier self-mass in the frequency term are neglected (slightly non-conservative on stiffness). +- Pointing tilt uses the cantilever tip SLOPE F*L^2/2EI (the true mount-base rotation), 1.5x the delta/L estimate of the tip deflection F*L^3/3EI. +- Tilt driven by the 35 mph operational gust (roof-open max); the OTA is shielded at survival wind, so wind never governs the pier structurally. +- Tip mass = assumed 12 kg head + payload (18 kg) + counterweights (15.4 kg, shaft mass DERIVED from geometry); head mass ASSUMED. +- Seismic S_DS = 0.5 g is ASSUMED (Walker Lane vicinity; repo silent). Simplified ELF: V = S_DS*W, two-mass overturning distribution. +- Soil ASSUMED medium-dense granular: gamma = 18 kN/m^3, Kp = 3.0 (phi ~ 30 deg); passive resultant over the 0.914 m embedment provides the overturning resistance — dead weight alone would not. +- Concrete f'c = 27.6 MPa (SOURCED); modulus of rupture f_r = 0.62*sqrt(f'c) = 3.26 MPa (ASSUMED code value) used for the tension/cracking check. +- Frost line ASSUMED 0.3-0.6 m for central-NV high desert; the 0.914 m embedment (SOURCED) exceeds it. +- Coupled head/bearing rocking (the true system first mode) is owned by dynamics.py; this module only confirms the pier itself is not the soft/low-frequency element. + +### 9. Thermal focus stability over the diurnal swing — ❌ FAIL + +**Passive focus is NOT enough: over the 22 K diurnal swing the 6061-T6 tube walks focus ~10x the f/8 depth of focus (only 2.1 K to leave best focus) -> a temperature-compensated focuser is REQUIRED (the repo focuser has a -2.5 steps/C coefficient). The Astrositall mirror adds only ~5 um (0.7% of the tube term); a ~1.3 W corrector heater beats the 2 W all-sky ring.** + +*Target:* Passive focus stays within +/- DoF over the 22 K swing (SF = K_to_edge / swing) +*Governing safety factor:* 0.10× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Depth of focus, MN76 f/6 (+/-) | 39.6 | um | 2*lambda*N^2 | +| Depth of focus, MN78 f/8 (+/-) | 70.4 | um | larger -> f/8 more forgiving | +| DoF ratio f8/f6 | 1.778 | x | ~(8/6)^2 = 1.78 (scales as N^2) | +| Tube length (MN78) | 1.4 | m | aluminium metering length (choice) | +| Aluminium CTE | 2.360e-05 | 1/K | 6061-T6 (params) | +| Tube defocus per K | 33.04 | um/K | CTE_al * L_tube | +| K to leave best focus | 2.131 | K | DoF / (defocus per K) | +| Diurnal swing | 22.22 | K | P.ENV.diurnal_swing_c | +| Tube defocus over swing | 734.2 | um | = 10.4 x DoF | +| Passive-focus SF | 0.09588 | x | K_to_edge / swing (<1 => fails) | +| Astrositall CTE | 1.500e-07 | 1/K | ~157x below aluminium | +| Mirror defocus per K | 0.216 | um/K | f * CTE_mirror | +| Mirror defocus over swing | 4.8 | um | negligible vs tube | +| Tube / mirror defocus ratio | 153 | x | aluminium dominates | +| POS modelled coeff (MN78) | 2 | um/K | vs first-principles bare-Al tube -> optimistic | +| Corrector area (from aperture) | 0.02545 | m^2 | pi*(D/2)^2 | +| Dew heater P (hold +5 K) | 1.272 | W | h=10 W/m^2K | +| All-sky ring reference | 2 | W | heater fits inside this | + +_Assumptions:_ +- Depth of focus DoF = 2*lambda*N^2 (one-sided, +/-), lambda = P.OPTICAL.wavelength_m (0.55 um); the full in-focus window is twice this. DoF scales as N^2 so f/8 > f/6. +- Tube-expansion defocus uses the TUBE LENGTH (P.OTA.tube_length_m) as the aluminium metering length, not the focal length: it is the physical aluminium between the primary cell and the focuser, and for these folded Mak-Newts (700/1400 mm tube vs 1068/1440 mm focal) it is the more physical and less alarming choice. DERIVED. +- Tube growth maps ~1:1 to defocus (no strong secondary magnification in a Mak-Newt); a catadioptric focus-amplification factor would make passive focus WORSE, so 1:1 is non-conservative-favourable and the FAIL verdict is robust. ASSUMED. +- Mirror term ~ f * CTE_mirror with CTE_mirror = P.OPTICAL.astrositall_cte (1.5e-7/K); system focal length used as an upper-bound proxy for the primary's focal length. DERIVED. +- Dew heater P = h*A*dT with h = 10 W/m^2K (still-air convection+radiation, ASSUMED), dT = 5 K above dewpoint (ASSUMED), A = corrector area from the clear aperture (meniscus is a few % larger -> mild under-estimate). +- All-sky dew ring reference = 2 W (stated reference figure, ASSUMED). +- Focuser temperature coefficient -2.5 steps/C is SOURCED from the repo focuser service (tests/unit/test_focuser_service.py default; docs/telescope_tools note ~2.5 steps/C) but is NOT in params.py; a focuser step size would be needed to convert steps -> microns and confirm the compensation resolves the tube term. NOT in params (see notes). +- FINDING: first-principles bare-6061-T6 defocus is ~33 um/K, whereas the POS retreat sim models MN78 at 2 um/K (pos/POS_RETREAT_SIMULATION.md TEMP_COEFFICIENT) -> the POS figure implies a low-CTE (carbon-fibre / compensated) tube or is ~16x optimistic for bare aluminium. SOURCED cross-check. +- CONCLUSION: passive focus FAILS the diurnal swing by ~10x; a temperature-compensated focuser (which the repo has) is REQUIRED, not optional. The Astrositall primary and a <2 W corrector heater are genuine wins that make the residual tractable once the tube term is compensated. + +### 10. Roll-off roof drive + snow load — ✅ PASS + +**Drive PASS: moving the 180 kg roof against rolling + the 35 mph close gust needs 235 N (11.8 Nm @ 5 cm wheel), SF 2.1 vs a ~500 N garage-door-class drive — and wind is 62% of that load, so the drive is WIND-sized. FINDING: the closed flat roof carries 10.8 kN of snow (~6.1x its dead weight) — the snow case is the roof's structural design driver; a sloped roof sheds it, and a snow-laden roof CANNOT be driven -> snow interlock required.** + +*Target:* Roof drive overcomes rolling + 35 mph gust with SF >= 2 and traverses within open_time +*Governing safety factor:* 2.12× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Roof mass | 180 | kg | ASSUMED (params) | +| Rolling resistance force | 88.26 | N | mu=0.05 * m * g | +| Wind drag @ 35 mph gust | 147.1 | N | 0.5*rho*V^2*Cd*A, Cd=1.3 | +| Frontal area (wind) | 0.9 | m^2 | span x 0.3 m fascia | +| Total tractive force | 235.3 | N | rolling + wind | +| Wind fraction of tractive force | 62.49 | % | drive is wind-sized | +| Drive torque @ wheel | 11.77 | Nm | F * 0.05 m | +| Drive capacity (reference) | 500 | N | garage-door-opener class, ASSUMED | +| Drive safety factor | 2.125 | x | capacity / tractive (pass >= 2) | +| Required roof speed | 0.06667 | m/s | 3 m / 45 s | +| Feasible speed ceiling | 0.3 | m/s | speed ok = True | +| Open time vs motor timeout | 45 | s | < 60 s timeout | +| Ground snow pressure | 1197 | Pa | 25 psf, ASSUMED (params) | +| Roof plan area | 9 | m^2 | span x length | +| Closed-roof snow load | 1.077e+04 | N | pressure x area | +| Snow mass on roof | 1099 | kg | ~1.1 tonne | +| Snow / roof dead weight | 6.103 | x | snow dominates the structure | +| Snow-laden rolling force | 626.9 | N | mu*(m_roof+m_snow)*g > drive | + +_Assumptions:_ +- Rolling resistance coefficient mu = 0.05 (ASSUMED): far above ideal steel-on-steel (~0.001-0.005) to cover V-groove wheels, misalignment, seal drag and grit — conservative. +- Drive-wheel radius 0.05 m (ASSUMED, stated). Drive torque = F * radius. +- Wind drag uses the 35 mph emergency-close gust (P.ENV.wind_gust_close_ms, SOURCED) on a frontal area = roof span x 0.3 m fascia height (ASSUMED) with a flat-panel Cd=1.3 (ASSUMED; ENV.cd_cylinder=1.1 is for a round tube). Air density 1.027 kg/m^3 at 1800 m (DERIVED). The result is sensitive to the assumed fascia height: a taller roof profile pushes the drive SF toward MARGINAL. +- Drive capacity 500 N is a residential garage-door-opener-class chain drive (ASSUMED reference); a commercial roof drive is stronger. FINDING: at 35 mph the WIND is 62% of the tractive load, so the drive is sized by wind, not roof weight — a positive close against the gust is the governing drive requirement. +- Required roof speed 0.067 m/s (3 m in 45 s) is well under the 0.30 m/s comfort/safety ceiling (ASSUMED), and open_time 45 s < 60 s motor timeout (SOURCED), so travel time is not the constraint. +- SNOW (structural): the closed flat roof carries the full ground snow load (25 psf, ASSUMED) = 10.8 kN over 9 m^2, ~6.1x the roof's own 1.8 kN dead weight. The snow case, not wind, is the roof panel/support design driver. A SLOPED, slippery roof sheds most of it (ASCE 7 slope factor Cs -> ~0 for a steep metal roof); a FLAT roll-off roof must carry all of it. +- SNOW (operational): a fully snow-laden roof needs 627 N to roll, which EXCEEDS the 500 N drive -> the roof must never be commanded open under snow. A snow/ice interlock (or a pitched shedding roof) is required; the safety monitor should hold the roof CLOSED while snow-loaded. +- OPEN ITEM: params.py specifies no roof structural section, so the snow load here is reported as the governing DESIGN LOAD, not certified against a computed roof capacity — see notes (recommend a POS Enclosure roof-structure entry). + +### 11. Night-time energy budget (UPS + solar autonomy) — ❌ FAIL + +**Night load ~287 W, 59% of it the ASSUMED 170 W DGX Spark. The 1500 VA/900 W UPS rides shutdown with 16x margin (~31 min) but only ~5% of a night. Off-grid AUTONOMY FAIL: the specified 400 W + 100 Ah pack gives only 3.3 h at 12 V (< 10 h night) — a 48 V pack gives 13.4 h — and the panel harvests ~1.4 kWh/day vs ~2.9 kWh needed. Drop/duty-cycle the DGX or go 48 V for true autonomy. (DGX dumps ~14 K into the enclosure at 2 ACH -> ventilate or locate it outside.)** + +*Target:* Solar+battery sustains the ~10 h winter night on the specified pack (SF = autonomy / night) +*Governing safety factor:* 0.33× + +| Quantity | Value | Unit | Note | +|---|---:|---|---| +| Mount (idle/track) | 36 | W | 12 V * 1.5 A * 2 axes | +| Mount (goto slew) | 48 | W | IGOTO 2.0 A (transient) | +| Camera | 36 | W | cooled CMOS + TEC, ASSUMED | +| Focuser | 5 | W | ASSUMED | +| Controllers / Pi | 15 | W | ASSUMED | +| Weather / sensors | 10 | W | ASSUMED | +| Network | 15 | W | ASSUMED | +| DGX Spark | 170 | W | ASSUMED (repo gives no wattage) | +| Night load (steady) | 287 | W | sum, mount idle | +| Night load (during goto) | 299 | W | mount at IGOTO | +| DGX share of night load | 59.23 | % | the load is DGX-dominated | +| UPS rating | 900 | W | 1500 VA, SOURCED spec | +| UPS usable energy | 150 | Wh | ~2x12V9Ah class, ASSUMED | +| UPS runtime @ night load | 31.36 | min | validates ~30 min spec | +| Shutdown time (park+close) | 2 | min | ASSUMED | +| UPS shutdown margin | 15.68 | x | usable / shutdown energy | +| UPS fraction of a night | 5.226 | % | of 10 h -> NOT overnight | +| Solar panel | 400 | W | SOURCED spec | +| Battery capacity | 100 | Ah | LiFePO4, SOURCED spec | +| Battery energy @ 12 V | 960 | Wh | 80% usable | +| Battery energy @ 48 V | 3840 | Wh | 4x -> voltage is decisive | +| Autonomy @ 12 V | 3.345 | h | < 10 h night -> FAILS | +| Autonomy @ 48 V | 13.38 | h | > 10 h night -> passes | +| Daily solar harvest (winter) | 1350 | Wh | 400W*4.5PSH*0.75 | +| Night energy need | 2870 | Wh | load * 10 h | +| Harvest / night need | 47.04 | % | < 100% -> not fully self-sustaining | +| Enclosure volume | 21.6 | m^3 | footprint x 2.4 m | +| DGX interior dT @ 2 ACH | 13.73 | K | forced ventilation needed | + +_Assumptions:_ +- Motor bus 12 V (ASSUMED; not in params) x IRUN 1.5 A / IGOTO 2.0 A (params) x 2 axes. V_bus*I over-estimates supply draw (the TMC driver chops the phase current, and DEC holds at reduced current), so the mount figure is conservative. +- Device wattages ASSUMED (repo gives none): camera 36 W (cooled CMOS+TEC), focuser 5 W, controllers/Pi 15 W, weather/sensors 10 W, network 15 W. These sum to ~117 W of non-DGX load. +- DGX Spark 170 W is ASSUMED — the README names 'NVIDIA DGX Spark' (Grace+Blackwell class) but states NO wattage. It is ~59% of the whole night load and therefore dominates the UPS, battery and thermal sizing. Every autonomy conclusion below hinges on this one assumed number. +- UPS 1500 VA / 900 W, ~30 min runtime (SOURCED spec, not in params). Usable energy 150 Wh (ASSUMED, ~2x12V9Ah at 80% DoD); cross-check: 150 Wh / 287 W ~ 31 min, consistent with the 30-min spec. Park+close+flush 2 min (ASSUMED) -> 16x shutdown margin, but the UPS holds only ~5% of a 10 h night: it is a graceful-shutdown store, not an overnight supply (matches power_manager: park at 50%, emergency close at 20%). +- Battery energy = 100 Ah x pack V x 80% usable. Pack voltage is UNSPECIFIED in the repo and DECISIVE: 12 V -> 1.2 kWh (~3.3 h autonomy, FAILS the night); 48 V -> 4.8 kWh (~13 h, passes). The headline verdict uses the lower, specified-by-default 12 V case — flag and resolve the pack voltage. +- Autonomy = usable Wh / night load; target = one 10 h winter imaging night (ASSUMED; astronomical dark is ~14 h in December, so 10 h is a generous target). SF = autonomy / night; pass at 1.0. At 12 V SF = 0.33 -> FAIL. +- Winter solar harvest = 400 W x 4.5 PSH x 0.75 derate = 1350 Wh/day (all ASSUMED), vs 2870 Wh/night -> harvest is only ~47% of a night's energy, so even a bigger battery drains over successive nights without grid. The 400 W array cannot sustain the DGX-dominated load off-grid; duty-cycling the DGX (run inference on demand, not idle all night) is the highest-leverage fix. +- DGX thermal: 170 W into a 22 m^3 enclosure (footprint x 2.4 m, ASSUMED) at 2 natural ACH (ASSUMED) gives dT = Q/(rho*cp*Vdot) ~ 14 K rise — enough to spoil local seeing and drive dew. Force ~10 ACH of ventilation, or locate the DGX OUTSIDE the optical enclosure. cp_air 1005 J/kg-K, rho 1.027 kg/m^3 at 1800 m (DERIVED). +- CONCLUSION (open question resolved): graceful shutdown is comfortably covered; full off-grid autonomy on the specified 12 V / 400 W system is NOT — the assumed 170 W DGX Spark is the reason. Specify the pack at 48 V and/or duty-cycle the DGX to make a winter night off-grid feasible. + +## 6. Provenance ledger + +Where each key input comes from — the honesty table the repo lacks. + +| Parameter | Provenance | Source / basis | +|---|:--:|---| +| `MN76.mass_kg` | **S** | NIGHTWATCH_Build_Package.md spec table | +| `MN78.mass_kg` | **S** | docs/INTES_MICRO_HISTORY.md ('selected') | +| `OTA.cg_from_saddle_m` | **D** | geometric mid-tube estimate | +| `IMAGING_TRAIN.mass_kg` | **S** | pos/agents/05_walton_musser.md load analysis | +| `RA_DRIVE` | **S** | Build_Package.md / Musser (CSF-32-100) | +| `DEC_DRIVE` | **S** | Build_Package.md / Musser (CSF-25-80) | +| `MOTOR.slew_rate_dps` | **S** | firmware/onstepx_config/Config.h | +| `ENC_MOTOR_AMT103` | **S** | Build_Package.md / Hedrick (8192 PPR x100) | +| `ENC_AXIS_AS5600` | **S** | Build_Package.md / Hedrick (12-bit on-axis) | +| `ENC_AXIS_RESA` | **A** | proposed on-axis ring to meet sub-arcsec | +| `RA_HOUSING.wall_m` | **S** | Hedrick min 8 mm (12 mm bold review) | +| `BRG_RA_6008` | **S** | Build_Package.md bearing table (deep-groove C ratings) | +| `COUNTERWEIGHTS` | **S** | Build_Package.md counterweight table | +| `PIER` | **S** | Build_Package.md concrete pier spec | +| `ENCLOSURE.roof_mass_kg` | **A** | roof geometry unspecified in repo | +| `SITE` | **S** | config.py / Config.h (central Nevada, 1800 m) | +| `SITE.air_density` | **D** | ISA at 1800 m | +| `ENV.wind_park_ms` | **S** | services/safety_monitor/monitor.py | +| `ENV.survival_wind_ms` | **A** | ASCE 7 basic wind speed, central NV | +| `ENV.ground_snow_load_Pa` | **A** | high-desert @ 6000 ft | +| `ENV.seismic_sds_g` | **A** | Walker Lane seismicity | +| `TRACKING_RMS_TARGET_ARCSEC` | **S** | Hedrick '< 1 arcsec RMS' | +| `DEFLECTION_TARGET_ARCSEC` | **S** | Hedrick '< 5 arcsec @ 25 kg' | +| `NATURAL_FREQ_TARGET_HZ` | **S** | Hedrick '> 10 Hz' | + +**S** = sourced (repo file) · **D** = derived (computed) · **A** = assumed (repo silent). + +## 7. Trade Study (Phase B) — Morphological Box + Weighted-Pugh Selection + +This section does not compute physics; it **selects** among design permutations by +scoring them against the verdicts the Phase-C proof modules actually computed, so +the configuration that goes into CAD is grounded rather than asserted. The scoring +matrix, the three CSVs below, and their invariants are a pure function encoded in +`design/mechanical/tradestudy/build_tradestudy.py` and locked by +`design/mechanical/tests/test_tradestudy.py` (10 tests, green). + +**Grounding — the Phase-C verdict ledger the scores are anchored to:** + +| Proof | Verdict | Governing number | +|---|---|---| +| torque | PASS | counterweight-FREE MN78: RA SF 2.5, DEC SF 2.6 vs rated | +| stiffness | **FAIL** | 43.3" vs 5" target — bearing compliance at 63.5/76.2 mm spans governs | +| dynamics | PASS | first mode 65 Hz vs 10 Hz (SF 6.5) | +| encoder | **FAIL** | baseline 5.02" RMS vs 1.0"; on-axis RESA ring 0.54" | +| balance | PASS | 12.5 kg balances 18 kg (fit 1.59); CW-free deletes 15.4 kg + 29% RA inertia | +| bearings | PASS + finding | load never governs; 60xx are deep-groove — use angular-contact 7008/7006 pairs | +| wind | **FAIL** | roof uplift SF 0.19 — hold-down anchors mandatory (SF 3.9 with 4x 2 klbf) | +| pier | PASS | governing SF 6.8, f_n 152 Hz | +| thermal | **FAIL** (passive) | 22 K swing walks focus ~10x DoF — temp-compensated focuser required | +| enclosure | PASS + finding | drive SF 2.1; snow case governs the roof, snow interlock required | +| power | **FAIL** (off-grid) | 12 V pack 3.3 h vs 10 h night; 48 V -> 13.4 h; DGX = 59% of load | + +**Scoring convention.** Every criterion is *higher-is-better* on a 1–5 scale (for +cost and sourcing, 5 = cheapest / lowest risk), so a weighted score stays in 1–5. +Default weights: **stiffness 0.22, precision 0.22, cost 0.15, buildability 0.15, +thermal 0.10, sourcing-risk 0.16**. Crucially, precision against the hard 1.0" +tracking target is a **pass/fail GATE**, not merely a weighted term: an option that +fails it is *disqualified* regardless of its weighted score. + +### 1. Morphological (Zwicky) matrix — the option space + +`morphological.csv`. Bold picks are marked **[BOLD]**. + +| Subsystem | Baseline | Alternatives | Bold | +|---|---|---|---| +| OTA | MN78 f/8 (14 kg) | MN76 f/6 (9 kg), MN86 8", ES-MN152 | APM-LZOS apo triplet | +| Mount topology | Counterweighted GEM | Fork + derotator | **Counterweight-FREE GEM** | +| Axis drive | NEMA17 + 27:1 + harmonic | Direct-to-harmonic | **Torque-motor direct drive** | +| Encoder | AMT103 + AS5600 | Hybrid (motor + on-axis abs.) | **On-axis high-res absolute ring** | +| Pier | Concrete Sonotube | Steel-concrete hybrid | **Isolated pier-in-pier** | +| Enclosure | Roll-off roof | Clamshell dome | **Roll-off + active thermal** | +| Frame | 6061-T6 CNC plates | Steel weldment | **Cast housings** | + +### 2. Weighted-Pugh scores (default weights) + +`pugh_scores.csv`. Winner of each subsystem in **bold**; the encoder baseline is +`DISQUALIFIED` by the tracking gate. + +| Subsystem | Option | stiff | prec | cost | build | therm | src | **Weighted** | +|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +| OTA | MN78 f/8 (baseline) | 2 | 4 | 3 | 3 | 4 | 2 | 2.94 | +| OTA | MN76 f/6 | 4 | 3 | 3 | 4 | 3 | 2 | 3.21 | +| OTA | MN86 8" | 1 | 4 | 2 | 2 | 3 | 1 | 2.16 | +| OTA | APM-LZOS **[BOLD]** | 2 | 5 | 1 | 3 | 3 | 2 | 2.76 | +| OTA | **ES-MN152** | 4 | 3 | 5 | 4 | 3 | 5 | **3.99** | +| Topology | Counterweighted GEM (baseline) | 3 | 3 | 3 | 4 | 3 | 4 | 3.31 | +| Topology | **Counterweight-FREE GEM [BOLD]** | 3 | 4 | 4 | 4 | 3 | 4 | **3.68** | +| Topology | Fork + derotator | 3 | 2 | 2 | 2 | 3 | 2 | 2.32 | +| Drive | **NEMA17 + 27:1 + harmonic (baseline)** | 3 | 2 | 4 | 4 | 3 | 4 | **3.24** | +| Drive | Direct-to-harmonic | 3 | 3 | 3 | 3 | 3 | 4 | 3.16 | +| Drive | Torque-motor DD **[BOLD]** | 3 | 5 | 1 | 2 | 2 | 2 | 2.73 | +| Encoder | AMT103 + AS5600 (baseline) | 3 | 1 | 5 | 4 | 3 | 5 | 3.33 `DISQ` | +| Encoder | On-axis RESA ring **[BOLD]** | 3 | 5 | 1 | 2 | 3 | 2 | 2.83 | +| Encoder | **Hybrid (motor + on-axis abs.)** | 3 | 4 | 2 | 3 | 3 | 3 | **3.07** | +| Pier | **Concrete Sonotube (baseline)** | 4 | 3 | 5 | 4 | 4 | 5 | **4.09** | +| Pier | Isolated pier-in-pier **[BOLD]** | 4 | 4 | 3 | 3 | 4 | 4 | 3.70 | +| Pier | Steel-concrete hybrid | 3 | 3 | 3 | 3 | 2 | 3 | 2.90 | +| Enclosure | **Roll-off roof (baseline)** | 3 | 3 | 5 | 5 | 3 | 5 | **3.92** | +| Enclosure | Clamshell dome | 3 | 3 | 2 | 2 | 2 | 2 | 2.44 | +| Enclosure | Roll-off + active thermal **[BOLD]** | 3 | 4 | 4 | 4 | 5 | 4 | 3.88 | +| Frame | **6061-T6 CNC plates (baseline)** | 3 | 3 | 4 | 4 | 4 | 4 | **3.56** | +| Frame | Steel weldment | 4 | 3 | 3 | 2 | 3 | 3 | 3.07 | +| Frame | Cast housings **[BOLD]** | 4 | 4 | 2 | 1 | 3 | 2 | 2.83 | + +Two results are worth pausing on. First, the **encoder baseline has the *highest* +raw weighted score in its subsystem (3.33) yet is not selectable** — it fails the +1.0" gate (encoder proof: 5.02" RMS). A naive weighted sum would have kept a design +that misses the headline requirement by 5x; the gate is what prevents that, and it +is the single most important structural feature of this study. Second, the **OTA +math prefers the light in-production ES-MN152 (3.99), and among the Intes pair MN76 +(3.21) out-scores the doc-'selected' MN78 (2.94)** — MN78 is the *worst* realistic +tube on the six mechanical axes, precisely because its 14 kg / 1.4 m tube is the +lever the stiffness proof FAILs on. + +### 3. Sensitivity — precision-heavy and cost-heavy re-weighting + +`sensitivity.csv`. Precision-heavy = {prec 0.40, stiff 0.20, cost 0.10, build 0.10, +therm 0.08, src 0.12}; cost-heavy = {cost 0.35, src 0.20, build 0.15, stiff 0.12, +prec 0.10, therm 0.08}. + +| Subsystem | Default winner | Precision-heavy | Cost-heavy | Robust? | +|---|---|---|---|---| +| OTA | ES-MN152 (3.99) | ES-MN152 (3.74) | ES-MN152 (4.37) | **stable** | +| Topology | CW-FREE GEM (3.68) | CW-FREE GEM (3.72) | CW-FREE GEM (3.80) | **stable** | +| Drive | NEMA17+harmonic (3.24) | **Torque-motor DD (3.30)** | NEMA17+harmonic (3.60) | flips (prec) | +| Encoder | Hybrid (3.07) | **RESA ring (3.38)** | Hybrid (2.75) | flips (prec) | +| Pier | Sonotube (4.09) | Sonotube (3.82) | Sonotube (4.45) | **stable** | +| Enclosure | Roll-off (3.92) | **Roll-off + thermal (3.88)** | Roll-off (4.40) | flips (prec) | +| Frame | 6061 CNC (3.56) | 6061 CNC (3.40) | 6061 CNC (3.56/3.78) | **stable** | + +The winner changes in exactly three subsystems, and **all three flips are in the +same direction**: weighting the sub-arcsec mission more heavily pulls the drive to a +zero-gear-PE torque motor, the encoder to the full on-axis ring, and the enclosure +to active thermal control — i.e. the bold options are the *precision* options, and +they win the moment precision dominates. Four subsystems (OTA, topology, pier, +frame) never move. + +### 4. Selected permutation + rationale + +The delivered build takes the default-weight winners, with two deliberate, +disclosed departures. It comprises **4 baselines, 2 bold picks, and 1 mandatory +bold-adjacent upgrade**: + +1. **OTA — MN78 f/8 (science override of the mechanical winner).** The mechanical + Pugh winner is ES-MN152 under every weighting, and MN76 beats MN78 among the + Intes pair. MN78 is retained for its f/8 imaging scale and 0.134 obstruction — + an optical/science requirement that lives *outside* the six mechanical criteria. + This retention is not free: it is exactly why the stiffness proof FAILs, and it + is the reason item 8 below is mandatory. If the bearing remediation proves + insufficient, **MN76 is the drop-in mechanical hedge** (shorter CG lever, lower + RA torque). +2. **Mount topology — Counterweight-FREE GEM [BOLD].** Robust winner under all three + weightings. Grounded: torque proof gives RA SF 2.5 / DEC SF 2.6 on the harmonic + drives' back-drive resistance, and the balance proof shows deleting the shaft + + weights removes **15.4 kg and 29% of the RA inertia** — a strict improvement in + what the drives and pier must carry, the same principle the ZWO AM5 / RST-135 + exploit. +3. **Axis drive — NEMA17 + 27:1 planetary + harmonic (baseline).** Wins default and + cost-heavy; torque proof PASS. The planetary's periodic error — the only reason + to consider deleting it — is **mooted by the on-axis encoder (item 4), which + corrects everything upstream of the axis**, so the cheap standard OnStepX + drivetrain is retained. (Under a precision-only view it flips to the torque + motor; that is a documented, deferred upgrade path.) +4. **Encoder — Hybrid: motor encoder (velocity) + on-axis absolute (position) + [mandatory on-axis upgrade].** The as-specified baseline is **DISQUALIFIED** — + encoder proof: 5.02" RMS, 5x over target, because the AS5600's ~91" quantisation + makes it homing-grade only and the servo must close on the motor encoder, which + cannot see harmonic PE or mount flexure. Some form of on-axis absolute feedback + is *required*, not optional (proof: on-axis ring reaches 0.54" RMS). The hybrid + wins among qualifying options at default/cost weights; the **full on-axis RESA + ring [BOLD]** is the selected upgrade when precision is weighted heavily and is + the only option that reaches sub-arcsec outright. +5. **Pier — Concrete Sonotube (baseline).** Wins under all weightings (4.09). The + pier proof already passes with governing SF 6.8 and f_n 152 Hz, so the bold + isolated pier-in-pier would be **gold-plating the one part that is not the + constraint** — the head/bearings are. Retained as baseline. +6. **Enclosure — Roll-off + active thermal [BOLD].** A statistical tie with the + plain roll-off at default weights (3.88 vs 3.92, within noise), broken toward the + bold variant by two proof findings the plain roof ignores: the **thermal FAIL** + (22 K diurnal swing walks focus ~10x depth-of-focus) and the **power finding** + that the DGX dumps ~14 K into the enclosure at 2 ACH ("ventilate or locate + outside"). Active ventilation + insulation + day pre-cooling is the direct remedy, + and it becomes the outright winner the instant precision is weighted up. +7. **Frame — 6061-T6 CNC plates (baseline).** Wins under all weightings (3.56). The + key insight from the stiffness proof: the **aluminium plates are *not* the + governing compliance — the bearings are** — so switching to a steel weldment or + cast housings spends stiffness budget in the wrong place. Retain the machinable, + DIY-friendly 6061 frame and fix the bearings instead (item 8). + +**Cross-cutting actions the trade study surfaces but cannot select away.** Three +Phase-C FAILs are not closable by any of the seven morphological axes and are +booked here as required detail-design actions: + +- **Stiffness (governing FAIL, 43.3" vs 5"):** fix at the *component* level, not the + configuration level — replace the deep-groove 6008/6006 with **matched + angular-contact 7008/7006 pairs (back-to-back) on larger spans** (bearings + + stiffness proofs). No frame or topology choice in the box moves this number. +- **Wind (FAIL, roof uplift SF 0.19):** hold-down anchors are **mandatory and + currently unspecified** — 4x 2 klbf anchors restore SF 3.9 (wind proof). Pair with + the snow interlock the enclosure proof requires. +- **Off-grid power (FAIL):** the selected build assumes grid + UPS (16x shutdown + margin). True autonomy needs a **48 V pack (13.4 h) or a DGX duty-cycle**, not the + specified 12 V / 100 Ah pack (3.3 h). Orthogonal to the enclosure/pier choices. + +**Net:** the selected permutation banks the two clearly-won bold improvements +(counterweight-free GEM, active-thermal enclosure) plus the mandatory on-axis +encoder, keeps the baseline where the baseline already passes (drive, pier, frame), +and honestly flags that the OTA choice, the stiffness fix, the wind anchors, and the +power autonomy are decisions the science mission and detail design must own — the +trade study cannot make them disappear. + +## 8. Mechanical CAD (Phase D) — parametric geometry + +The repository shipped **zero usable CAD**: every `.step` reference in the build package is marked *Pending*. Phase D replaces that void with **text-based, parametric geometry driven entirely by the computed design** in `design/mechanical/calc/params.py`. Nothing is hand-dimensioned that already exists as a parameter — change a number in `params.py` and the models (and the preview) move with it. + +### The immediate visual + +![NIGHTWATCH GEM meridian elevation](cad/preview_assembly.svg) + +`cad/preview_assembly.svg` is a to-scale meridian-plane side elevation of the mount in its park pose — pier, RA housing on the polar axis inclined at the **38.9° site latitude**, DEC head, MN78 OTA pointing at the pole, and the counterweight shaft hanging on the down side. It is produced by a **pure-stdlib** Python script (no OpenSCAD, no third-party libraries) that imports `params.py` and writes the SVG with the real numbers annotated. This is the deliverable you can look at right now; the OpenSCAD files below are the manufacturable source. + +### The parts + +| File | Part | Headline dimensions (all traced to `params.py`) | +|---|---|---| +| `cad/ra_housing.scad` | RA (polar) axis housing | 203.2 × 203.2 × 76.2 mm block (`RA_HOUSING` 8″×8″×3″), 8 mm wall; stepped axial bore — 6008 seat (Ø68/Ø40, `BRG_RA_6008`) opening to the CSF‑32 Ø80 hollow‑drive register (`RA_DRIVE.bore_m`); flange bolt circle ≈ Ø104 | +| `cad/dec_housing.scad` | DEC axis housing + **Losmandy‑D saddle** | 152.4 × 152.4 × 63.5 mm (`DEC_HOUSING` 6″×6″×2.5″); 6006 seat (Ø55/Ø30, `BRG_DEC_6006`) → CSF‑25 Ø64 register (`DEC_DRIVE.bore_m`); dovetail saddle 76.2 mm / 15° with side clamp | +| `cad/pier_adapter.scad` | Pier adapter plate | 254 × 254 × 19.05 mm (10″×10″×0.75″) bridging the SOURCED 304.8 × 9.525 mm (12″×0.375″) `PIER` top plate to the RA housing corner pattern | +| `cad/counterweight_shaft.scad` | Counterweight shaft + weights | Ø31.75 mm × 457.2 mm 303‑SS (`COUNTERWEIGHTS` 1.25″×18″), stud + safety knob, 5+5+2.5 kg discs (12.5 kg available) | +| `cad/assembly.scad` | Full GEM assembly | Composes all four parts + the MN78 OTA tube (Ø210 × 1400 mm) on the pier, posed at latitude `SITE.latitude_deg` | +| `cad/svg_preview.py` | Zero‑dep previewer | Imports `params`/`units`, emits `preview_assembly.svg` | + +### Parametric approach + +Each `.scad` opens with a **params block** whose every entry carries a `// = P.<...>` comment tracing it to `params.py` (SI metres in the calculator → millimetres in OpenSCAD, the CAD convention). Parts are pure `module`s; `assembly.scad` pulls them in with `use <...>` and places them with explicit transforms (`rotate([0, 90-lat, 0])` puts local *+z* on the celestial pole; a nested `rotate([90,0,0])` sets the DEC axis perpendicular to RA). The SVG previewer is the strictest link in the chain — it reads `params.py` **at runtime**, so it can never disagree with the proofs, and a test pins the committed SVG byte‑for‑byte to the renderer output. + +### How to render + +OpenSCAD is **not installed** in the design sandbox, so each part header documents the command to run locally: + +``` +openscad -o ra_housing.stl design/mechanical/cad/ra_housing.scad +openscad -o assembly.stl design/mechanical/cad/assembly.scad +openscad -o assembly.png --imgsize=1600,1200 design/mechanical/cad/assembly.scad +``` + +The SVG needs nothing but Python: `python3 design/mechanical/cad/svg_preview.py` regenerates `preview_assembly.svg` (re‑run it whenever `params.py` changes). + +### Honest assumptions (tagged ASSUMED/DERIVED in‑file and in `svg_preview`) + +- **Pier adapter (10×10×0.75″)** is an *added* part — the repo's `PIER` only specifies a 12×12×0.375″ top plate, so the adapter geometry is **ASSUMED**. +- **Losmandy‑D dovetail** 76.2 mm / 15° is the **ASSUMED** industry standard; the repo never dimensions the saddle. +- **Bearing widths** (6008 = 15 mm, 6006 = 13 mm) and **counterweight disc OD/thickness** are **ASSUMED** — `params` gives bore/OD and weight mass only. +- **CSF flange bolt circle** is taken as **DERIVED** ≈ 1.3 × drive bore (`RA_DRIVE.bore_m` / `DEC_DRIVE.bore_m`), since datasheet PCDs are not in the repo. + +### Verification + +`design/mechanical/tests/test_cad.py` — **13 tests, all green** (and the full mechanical suite stays at 118 passed): the polar‑axis vector is a unit vector at the site latitude with the perpendiculars orthogonal to it; drawn dimensions equal `params.py` exactly; swapping MN76↔MN78 changes the geometry (proof it is parametric); the head sits above the pier top, the OTA clears the DEC head toward the pole, and the counterweight hangs on the opposite side of the inclined axis; the SVG is valid, dimensioned with the real numbers, and actually writes; and every `.scad` carries its render command and `params.py` traceability. + +## 9. Selected configuration, cost delta, and roadmap + +The trade study's selected permutation, with the cost delta against the repo's **$8,305** BOM +(DGX excluded) and a build sequencing that ships a working v1 before the sub-arcsec v2. + +| Subsystem | Selected | Why | Cost delta | +|---|---|---|---| +| OTA | **MN78 f/8** (science override) | f/8 scale + 0.134 obstruction for Mars/lucky imaging; MN76 is the mechanical hedge | baseline | +| Topology | **Counterweight-FREE GEM** (bold) | torque proof SF 2.5/2.6; deletes 15.4 kg + 29% RA inertia | −$110 (no CW set) | +| Drive | NEMA17 + 27:1 + harmonic | PASS; planetary PE mooted by on-axis encoder | baseline | +| Encoder | **Hybrid → on-axis absolute** (mandatory) | baseline DISQUALIFIED (5.02″ RMS); on-axis ring 0.54″ | +$1,500–5,000 | +| Bearings | **Angular-contact 7008/7006** (fix) | deep-groove 6008/6006 are 98% of the stiffness FAIL | +$80 | +| Pier | Concrete Sonotube | PASS SF 6.8; not the constraint | baseline | +| Enclosure | **Roll-off + active thermal** (bold) | closes thermal FAIL + DGX heat | +$300 | +| Anchors | **4× 2 klbf hold-downs** (fix) | survival-wind uplift SF 0.19 → 3.9 | +$120 | +| Power | Grid + UPS (v1); **48 V pack** for autonomy | 12 V pack gives 3.3 h vs 10 h night | +$400 (v2) | + +**Roadmap.** +- **v1 (buildable now):** MN78 counterweight-free GEM on the concrete pier, roll-off roof with the + mandatory hold-down anchors and snow interlock, angular-contact bearings, grid + UPS power, + temperature-compensated focus. Meets every structural and thermal requirement; tracks to ~5″ + (motor-encoder + PE), which is fine for the wide-field/lucky-imaging use with plate-solve recentering. +- **v2 (sub-arcsecond):** add the on-axis absolute ring (closes the 1″ tracking target), active-thermal + enclosure, and — if precision is weighted above cost — the torque-motor direct drive. Off-grid + autonomy via the 48 V battery + duty-cycled DGX. + +## 10. Risk register + +| Risk | Severity | Mitigation (computed) | +|---|---|---| +| Frame stiffness FAIL (43″) | High | Angular-contact 7008/7006 back-to-back on max spans; verify with the stiffness proof once bearing stiffness is vendor-confirmed | +| Sub-arcsec unmet by baseline encoder | High | On-axis absolute ring (proof: 0.54″); v1 relies on plate-solve recentering | +| Survival-wind roof uplift (SF 0.19) | High | 4× 2 klbf hold-down anchors (SF 3.9) + snow interlock | +| Passive focus drift over diurnal swing | Medium | Temperature-compensated focuser (repo already has −2.5 steps/°C) | +| Off-grid autonomy shortfall | Medium | 48 V pack (13.4 h) or DGX duty-cycle; v1 stays on grid + UPS | +| Bearing-stiffness assumption (250/180 N/µm) | Medium | Confirm vendor radial stiffness; it scales the deflection linearly | +| DGX watts/heat unspecified (assumed 170 W) | Low | Ventilate or locate the DGX outside the OTA enclosure | + +--- + +_Generated by `design/mechanical/calc/report.py` from the proof modules. Regenerate with +`python3 -m design.mechanical.calc.report`; `test_report.py` guards against drift._ diff --git a/design/mechanical/cad/SECTION.md b/design/mechanical/cad/SECTION.md new file mode 100644 index 0000000..8c505ae --- /dev/null +++ b/design/mechanical/cad/SECTION.md @@ -0,0 +1,47 @@ +## Mechanical CAD (Phase D) — parametric geometry + +The repository shipped **zero usable CAD**: every `.step` reference in the build package is marked *Pending*. Phase D replaces that void with **text-based, parametric geometry driven entirely by the computed design** in `design/mechanical/calc/params.py`. Nothing is hand-dimensioned that already exists as a parameter — change a number in `params.py` and the models (and the preview) move with it. + +### The immediate visual + +![NIGHTWATCH GEM meridian elevation](cad/preview_assembly.svg) + +`cad/preview_assembly.svg` is a to-scale meridian-plane side elevation of the mount in its park pose — pier, RA housing on the polar axis inclined at the **38.9° site latitude**, DEC head, MN78 OTA pointing at the pole, and the counterweight shaft hanging on the down side. It is produced by a **pure-stdlib** Python script (no OpenSCAD, no third-party libraries) that imports `params.py` and writes the SVG with the real numbers annotated. This is the deliverable you can look at right now; the OpenSCAD files below are the manufacturable source. + +### The parts + +| File | Part | Headline dimensions (all traced to `params.py`) | +|---|---|---| +| `cad/ra_housing.scad` | RA (polar) axis housing | 203.2 × 203.2 × 76.2 mm block (`RA_HOUSING` 8″×8″×3″), 8 mm wall; stepped axial bore — 6008 seat (Ø68/Ø40, `BRG_RA_6008`) opening to the CSF‑32 Ø80 hollow‑drive register (`RA_DRIVE.bore_m`); flange bolt circle ≈ Ø104 | +| `cad/dec_housing.scad` | DEC axis housing + **Losmandy‑D saddle** | 152.4 × 152.4 × 63.5 mm (`DEC_HOUSING` 6″×6″×2.5″); 6006 seat (Ø55/Ø30, `BRG_DEC_6006`) → CSF‑25 Ø64 register (`DEC_DRIVE.bore_m`); dovetail saddle 76.2 mm / 15° with side clamp | +| `cad/pier_adapter.scad` | Pier adapter plate | 254 × 254 × 19.05 mm (10″×10″×0.75″) bridging the SOURCED 304.8 × 9.525 mm (12″×0.375″) `PIER` top plate to the RA housing corner pattern | +| `cad/counterweight_shaft.scad` | Counterweight shaft + weights | Ø31.75 mm × 457.2 mm 303‑SS (`COUNTERWEIGHTS` 1.25″×18″), stud + safety knob, 5+5+2.5 kg discs (12.5 kg available) | +| `cad/assembly.scad` | Full GEM assembly | Composes all four parts + the MN78 OTA tube (Ø210 × 1400 mm) on the pier, posed at latitude `SITE.latitude_deg` | +| `cad/svg_preview.py` | Zero‑dep previewer | Imports `params`/`units`, emits `preview_assembly.svg` | + +### Parametric approach + +Each `.scad` opens with a **params block** whose every entry carries a `// = P.<...>` comment tracing it to `params.py` (SI metres in the calculator → millimetres in OpenSCAD, the CAD convention). Parts are pure `module`s; `assembly.scad` pulls them in with `use <...>` and places them with explicit transforms (`rotate([0, 90-lat, 0])` puts local *+z* on the celestial pole; a nested `rotate([90,0,0])` sets the DEC axis perpendicular to RA). The SVG previewer is the strictest link in the chain — it reads `params.py` **at runtime**, so it can never disagree with the proofs, and a test pins the committed SVG byte‑for‑byte to the renderer output. + +### How to render + +OpenSCAD is **not installed** in the design sandbox, so each part header documents the command to run locally: + +``` +openscad -o ra_housing.stl design/mechanical/cad/ra_housing.scad +openscad -o assembly.stl design/mechanical/cad/assembly.scad +openscad -o assembly.png --imgsize=1600,1200 design/mechanical/cad/assembly.scad +``` + +The SVG needs nothing but Python: `python3 design/mechanical/cad/svg_preview.py` regenerates `preview_assembly.svg` (re‑run it whenever `params.py` changes). + +### Honest assumptions (tagged ASSUMED/DERIVED in‑file and in `svg_preview`) + +- **Pier adapter (10×10×0.75″)** is an *added* part — the repo's `PIER` only specifies a 12×12×0.375″ top plate, so the adapter geometry is **ASSUMED**. +- **Losmandy‑D dovetail** 76.2 mm / 15° is the **ASSUMED** industry standard; the repo never dimensions the saddle. +- **Bearing widths** (6008 = 15 mm, 6006 = 13 mm) and **counterweight disc OD/thickness** are **ASSUMED** — `params` gives bore/OD and weight mass only. +- **CSF flange bolt circle** is taken as **DERIVED** ≈ 1.3 × drive bore (`RA_DRIVE.bore_m` / `DEC_DRIVE.bore_m`), since datasheet PCDs are not in the repo. + +### Verification + +`design/mechanical/tests/test_cad.py` — **13 tests, all green** (and the full mechanical suite stays at 118 passed): the polar‑axis vector is a unit vector at the site latitude with the perpendiculars orthogonal to it; drawn dimensions equal `params.py` exactly; swapping MN76↔MN78 changes the geometry (proof it is parametric); the head sits above the pier top, the OTA clears the DEC head toward the pole, and the counterweight hangs on the opposite side of the inclined axis; the SVG is valid, dimensioned with the real numbers, and actually writes; and every `.scad` carries its render command and `params.py` traceability. \ No newline at end of file diff --git a/design/mechanical/cad/assembly.scad b/design/mechanical/cad/assembly.scad new file mode 100644 index 0000000..f557d91 --- /dev/null +++ b/design/mechanical/cad/assembly.scad @@ -0,0 +1,83 @@ +// ============================================================================ +// NIGHTWATCH GEM — full mount assembly +// Composes the parametric parts into a German Equatorial Mount on its pier, +// posed in the classic "counterweight-down / OTA-to-pole" park. +// Parametric OpenSCAD model. Units: MILLIMETRES. +// +// Render (OpenSCAD not installed here): +// openscad -o assembly.stl assembly.scad +// openscad -o assembly.png --imgsize=1600,1200 assembly.scad +// +// The immediate, dependency-free visual is design/mechanical/cad/preview_assembly.svg +// (generated by svg_preview.py, which reads the SAME params.py numbers). +// +// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm). +// ============================================================================ + +use +use +use +use + +$fn = 64; + +// ---- parameters (traced to params.py) -------------------------------------- +lat = 38.9; // P.SITE.latitude_deg -> polar-axis altitude +pier_h = 914.4; // P.PIER.height_above_m = u.inch(36) +pier_d = 304.8; // P.PIER.diameter_m = u.inch(12) +tp_side = 304.8; // P.PIER.top_plate_side_m = u.inch(12) +tp_thk = 9.525; // P.PIER.top_plate_thk_m = u.inch(0.375) +adapter_thk = 19.05; // pier_adapter.scad (0.75 in), ASSUMED added part + +ra_depth = 76.2; // P.RA_HOUSING.depth_m = u.inch(3) +dec_face = 152.4; // P.DEC_HOUSING.outer_x_m = u.inch(6) +dec_depth = 63.5; // P.DEC_HOUSING.depth_m = u.inch(2.5) + +// Default OTA = MN78 (the params default across the torque/balance/pier proofs). +ota_od = 210.0; // P.MN78.tube_od_m = u.mm(210) +ota_len = 1400.0; // P.MN78.tube_length_m = u.mm(1400) +saddle_stack = 50.0; // stiffness.SADDLE_LEVER_M (assumed saddle offset) + +// Derived placements +adapter_top = pier_h + tp_thk + adapter_thk; +L_ra = ra_depth + dec_face/2 + 40; // DEC-head seat along the polar axis (DERIVED) +ota_offset = dec_face/2 + saddle_stack + ota_od/2; // saddle-side radial offset + +// ---- assembly -------------------------------------------------------------- +module gem_assembly() { + // Concrete pier (exposed height only) + color([0.62, 0.62, 0.60]) cylinder(d = pier_d, h = pier_h); + + // Steel top plate (SOURCED, P.PIER) + color([0.30, 0.30, 0.32]) + translate([-tp_side/2, -tp_side/2, pier_h]) cube([tp_side, tp_side, tp_thk]); + + // Pier adapter plate (10x10x0.75 in, ASSUMED added part) + color([0.80, 0.67, 0.24]) + translate([0, 0, pier_h + tp_thk]) pier_adapter(); + + // ---- mount head, mounted on the polar (RA) axis ---- + translate([0, 0, adapter_top]) + rotate([0, 90 - lat, 0]) { // local +z -> celestial pole (alt = lat) + + color([0.75, 0.78, 0.80]) ra_housing(); + + // DEC head at the far end of the RA axis; DEC axis _|_ RA axis + translate([0, 0, L_ra]) { + + rotate([90, 0, 0]) color([0.70, 0.73, 0.76]) dec_housing(); + + // Counterweight shaft on the "down" side (classical GEM option; + // balance.py recommends deleting it — drawn for completeness). + translate([0, dec_face/2, 0]) + rotate([-90, 0, 0]) color([0.20, 0.20, 0.22]) counterweight_shaft(); + + // OTA tube, parallel to the polar axis, on the saddle side. + // Saddle sits ~1/3 up the tube (matches OTA.cg_from_saddle model). + translate([0, -ota_offset, -ota_len/3]) + color([0.10, 0.10, 0.12]) cylinder(d = ota_od, h = ota_len); + } + } +} + +gem_assembly(); diff --git a/design/mechanical/cad/counterweight_shaft.scad b/design/mechanical/cad/counterweight_shaft.scad new file mode 100644 index 0000000..058da3d --- /dev/null +++ b/design/mechanical/cad/counterweight_shaft.scad @@ -0,0 +1,56 @@ +// ============================================================================ +// NIGHTWATCH GEM — counterweight shaft + weights +// Parametric OpenSCAD model. Units: MILLIMETRES. +// +// Render (OpenSCAD not installed here): +// openscad -o counterweight_shaft.stl counterweight_shaft.scad +// openscad -o counterweight_shaft.png --imgsize=1200,900 counterweight_shaft.scad +// +// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm). +// NOTE: the counterweight-FREE study (balance.py) recommends DELETING this shaft; +// it is modelled so the classical GEM option remains fully drawable. +// ============================================================================ + +$fn = 96; + +// ---- parameters (traced to params.py) -------------------------------------- +shaft_d = 31.75; // P.COUNTERWEIGHTS.shaft_dia_m = u.inch(1.25) -> 0.03175 m (303-SS) +shaft_l = 457.2; // P.COUNTERWEIGHTS.shaft_len_m = u.inch(18.0) -> 0.4572 m + +// Weights available = 12.5 kg (2 x 5 kg + 1 x 2.5 kg) = P.COUNTERWEIGHTS.weights_available_kg +// Cast-iron disc geometry is ASSUMED (params gives mass only, not OD/thickness). +disc_bore = 33.0; // slip fit over the shaft — ASSUMED +disc5_od = 127.0; // 5.0 kg disc OD — ASSUMED +disc5_t = 40.0; // 5.0 kg disc thickness — ASSUMED +disc25_od = 102.0; // 2.5 kg disc OD — ASSUMED +disc25_t = 33.0; // 2.5 kg disc thickness — ASSUMED + +// End fittings — ASSUMED. +stud_d = 20.0; stud_l = 26.0; // threaded stud into the DEC block +knob_d = 46.0; knob_h = 16.0; // safety stop knob at the far end + +module _disc(od, t) { + difference() { + cylinder(d = od, h = t); + translate([0, 0, -1]) cylinder(d = disc_bore, h = t + 2); + // knurl-substitute relief so the disc reads as a weight, not a plain ring + translate([0, 0, t/2]) rotate_extrude() translate([od/2 - 3, 0, 0]) square([3, t], center=true); + } +} + +module counterweight_shaft() { + // threaded stud (mount end) at z<0 + translate([0, 0, -stud_l]) cylinder(d = stud_d, h = stud_l); + // main 303-SS shaft + cylinder(d = shaft_d, h = shaft_l); + // safety stop knob at the far (top) end + translate([0, 0, shaft_l]) cylinder(d = knob_d, h = knob_h); + + // Stacked weights near the far end (parked position): 5 + 5 + 2.5 kg. + z0 = shaft_l - 40; + translate([0, 0, z0 - disc5_t]) _disc(disc5_od, disc5_t); + translate([0, 0, z0 - disc5_t - disc5_t]) _disc(disc5_od, disc5_t); + translate([0, 0, z0 - disc5_t - disc5_t - disc25_t]) _disc(disc25_od, disc25_t); +} + +counterweight_shaft(); // standalone; ignored by `use <>` in assembly diff --git a/design/mechanical/cad/dec_housing.scad b/design/mechanical/cad/dec_housing.scad new file mode 100644 index 0000000..b2ed942 --- /dev/null +++ b/design/mechanical/cad/dec_housing.scad @@ -0,0 +1,90 @@ +// ============================================================================ +// NIGHTWATCH GEM — DEC axis housing + Losmandy-D saddle +// Parametric OpenSCAD model. Units: MILLIMETRES. +// +// Render (OpenSCAD not installed here): +// openscad -o dec_housing.stl dec_housing.scad +// openscad -o dec_housing.png --imgsize=1200,900 dec_housing.scad +// +// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm). +// ============================================================================ + +$fn = 96; + +// ---- parameters (traced to params.py) -------------------------------------- +dec_outer_x = 152.4; // P.DEC_HOUSING.outer_x_m = u.inch(6) -> 0.1524 m +dec_outer_y = 152.4; // P.DEC_HOUSING.outer_y_m = u.inch(6) +dec_depth = 63.5; // P.DEC_HOUSING.depth_m = u.inch(2.5) -> bearing separation +dec_wall = 8.0; // P.DEC_HOUSING.wall_m = u.mm(8) + +brg_od = 55.0; // P.BRG_DEC_6006.od_m = u.mm(55) (6006 output bearing) +brg_bore = 30.0; // P.BRG_DEC_6006.bore_m = u.mm(30) +brg_w = 13.0; // 6006 nominal width — ASSUMED (not in params.py) + +drive_bore = 64.0; // P.DEC_DRIVE.bore_m = u.mm(64) (CSF-25-80 hollow bore) +flange_bc = 83.0; // ~ P.DEC_DRIVE.bore_m ; CSF-25 flange PCD (DERIVED ~1.3*bore) +flange_nbolt = 8; // ASSUMED +flange_bolt_d = 4.5; // M4 clearance — ASSUMED + +// Losmandy-D dovetail (industry standard) — ASSUMED (repo does not dimension it). +losmandy_plate_w = 76.2; // 3.00 in nominal Losmandy-D plate width — ASSUMED std +dovetail_angle = 15.0; // deg, standard dovetail flank — ASSUMED std +saddle_height = 24.0; // jaw block height — ASSUMED +saddle_slot_depth = 14.0; // dovetail engagement depth — ASSUMED +clamp_bolt_d = 8.0; // side clamp knob thread — ASSUMED + +// ---- dovetail channel (trapezoid: wide at base, narrow mouth) --------------- +module _dovetail_channel(chlen, plate_w, ang, depth) { + mouth = plate_w - 2 * depth * tan(ang); // narrower opening at the top + translate([0, chlen / 2, 0]) + rotate([90, 0, 0]) // extrude +Z -> run along -Y + linear_extrude(height = chlen) + polygon(points = [ + [-plate_w / 2, 0], // base (inside), wide + [ plate_w / 2, 0], + [ mouth / 2, depth], // mouth (top), narrow -> jaws grip + [-mouth / 2, depth] + ]); +} + +// ---- DEC bearing/drive housing --------------------------------------------- +module dec_core() { + difference() { + translate([-dec_outer_x/2, -dec_outer_y/2, 0]) + cube([dec_outer_x, dec_outer_y, dec_depth]); + + // Stepped axial bore along +z (the DEC axis): + translate([0, 0, -1]) cylinder(d = brg_od, h = brg_w + 1); + translate([0, 0, brg_w]) cylinder(d = drive_bore, h = dec_depth - brg_w + 1); + translate([0, 0, -1]) cylinder(d = brg_bore - 6, h = dec_depth + 2); + + // CSF-25 flange bolt circle from the rear face. + for (i = [0 : flange_nbolt - 1]) + rotate([0, 0, i * 360 / flange_nbolt]) + translate([flange_bc / 2, 0, dec_depth - 14]) + cylinder(d = flange_bolt_d, h = 18); + } +} + +// ---- Losmandy-D saddle jaw block (sits on the +Y face of the DEC output) ----- +module losmandy_saddle() { + blk_w = losmandy_plate_w + 44; // jaw width straddling the plate — ASSUMED + blk_l = dec_outer_y; // saddle length = DEC housing face + difference() { + translate([-blk_w/2, -blk_l/2, 0]) cube([blk_w, blk_l, saddle_height]); + // dovetail channel, mouth flush with the top surface + translate([0, 0, saddle_height - saddle_slot_depth]) + _dovetail_channel(blk_l + 2, losmandy_plate_w, dovetail_angle, saddle_slot_depth + 2); + // side clamp knob cross-hole + translate([blk_w/2 - 8, 0, saddle_height/2]) + rotate([0, 90, 0]) cylinder(d = clamp_bolt_d, h = 24, center = true); + } +} + +module dec_housing() { + dec_core(); + // Mount the saddle on the DEC output face (+z), lifted clear of the bore boss. + translate([0, 0, dec_depth]) losmandy_saddle(); +} + +dec_housing(); // executed standalone; ignored by `use <>` in assembly diff --git a/design/mechanical/cad/pier_adapter.scad b/design/mechanical/cad/pier_adapter.scad new file mode 100644 index 0000000..95189d1 --- /dev/null +++ b/design/mechanical/cad/pier_adapter.scad @@ -0,0 +1,59 @@ +// ============================================================================ +// NIGHTWATCH GEM — pier adapter plate +// Bridges the concrete pier's steel top plate (12" x 0.375") to the RA housing. +// Parametric OpenSCAD model. Units: MILLIMETRES. +// +// Render (OpenSCAD not installed here): +// openscad -o pier_adapter.stl pier_adapter.scad +// openscad -o pier_adapter.png --imgsize=1200,900 pier_adapter.scad +// +// Dimensions trace to design/mechanical/calc/params.py (SI metres -> mm). +// ============================================================================ + +$fn = 96; + +// ---- parameters ------------------------------------------------------------ +// The adapter plate itself is 10" x 10" x 0.75" — an ADDED part (repo is silent +// on an adapter; tagged ASSUMED). It seats on the SOURCED pier top plate below. +adapter_side = 254.0; // 10 in = u.inch(10) -> ASSUMED (adapter geometry added) +adapter_thk = 19.05; // 0.75 in = u.inch(0.75) -> ASSUMED + +// Below: the pier's steel top plate (SOURCED, params.py P.PIER). +pier_plate_side = 304.8; // P.PIER.top_plate_side_m = u.inch(12) +pier_plate_thk = 9.525; // P.PIER.top_plate_thk_m = u.inch(0.375) (reference) +pier_dia = 304.8; // P.PIER.diameter_m = u.inch(12) (cable-hole guide) + +// Above: the RA housing corner bolt pattern it must accept. +ra_outer = 203.2; // P.RA_HOUSING.outer_x_m = u.inch(8) +ra_corner_inset = 18.0; // must match ra_housing.scad corner_inset — DERIVED + +// Fastener geometry — ASSUMED. +down_bolt_d = 13.5; // M12 anchor pattern into the pier top plate +down_bc = 228.6; // bolt circle to the pier plate (9.0 in square) — ASSUMED +up_bolt_d = 7.0; // M6 tapped-clearance for the RA housing corners +cable_bore = 60.0; // central cable / drawbar pass-through — ASSUMED + +module pier_adapter() { + ra_bc = ra_outer - 2 * ra_corner_inset; // RA housing corner square (mm) + difference() { + // 10" square plate with lightly chamfered footprint (via minkowski-free + // bevel: a plain square keeps the model unambiguous). + translate([-adapter_side/2, -adapter_side/2, 0]) + cube([adapter_side, adapter_side, adapter_thk]); + + // central cable / drawbar bore + translate([0, 0, -1]) cylinder(d = cable_bore, h = adapter_thk + 2); + + // DOWN pattern: 4 bolts on a square to the pier top plate + for (sx = [-1, 1], sy = [-1, 1]) + translate([sx * down_bc/2, sy * down_bc/2, -1]) + cylinder(d = down_bolt_d, h = adapter_thk + 2); + + // UP pattern: 4 tapped holes matching the RA housing corner bolts + for (sx = [-1, 1], sy = [-1, 1]) + translate([sx * ra_bc/2, sy * ra_bc/2, -1]) + cylinder(d = up_bolt_d, h = adapter_thk + 2); + } +} + +pier_adapter(); // standalone; ignored by `use <>` in assembly diff --git a/design/mechanical/cad/preview_assembly.svg b/design/mechanical/cad/preview_assembly.svg new file mode 100644 index 0000000..940c70d --- /dev/null +++ b/design/mechanical/cad/preview_assembly.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +pier 0.914 m (36") +Ø 0.305 m +OTA Intes-Micro MN78: L 1.400 m +CW shaft 0.457 m (18") +polar axis @ lat 38.9° +NIGHTWATCH GEM — meridian elevation (park pose) +OTA Intes-Micro MN78 (f/8) · tube Ø 0.210 m · mass 14 kg + 4 kg train · drawn to scale from params.py + +concrete pier 12" Ø + +adapter 10x10x0.75" + +RA housing (CSF-32, 6008) + +DEC housing + Losmandy-D (CSF-25, 6006) + +OTA tube + +counterweight shaft + \ No newline at end of file diff --git a/design/mechanical/cad/ra_housing.scad b/design/mechanical/cad/ra_housing.scad new file mode 100644 index 0000000..cce12b5 --- /dev/null +++ b/design/mechanical/cad/ra_housing.scad @@ -0,0 +1,66 @@ +// ============================================================================ +// NIGHTWATCH GEM — RA (polar) axis housing +// Parametric OpenSCAD model. Units: MILLIMETRES (OpenSCAD convention). +// +// Render (OpenSCAD is NOT installed in this design sandbox; this is text CAD): +// openscad -o ra_housing.stl ra_housing.scad +// openscad -o ra_housing.png --imgsize=1200,900 ra_housing.scad +// +// Every dimension traces to design/mechanical/calc/params.py so the geometry +// cannot silently drift from the computed design. Edit params.py, then mirror +// the value here (and in the sibling parts). SI metres in params -> mm here. +// ============================================================================ + +$fn = 96; + +// ---- parameters (traced to params.py) -------------------------------------- +ra_outer_x = 203.2; // P.RA_HOUSING.outer_x_m = u.inch(8) -> 0.2032 m +ra_outer_y = 203.2; // P.RA_HOUSING.outer_y_m = u.inch(8) -> 0.2032 m +ra_depth = 76.2; // P.RA_HOUSING.depth_m = u.inch(3) -> bearing separation +ra_wall = 8.0; // P.RA_HOUSING.wall_m = u.mm(8) (Hedrick min; 12 mm bold) + +brg_od = 68.0; // P.BRG_RA_6008.od_m = u.mm(68) (6008 output bearing) +brg_bore = 40.0; // P.BRG_RA_6008.bore_m = u.mm(40) +brg_w = 15.0; // 6008 nominal width — ASSUMED (not dimensioned in params.py) + +drive_bore = 80.0; // P.RA_DRIVE.bore_m = u.mm(80) (CSF-32-100 hollow bore) +// CSF-32 output flange mounting bolt circle scales with the hollow bore. +// DERIVED: ~1.3 x bore keeps the ring clear of the bore wall. Traced ~ P.RA_DRIVE.bore_m. +flange_bc = 104.0; // ~ P.RA_DRIVE.bore_m ; CSF-32 flange PCD (DERIVED) +flange_nbolt = 8; // ASSUMED (CSF-32 flange hole count) +flange_bolt_d = 4.5; // M4 clearance — ASSUMED + +corner_bolt_d = 6.6; // M6 clearance: housing -> RA stage / pier adapter — ASSUMED +corner_inset = 18.0; // ASSUMED + +// ---- part ------------------------------------------------------------------ +module ra_housing() { + difference() { + // Solid 6061-T6 block; production part would be pocket-lightened, kept + // solid here so the parametric proof stays unambiguous. + translate([-ra_outer_x/2, -ra_outer_y/2, 0]) + cube([ra_outer_x, ra_outer_y, ra_depth]); + + // Stepped axial bore along +z (the polar / RA axis): + // front (z=0) : 6008 bearing seat (OD 68) + // rear (z=brg_w+) : opens to the CSF-32 hollow-drive register (80) + translate([0, 0, -1]) cylinder(d = brg_od, h = brg_w + 1); + translate([0, 0, brg_w]) cylinder(d = drive_bore, h = ra_depth - brg_w + 1); + // clear through-hole for drawbar / cable routing + translate([0, 0, -1]) cylinder(d = brg_bore - 6, h = ra_depth + 2); + + // CSF-32 drive flange bolt circle, counter-drilled from the rear face. + for (i = [0 : flange_nbolt - 1]) + rotate([0, 0, i * 360 / flange_nbolt]) + translate([flange_bc / 2, 0, ra_depth - 16]) + cylinder(d = flange_bolt_d, h = 20); + + // Four corner through-holes (housing -> adapter / RA stage). + for (sx = [-1, 1], sy = [-1, 1]) + translate([sx * (ra_outer_x/2 - corner_inset), + sy * (ra_outer_y/2 - corner_inset), -1]) + cylinder(d = corner_bolt_d, h = ra_depth + 2); + } +} + +ra_housing(); // executed when opened directly; ignored by `use <>` in assembly diff --git a/design/mechanical/cad/svg_preview.py b/design/mechanical/cad/svg_preview.py new file mode 100644 index 0000000..75b36a2 --- /dev/null +++ b/design/mechanical/cad/svg_preview.py @@ -0,0 +1,401 @@ +""" +Zero-dependency SVG side-elevation of the NIGHTWATCH German Equatorial Mount. + +This is the *immediate* visual for the CAD package: OpenSCAD is not installed in +the design sandbox, so rather than ship un-renderable ``.step`` stubs (the repo's +current state — every CAD file is "Pending"), this module draws a correctly- +proportioned meridian-plane elevation straight from the computed design. + +Every dimension is read live from ``design.mechanical.calc.params`` — nothing is +hardcoded that already lives in params.py — so the drawing cannot drift from the +proofs. The pose is the classic GEM park: polar (RA) axis inclined at the site +latitude, OTA pointing at the celestial pole, counterweight shaft hanging on the +"down" side. + +Pure standard-library string templating; run it to (re)write ``preview_assembly.svg``: + + python3 design/mechanical/cad/svg_preview.py +""" + +from __future__ import annotations + +import math +import sys +from pathlib import Path + +# Allow ``python3 design/mechanical/cad/svg_preview.py`` from anywhere: put the +# repo root (…/NIGHTWATCH) on sys.path so the calc package imports cleanly. +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from design.mechanical.calc import params as P # noqa: E402 +from design.mechanical.calc import units as u # noqa: E402 + +DEFAULT_OUT = Path(__file__).resolve().parent / "preview_assembly.svg" + +# -------------------------------------------------------------------------- +# 2-D vector helpers (metres, maths frame: +x = north/right, +y = up). +# -------------------------------------------------------------------------- +Vec = tuple[float, float] + + +def _add(a: Vec, b: Vec) -> Vec: + return (a[0] + b[0], a[1] + b[1]) + + +def _mul(a: Vec, s: float) -> Vec: + return (a[0] * s, a[1] * s) + + +def polar_axis_unit(lat_deg: float) -> Vec: + """Unit vector of the polar (RA) axis: altitude = latitude, toward +x.""" + r = math.radians(lat_deg) + return (math.cos(r), math.sin(r)) + + +def _perp_up(lat_deg: float) -> Vec: + """Unit vector _|_ to the polar axis, on the OTA (up) side.""" + r = math.radians(lat_deg) + return (-math.sin(r), math.cos(r)) + + +def _perp_down(lat_deg: float) -> Vec: + """Unit vector _|_ to the polar axis, on the counterweight (down) side.""" + r = math.radians(lat_deg) + return (math.sin(r), -math.cos(r)) + + +def _oriented_rect(center: Vec, along: Vec, cross: Vec, length: float, width: float) -> list[Vec]: + """Four corners of a rectangle centred at ``center`` with the given axes.""" + a = _mul(along, length / 2.0) + c = _mul(cross, width / 2.0) + return [ + _add(_add(center, a), c), + _add(_add(center, a), _mul(c, -1)), + _add(_add(center, _mul(a, -1)), _mul(c, -1)), + _add(_add(center, _mul(a, -1)), c), + ] + + +# -------------------------------------------------------------------------- +# Geometry model — all lengths in metres, straight from params.py. +# -------------------------------------------------------------------------- +def build_geometry(ota: P.OTA | None = None) -> dict: + """Assemble the meridian-plane geometry of the GEM from params.py. + + Returns a dict of named parts (each a list of corner Vecs) plus the scalar + dimensions used for annotation. DERIVED placement constants are flagged. + """ + if ota is None: + ota = P.MN78 # params default across the proofs + + lat = P.SITE.latitude_deg + u_ax = polar_axis_unit(lat) + up = _perp_up(lat) + down = _perp_down(lat) + + # --- pier + plates (stacked on +y) --- + pier_h = P.PIER.height_above_m + pier_d = P.PIER.diameter_m + tp_thk = P.PIER.top_plate_thk_m + tp_side = P.PIER.top_plate_side_m + adapter_thk = u.inch(0.75) # pier_adapter.scad, ASSUMED added part + adapter_side = u.inch(10.0) # pier_adapter.scad, ASSUMED added part + + pier = [(-pier_d / 2, 0.0), (pier_d / 2, 0.0), (pier_d / 2, pier_h), (-pier_d / 2, pier_h)] + y0 = pier_h + top_plate = [(-tp_side / 2, y0), (tp_side / 2, y0), + (tp_side / 2, y0 + tp_thk), (-tp_side / 2, y0 + tp_thk)] + y0 += tp_thk + adapter = [(-adapter_side / 2, y0), (adapter_side / 2, y0), + (adapter_side / 2, y0 + adapter_thk), (-adapter_side / 2, y0 + adapter_thk)] + adapter_top = y0 + adapter_thk + + # --- RA housing on the polar axis --- + ra_depth = P.RA_HOUSING.depth_m + ra_face = P.RA_HOUSING.outer_x_m + dec_face = P.DEC_HOUSING.outer_x_m + dec_depth = P.DEC_HOUSING.depth_m + + # RA housing centre: lifted off the adapter so the tilted block clears it. DERIVED. + p_ra = (0.0, adapter_top + ra_face / 2.0 * math.sin(math.radians(lat)) + 0.02) + ra_housing = _oriented_rect(p_ra, u_ax, up, ra_depth, ra_face) + + # DEC head at the far end of the RA axis. DERIVED seat distance. + L_ra = ra_depth + dec_face / 2.0 + 0.04 + c_dec = _add(p_ra, _mul(u_ax, L_ra)) + dec_housing = _oriented_rect(c_dec, u_ax, up, dec_depth, dec_face) + + # --- OTA tube, parallel to the polar axis, on the saddle (up) side --- + saddle = 0.05 # stiffness.SADDLE_LEVER_M, ASSUMED + ota_offset = dec_face / 2.0 + saddle + ota.tube_od_m / 2.0 + tube_center = _add(c_dec, _mul(up, ota_offset)) + # saddle ~1/3 up the tube: shift the tube so more of it rises toward the pole + tube_center = _add(tube_center, _mul(u_ax, ota.tube_length_m / 6.0)) + ota_tube = _oriented_rect(tube_center, u_ax, up, ota.tube_length_m, ota.tube_od_m) + + # --- counterweight shaft on the down side --- + cw_len = P.COUNTERWEIGHTS.shaft_len_m + cw_dia = P.COUNTERWEIGHTS.shaft_dia_m + cw_start = _add(c_dec, _mul(down, dec_face / 2.0)) + cw_center = _add(cw_start, _mul(down, cw_len / 2.0)) + cw_shaft = _oriented_rect(cw_center, down, up, cw_len, cw_dia) + + # counterweight discs near the far end of the shaft (12.5 kg available) + disc_od = 0.127 + discs = [] + for k, frac in enumerate((0.72, 0.82, 0.92)): + dc = _add(cw_start, _mul(down, cw_len * frac)) + discs.append(_oriented_rect(dc, down, up, 0.04, disc_od)) + + return { + "ota": ota, + "lat": lat, + "u_axis": u_ax, + "parts": { + "pier": pier, + "top_plate": top_plate, + "adapter": adapter, + "ra_housing": ra_housing, + "dec_housing": dec_housing, + "ota_tube": ota_tube, + "cw_shaft": cw_shaft, + }, + "discs": discs, + "dims": { + "pier_h": pier_h, + "pier_d": pier_d, + "tube_len": ota.tube_length_m, + "tube_od": ota.tube_od_m, + "cw_len": cw_len, + "cw_dia": cw_dia, + "adapter_side": adapter_side, + "adapter_thk": adapter_thk, + }, + "anchors": { + "p_ra": p_ra, + "c_dec": c_dec, + "tube_center": tube_center, + "cw_start": cw_start, + "adapter_top": adapter_top, + }, + } + + +# -------------------------------------------------------------------------- +# SVG rendering. +# -------------------------------------------------------------------------- +_STYLE = { + "pier": ("#8a8577", "#4a463d"), + "top_plate": ("#4c4c52", "#2a2a2e"), + "adapter": ("#c9a83d", "#7a6417"), + "ra_housing": ("#b9c2c8", "#5d666c"), + "dec_housing": ("#a9b4bb", "#525b61"), + "ota_tube": ("#20232a", "#000000"), + "cw_shaft": ("#3a3a40", "#101014"), +} + + +def _fmt_m(x: float) -> str: + return f"{x:.3f} m" + + +def _fmt_mm(x: float) -> str: + return f"{x * 1000:.0f} mm" + + +def render_svg(ota: P.OTA | None = None, scale_px_per_m: float = 300.0) -> str: + """Render the GEM meridian elevation to an SVG string (pure stdlib).""" + geo = build_geometry(ota) + ota = geo["ota"] + + # Collect all points to compute bounds (maths frame, +y up). + all_pts: list[Vec] = [] + for poly in geo["parts"].values(): + all_pts.extend(poly) + for d in geo["discs"]: + all_pts.extend(d) + + xs = [p[0] for p in all_pts] + ys = [p[1] for p in all_pts] + min_x, max_x = min(xs), max(xs) + min_y, max_y = min(ys), max(0.0, max(ys)) # include grade (y=0) + + pad_m = 0.55 + label_pad_m = 1.15 # extra room on the right for the dimension column + min_x -= pad_m + max_x += pad_m + label_pad_m + min_y -= pad_m + max_y += pad_m + + S = scale_px_per_m + width_px = (max_x - min_x) * S + height_px = (max_y - min_y) * S + + def tx(x: float) -> float: + return (x - min_x) * S + + def ty(y: float) -> float: + return (max_y - y) * S # flip: SVG +y is down + + def poly_svg(poly: list[Vec]) -> str: + return " ".join(f"{tx(px):.1f},{ty(py):.1f}" for px, py in poly) + + parts: list[str] = [] + + # --- header + defs --- + parts.append( + f'' + ) + parts.append( + '' + ) + + # --- ground line + hatch --- + gy = ty(0.0) + parts.append( + f'' + ) + hx = 0.0 + while hx < width_px: + parts.append( + f'' + ) + hx += 18 + + # --- polar-axis centreline (through RA housing to DEC head) --- + p_ra = geo["anchors"]["p_ra"] + u_ax = geo["u_axis"] + a0 = _add(p_ra, _mul(u_ax, -0.12)) + a1 = _add(geo["anchors"]["c_dec"], _mul(u_ax, 0.20)) + parts.append( + f'' + ) + + # --- parts (draw pier first so the head overlaps it) --- + order = ["pier", "top_plate", "adapter", "cw_shaft", "ra_housing", "dec_housing", "ota_tube"] + for name in order: + fill, stroke = _STYLE[name] + parts.append( + f'' + ) + + # counterweight discs + for d in geo["discs"]: + parts.append( + f'' + ) + + # --- dimension annotations (real numbers) --- + dims = geo["dims"] + + def dim_line(x1, y1, x2, y2, text, ty_off=-6, color="#20232a"): + out = [ + f'' + ] + mx = (tx(x1) + tx(x2)) / 2.0 + my = (ty(y1) + ty(y2)) / 2.0 + ty_off + out.append( + f'{text}' + ) + return "".join(out) + + # arrow marker + parts.append( + '' + ) + + # pier height dimension (left of pier) + xdim = -P.PIER.diameter_m / 2 - 0.30 + parts.append(dim_line(xdim, 0.0, xdim, dims["pier_h"], + f'pier {_fmt_m(dims["pier_h"])} ({dims["pier_h"] / u.IN_TO_M:.0f}")', + ty_off=0, color="#4a463d")) + parts.append( + f'' + f'Ø {_fmt_m(dims["pier_d"])}' + ) + + # OTA tube length dimension (along the tube axis) + ota_poly = geo["parts"]["ota_tube"] + up = _perp_up(geo["lat"]) + tl_a = _add(ota_poly[3], _mul(up, 0.12)) # near end top-left corner + tl_b = _add(ota_poly[2], _mul(up, 0.12)) # far end + parts.append(dim_line(tl_a[0], tl_a[1], tl_b[0], tl_b[1], + f'OTA {ota.name.split("(")[0].strip()}: L {_fmt_m(dims["tube_len"])}', + ty_off=-8, color="#20232a")) + + # counterweight shaft length + cw_poly = geo["parts"]["cw_shaft"] + parts.append(dim_line(cw_poly[0][0], cw_poly[0][1], cw_poly[3][0], cw_poly[3][1], + f'CW shaft {_fmt_m(dims["cw_len"])} ({dims["cw_len"] / u.IN_TO_M:.0f}")', + ty_off=14, color="#101014")) + + # latitude callout at the RA housing + parts.append( + f'polar axis @ lat {geo["lat"]:.1f}°' + ) + + # --- title + legend block (top-left) --- + tube_od_txt = _fmt_m(dims["tube_od"]) + parts.append( + '' + 'NIGHTWATCH GEM — meridian elevation (park pose)' + ) + parts.append( + f'' + f'OTA {ota.name} · tube Ø {tube_od_txt} · ' + f'mass {ota.mass_kg:.0f} kg + {P.IMAGING_TRAIN.mass_kg:.0f} kg train · ' + f'drawn to scale from params.py' + ) + + legend = [ + ("pier", "concrete pier 12\" Ø"), + ("adapter", "adapter 10x10x0.75\""), + ("ra_housing", "RA housing (CSF-32, 6008)"), + ("dec_housing", "DEC housing + Losmandy-D (CSF-25, 6006)"), + ("ota_tube", "OTA tube"), + ("cw_shaft", "counterweight shaft"), + ] + ly = 64 + for key, txt in legend: + fill, stroke = _STYLE[key] + parts.append( + f'' + ) + parts.append(f'{txt}') + ly += 18 + + parts.append("") + return "\n".join(parts) + + +def main(out_path: str | Path = DEFAULT_OUT, ota: P.OTA | None = None) -> Path: + out_path = Path(out_path) + svg = render_svg(ota) + out_path.write_text(svg, encoding="utf-8") + return out_path + + +if __name__ == "__main__": + p = main() + geo = build_geometry() + print(f"wrote {p} ({p.stat().st_size} bytes)") + print(f" OTA : {geo['ota'].name}") + print(f" pier height: {geo['dims']['pier_h']:.3f} m") + print(f" tube length: {geo['dims']['tube_len']:.3f} m") + print(f" CW shaft : {geo['dims']['cw_len']:.3f} m") diff --git a/design/mechanical/calc/__init__.py b/design/mechanical/calc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/design/mechanical/calc/balance.py b/design/mechanical/calc/balance.py new file mode 100644 index 0000000..3958ab3 --- /dev/null +++ b/design/mechanical/calc/balance.py @@ -0,0 +1,185 @@ +""" +Mass-balance proof — can the mount be balanced, and can the counterweights be +deleted entirely? + +Two questions, one module: + +Counterweighted (classical GEM) + Solve the RA moment balance ``m_cw * r_cw = m_payload * r_payload`` for the + shaft position of the available 12.5 kg of weights, and check it lands inside + the 18" (0.457 m) counterweight shaft. DEC fore-aft balance is qualitative: + it is set by sliding the OTA in its dovetail saddle, needs no counterweight, + and leaves only a small residual lever (``torque.DEC_RESIDUAL_LEVER_M``). + +Counterweight-FREE (the recommended configuration) + A strain-wave drive back-drives so poorly that the torque proof + (``torque.evaluate(..., counterweight_free=True)``) already shows the RA + harmonic holds the *unbalanced* payload with margin. This module quantifies + what deleting the counterweight buys: the removed mass (weights + the 303-SS + shaft's own ``rho*A*L``) and the reduced RA polar-axis inertia. + +Headline decision: the mount balances comfortably within the existing shaft, AND +the counterweight-free option is viable (per the torque proof) — deleting ~15 kg +of steel and ~29% of the RA slewing inertia. +""" + +from __future__ import annotations + +import math + +from . import params as P +from . import torque +from . import units as u +from .budget import BudgetResult, Verdict + + +def _payload_mass(ota: P.OTA) -> float: + """OTA + lumped rear imaging train (single source: params).""" + return ota.mass_kg + P.IMAGING_TRAIN.mass_kg + + +def shaft_mass_kg(cw: P.CounterweightSystem = P.COUNTERWEIGHTS) -> float: + """Mass of the solid 303-SS counterweight shaft = rho * A * L. DERIVED.""" + area = math.pi / 4.0 * cw.shaft_dia_m ** 2 + return cw.material.rho * area * cw.shaft_len_m + + +def deleted_mass_kg(cw: P.CounterweightSystem = P.COUNTERWEIGHTS) -> float: + """Mass removed by going counterweight-FREE: the weights + the shaft itself.""" + return cw.weights_available_kg + shaft_mass_kg(cw) + + +def required_counterweight_position_m( + ota: P.OTA = P.MN78, + m_cw: float | None = None, + r_payload: float | None = None, +) -> float: + """ + Solve ``m_cw * r_cw = m_payload * r_payload`` for the shaft position r_cw. + + Treats the weights as a point mass (ignores the shaft's own distributed + restoring moment) — CONSERVATIVE: crediting the shaft moment would only pull + r_cw inward, so the point-mass answer is the furthest-out (worst) case. + """ + if m_cw is None: + m_cw = P.COUNTERWEIGHTS.weights_available_kg + if r_payload is None: + r_payload = torque.RA_CG_OFFSET_M + return _payload_mass(ota) * r_payload / m_cw + + +def _ra_payload_inertia_kgm2(ota: P.OTA) -> float: + """RA polar-axis inertia of the payload alone (same model as torque.py).""" + return _payload_mass(ota) * torque.RA_CG_OFFSET_M ** 2 + \ + (1.0 / 12.0) * ota.mass_kg * ota.tube_length_m ** 2 + + +def deleted_ra_inertia_kgm2( + ota: P.OTA = P.MN78, cw: P.CounterweightSystem = P.COUNTERWEIGHTS +) -> float: + """ + RA polar-axis inertia removed with the counterweight system: the weights as + a point mass at r_cw, plus the shaft as a uniform rod about the axis end + (I = 1/3 m L^2). DERIVED. + """ + r_cw = required_counterweight_position_m(ota, cw.weights_available_kg) + i_weights = cw.weights_available_kg * r_cw ** 2 + i_shaft = (1.0 / 3.0) * shaft_mass_kg(cw) * cw.shaft_len_m ** 2 + return i_weights + i_shaft + + +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + cw = P.COUNTERWEIGHTS + + # --- Counterweighted RA moment balance --- + m_pay = _payload_mass(ota) + r_pay = torque.RA_CG_OFFSET_M + payload_moment = m_pay * r_pay # kg*m + r_cw = required_counterweight_position_m(ota) # m + shaft_len = cw.shaft_len_m + fits = r_cw <= shaft_len + fit_factor = shaft_len / r_cw # >1 => inside the shaft + + # --- DEC fore-aft (qualitative) --- + # Rear imaging stack hangs behind the DEC axis; balanced by sliding the OTA + # in the saddle. Residual lever after adjustment = torque.DEC_RESIDUAL_LEVER_M. + dec_rear_moment_Nm = u.weight_N(P.IMAGING_TRAIN.mass_kg) * P.IMAGING_TRAIN.offset_behind_tube_m + dec_residual_moment_Nm = u.weight_N(m_pay) * torque.DEC_RESIDUAL_LEVER_M + + # --- Counterweight-FREE deletion --- + del_mass = deleted_mass_kg(cw) + del_I = deleted_ra_inertia_kgm2(ota, cw) + i_pay = _ra_payload_inertia_kgm2(ota) + inertia_reduction_frac = del_I / (i_pay + del_I) + + # Counterweight-free viability is decided by the torque proof, not re-derived. + tq = torque.evaluate(ota_key, counterweight_free=True) + cwfree_ok = tq.verdict in (Verdict.PASS, Verdict.MARGINAL) + + if fits and cwfree_ok: + verdict = Verdict.PASS + elif fits: + verdict = Verdict.MARGINAL # balances, but torque proof did not clear cw-free + else: + verdict = Verdict.FAIL # cannot balance within the shaft + + res = BudgetResult( + key="balance", + title="Mass-balance proof (counterweighted & counterweight-free)", + verdict=verdict, + headline=( + f"{ota.name}: {cw.weights_available_kg:.1f} kg balances the {m_pay:.0f} kg payload at " + f"r_cw={r_cw*1000:.0f} mm on the {shaft_len*1000:.0f} mm shaft (fit factor {fit_factor:.2f}). " + f"Counterweight-FREE deletes {del_mass:.1f} kg and {inertia_reduction_frac*100:.0f}% of RA " + f"inertia — viable per the torque proof (RA SF {tq.safety_factor:.1f})." + ), + target="RA balance achievable within shaft length AND counterweight-free viable (torque proof)", + safety_factor=fit_factor, + ) + + # --- Counterweighted balance --- + res.add("Payload mass (OTA + train)", m_pay, "kg") + res.add("Payload CG offset from polar axis (r_payload)", r_pay, "m", "torque.RA_CG_OFFSET_M") + res.add("Payload moment about RA axis", payload_moment, "kg*m", "m_payload * r_payload") + res.add("Counterweights available", cw.weights_available_kg, "kg", "2x5 + 1x2.5 kg") + res.add("Required shaft position r_cw", r_cw, "m", "m_cw * r_cw = m_pay * r_pay") + res.add("Shaft length available", shaft_len, "m", '18" 303-SS shaft') + res.add("Fit factor (shaft_len / r_cw)", fit_factor, "x", ">1 => fits") + + # --- DEC fore-aft (qualitative) --- + res.add("DEC rear-stack imbalance moment", dec_rear_moment_Nm, "Nm", + "train weight x 150 mm; nulled by dovetail slide") + res.add("DEC residual moment after adjust", dec_residual_moment_Nm, "Nm", + "residual lever 20 mm (torque.DEC_RESIDUAL_LEVER_M)") + + # --- Counterweight-FREE deletion --- + res.add("Counterweight shaft mass (rho*A*L)", shaft_mass_kg(cw), "kg", "303-SS solid rod") + res.add("Total deleted mass (cw-free)", del_mass, "kg", "weights + shaft") + res.add("RA payload inertia (polar axis)", i_pay, "kg*m^2") + res.add("Deleted RA inertia (weights+shaft)", del_I, "kg*m^2") + res.add("RA inertia reduction (cw-free)", inertia_reduction_frac * 100.0, "%") + res.add("Counterweight-free viable (torque)", 1.0 if cwfree_ok else 0.0, "bool", + f"torque verdict {tq.verdict.value}") + + res.assumptions = [ + f"r_payload = RA CG offset {r_pay*1000:.0f} mm (imported from torque.RA_CG_OFFSET_M, DERIVED).", + "Counterweights modelled as a point mass; the shaft's own distributed moment is NOT " + "credited toward balance -> conservative (pushes r_cw outward, not inward).", + f"Counterweight shaft mass = rho*A*L of 303-SS " + f"(rho={cw.material.rho:.0f} kg/m^3, d={cw.shaft_dia_m*1000:.1f} mm, L={cw.shaft_len_m*1000:.0f} mm) " + f"= {shaft_mass_kg(cw):.2f} kg, DERIVED.", + "DEC fore-aft balance is set by sliding the OTA in its dovetail saddle (no DEC counterweight); " + f"residual lever {torque.DEC_RESIDUAL_LEVER_M*1000:.0f} mm from torque.DEC_RESIDUAL_LEVER_M, ASSUMED.", + "Deleted RA inertia = weights as point mass at r_cw + shaft as a rod about the axis end " + "(I = 1/3 m L^2), DERIVED.", + f"Counterweight-free viability is inherited from the torque proof " + f"(torque.evaluate('{ota_key}', counterweight_free=True) => {tq.verdict.value}), not re-derived here.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/bearings.py b/design/mechanical/calc/bearings.py new file mode 100644 index 0000000..d08c23e --- /dev/null +++ b/design/mechanical/calc/bearings.py @@ -0,0 +1,173 @@ +""" +Bearing proof — L10 rolling-fatigue life and static safety of the axis output +bearings, and the finding that matters more than either number. + +Load model (per axis) + The output bearing pair carries the cantilevered payload. Each pair sees a + direct transverse (radial) load ~ payload weight, plus a couple that reacts + the overturning moment M = W * lever across the bearing span s: F = M / s. + The governing bearing load is taken as ``P = W + M/s`` (conservative: the + direct component is not credited with load-sharing between the two rows). + +L10 fatigue + ``L10 = (C / P)**3 * 1e6`` revolutions, C = dynamic rating. The RA axis turns + only ~1 rev per sidereal day; converting L10 to YEARS shows the fatigue life + is astronomical (~1e7-1e8 yr). Rolling fatigue is NOT the constraint on this + mount and never will be. + +Static safety + ``S0 = C0 / P`` (C0 = static rating). Comfortable (~14-18x) at these loads. + +THE FINDING (headline + assumptions): the Build Package calls the 6008/6006 units +"angular contact", but 60xx are DEEP-GROOVE ball bearings. Deep-groove bearings +have poor MOMENT (tilting) stiffness — and moment stiffness, not load rating, is +what sets a telescope mount's pointing deflection. The recommendation is to use +matched ANGULAR-CONTACT pairs (7008 on RA, 7006 on DEC) in a back-to-back (O) +arrangement, which ties directly to the stiffness proof. The load-capacity verdict +here is PASS with room to spare; the real bearing decision is a stiffness decision. +""" + +from __future__ import annotations + +from . import params as P +from . import torque +from . import units as u +from .budget import BudgetResult, verdict_from_sf + +# Sidereal tracking rate: the RA axis completes ~1 revolution per sidereal day. +# Used for the L10 revolutions -> years conversion. DEC turns far less, so 1/day +# is a conservative (over-cycled) common figure for both. SOURCED (sidereal rate). +TRACK_REV_PER_DAY = 1.0 +DAYS_PER_YEAR = 365.25 + +# Transverse offset from the DEC axis to the OTA CG (tube centreline sits above +# the DEC bearing face). Modelled as tube radius + a dovetail/ring saddle stack. +# The saddle stack is ASSUMED; the tube radius is SOURCED (params tube_od_m). +DEC_SADDLE_STACK_M = 0.05 # ASSUMED: dovetail saddle + ring height, DEC face -> tube surface + +# Static-safety pass threshold. ISO 76 wants S0 ~1-2 for smooth rotation, higher +# for shock; 2.0 is a conservative pass gate here. ASSUMED design gate. +STATIC_SAFETY_PASS = 2.0 + + +def _payload_mass(ota: P.OTA) -> float: + return ota.mass_kg + P.IMAGING_TRAIN.mass_kg + + +def bearing_radial_load(weight_N: float, lever_m: float, span_m: float) -> float: + """ + Governing output-bearing radial load: direct transverse weight plus the + moment-couple reaction M/span. Conservative (no load-sharing credit on W). + """ + moment = weight_N * lever_m + couple = moment / span_m + return weight_N + couple + + +def ra_output_load(ota: P.OTA) -> float: + """RA output-bearing load: payload weight cantilevered at the RA CG offset.""" + W = u.weight_N(_payload_mass(ota)) + return bearing_radial_load(W, torque.RA_CG_OFFSET_M, P.RA_HOUSING.depth_m) + + +def dec_output_load(ota: P.OTA) -> float: + """DEC output-bearing load: OTA weight offset from the DEC axis (tube CG above axis).""" + W = u.weight_N(_payload_mass(ota)) + lever = ota.tube_od_m / 2.0 + DEC_SADDLE_STACK_M + return bearing_radial_load(W, lever, P.DEC_HOUSING.depth_m) + + +def l10_revolutions(bearing: P.Bearing, load_N: float) -> float: + """Basic rating life L10 = (C/P)^3 * 1e6 revolutions (ball bearings).""" + return (bearing.C_dynamic_N / load_N) ** 3 * 1.0e6 + + +def l10_years(bearing: P.Bearing, load_N: float, rev_per_day: float = TRACK_REV_PER_DAY) -> float: + """L10 expressed in YEARS at a given revolutions-per-day duty.""" + return l10_revolutions(bearing, load_N) / (rev_per_day * DAYS_PER_YEAR) + + +def static_safety(bearing: P.Bearing, load_N: float) -> float: + """Static safety factor S0 = C0 / P (ISO 76).""" + return bearing.C0_static_N / load_N + + +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + + ra_load = ra_output_load(ota) + dec_load = dec_output_load(ota) + + ra_L10_yr = l10_years(P.BRG_RA_6008, ra_load) + dec_L10_yr = l10_years(P.BRG_DEC_6006, dec_load) + + ra_S0 = static_safety(P.BRG_RA_6008, ra_load) + dec_S0 = static_safety(P.BRG_DEC_6006, dec_load) + + ra_moment = u.weight_N(_payload_mass(ota)) * torque.RA_CG_OFFSET_M + dec_moment = u.weight_N(_payload_mass(ota)) * (ota.tube_od_m / 2.0 + DEC_SADDLE_STACK_M) + + governing_S0 = min(ra_S0, dec_S0) + # Load-capacity verdict (fatigue + static). Fatigue is a non-issue by ~1e7 + # years, so static safety governs. The moment-stiffness caveat below is the + # real design driver and is flagged loudly, but it belongs to stiffness.py. + verdict = verdict_from_sf(governing_S0, pass_at=STATIC_SAFETY_PASS, marginal_at=1.0) + + res = BudgetResult( + key="bearings", + title="Axis bearing L10 fatigue + static safety", + verdict=verdict, + headline=( + f"Load capacity is NOT the constraint: L10 ~ {ra_L10_yr:.0e} yr (RA) / {dec_L10_yr:.0e} yr " + f"(DEC) at ~1 rev/sidereal-day, static S0 {ra_S0:.0f}x / {dec_S0:.0f}x. FINDING: the repo " + f"calls the 6008/6006 'angular contact' but 60xx are DEEP-GROOVE with poor moment stiffness " + f"— use matched angular-contact 7008/7006 pairs (back-to-back) for moment stiffness (see stiffness proof)." + ), + target=f"L10 >> service life AND static S0 >= {STATIC_SAFETY_PASS:.0f} (fatigue not the constraint)", + safety_factor=governing_S0, + ) + + res.add("Payload mass (OTA + train)", _payload_mass(ota), "kg") + # RA + res.add("RA overturning moment (W x CG offset)", ra_moment, "Nm", "lever = RA_CG_OFFSET_M") + res.add("RA bearing span (housing depth)", P.RA_HOUSING.depth_m, "m", "sets couple arm") + res.add("RA output bearing load P", ra_load, "N", "W + M/span (6008)") + res.add("RA dynamic rating C", P.BRG_RA_6008.C_dynamic_N, "N") + res.add("RA L10 life", l10_revolutions(P.BRG_RA_6008, ra_load), "rev", "(C/P)^3 x 1e6") + res.add("RA L10 life", ra_L10_yr, "yr", "@ ~1 rev/sidereal day") + res.add("RA static safety S0 = C0/P", ra_S0, "x", f"C0={P.BRG_RA_6008.C0_static_N:.0f} N") + # DEC + res.add("DEC overturning moment (W x lever)", dec_moment, "Nm", "lever = tube r + saddle stack") + res.add("DEC bearing span (housing depth)", P.DEC_HOUSING.depth_m, "m") + res.add("DEC output bearing load P", dec_load, "N", "W + M/span (6006)") + res.add("DEC dynamic rating C", P.BRG_DEC_6006.C_dynamic_N, "N") + res.add("DEC L10 life", dec_L10_yr, "yr", "@ ~1 rev/sidereal day") + res.add("DEC static safety S0 = C0/P", dec_S0, "x", f"C0={P.BRG_DEC_6006.C0_static_N:.0f} N") + + res.assumptions = [ + "Governing bearing load P = W + M/span (direct transverse weight + moment couple); " + "no load-sharing credit on the direct term -> conservative.", + f"RA lever = RA_CG_OFFSET_M {torque.RA_CG_OFFSET_M*1000:.0f} mm (from torque, DERIVED); " + f"DEC lever = tube radius {ota.tube_od_m/2*1000:.0f} mm + saddle stack {DEC_SADDLE_STACK_M*1000:.0f} mm " + f"(saddle stack ASSUMED).", + "Bearing span = housing depth (P.RA_HOUSING.depth_m / P.DEC_HOUSING.depth_m); axis tilt at " + "latitude would reduce the transverse component, so vertical W is conservative.", + f"L10 = (C/P)^3 x 1e6 rev; converted to years at {TRACK_REV_PER_DAY:.0f} rev/sidereal day " + f"(~1/day). Both axes exceed 1e7 years -> rolling fatigue is NOT a lifetime constraint.", + "FINDING: params labels BRG_RA_6008 / BRG_DEC_6006 'angular contact', but 60xx are DEEP-GROOVE " + "ball bearings (params.py already flags the deep-groove C ratings). Deep-groove bearings have " + "poor moment/tilting stiffness.", + "RECOMMENDATION: replace with matched ANGULAR-CONTACT pairs — 7008 (RA) / 7006 (DEC) — in a " + "back-to-back (O) arrangement with preload, which provides the moment stiffness that sets " + "pointing deflection. This is a STIFFNESS decision, not a load-rating one (see stiffness proof).", + "The RA output bearing additionally carries DEC-head and (if fitted) counterweight dead weight " + "not modelled here; even at 2-3x the modelled load, S0 stays > 5 and L10 > 1e6 yr, so the " + "conclusion is robust.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/budget.py b/design/mechanical/calc/budget.py new file mode 100644 index 0000000..9cd897b --- /dev/null +++ b/design/mechanical/calc/budget.py @@ -0,0 +1,87 @@ +""" +Common result type for every budget module. + +Each proof (`torque`, `stiffness`, ...) exposes ``evaluate() -> BudgetResult``. +The report is then a pure function of these results, and a test can regenerate +the report tables and diff them against the calculator so no hand-typed number +can drift from the computation. +""" + +from __future__ import annotations + +import enum +from dataclasses import dataclass, field + + +class Verdict(enum.Enum): + PASS = "PASS" + MARGINAL = "MARGINAL" + FAIL = "FAIL" + + @property + def symbol(self) -> str: + return {"PASS": "✅", "MARGINAL": "⚠️", "FAIL": "❌"}[self.value] + + +@dataclass(frozen=True) +class Line: + """One computed quantity in a budget table.""" + label: str + value: float + unit: str + note: str = "" + + def fmt(self, sig: int = 4) -> str: + v = self.value + if v == 0: + s = "0" + elif abs(v) >= 1e5 or (abs(v) < 1e-3 and v != 0): + s = f"{v:.{sig - 1}e}" + else: + s = f"{v:.{sig}g}" + return s + + +@dataclass +class BudgetResult: + key: str # stable id, e.g. "torque" + title: str + verdict: Verdict + headline: str # one-line human summary + lines: list[Line] = field(default_factory=list) + assumptions: list[str] = field(default_factory=list) + safety_factor: float | None = None + target: str = "" # the requirement being checked + + def add(self, label: str, value: float, unit: str, note: str = "") -> None: + self.lines.append(Line(label, value, unit, note)) + + def markdown_table(self) -> str: + rows = ["| Quantity | Value | Unit | Note |", "|---|---:|---|---|"] + for ln in self.lines: + rows.append(f"| {ln.label} | {ln.fmt()} | {ln.unit} | {ln.note} |") + return "\n".join(rows) + + def as_dict(self) -> dict: + return { + "key": self.key, + "title": self.title, + "verdict": self.verdict.value, + "headline": self.headline, + "safety_factor": None if self.safety_factor is None else round(self.safety_factor, 4), + "target": self.target, + "lines": [ + {"label": ln.label, "value": ln.value, "unit": ln.unit, "note": ln.note} + for ln in self.lines + ], + "assumptions": list(self.assumptions), + } + + +def verdict_from_sf(sf: float, pass_at: float = 2.0, marginal_at: float = 1.0) -> Verdict: + """Standard mapping from a safety factor to a verdict.""" + if sf >= pass_at: + return Verdict.PASS + if sf >= marginal_at: + return Verdict.MARGINAL + return Verdict.FAIL diff --git a/design/mechanical/calc/dynamics.py b/design/mechanical/calc/dynamics.py new file mode 100644 index 0000000..3d92e60 --- /dev/null +++ b/design/mechanical/calc/dynamics.py @@ -0,0 +1,191 @@ +""" +First-mode dynamics proof — is the mount's lowest natural frequency safely above +P.NATURAL_FREQ_TARGET_HZ (10 Hz)? + +Why 10 Hz matters: wind-gust energy on a compact tube is concentrated below ~2 Hz +and the OnStepX servo/guide loop operates at ~1-5 Hz. A structural resonance near +those bands would be excited by both the wind and the controller, wrecking +tracking. The first mode must sit well clear -- the repo asserts > 10 Hz. + +Two complementary models, both built from the same beam/bearing stiffnesses used +by ``stiffness.py``: + + 1. Translational bounce (the prescribed model): the payload rides on a series + stack of pier-lateral, housing-beam and bearing-radial springs. + k_eff = series(k_pier, k_ra_beam, k_dec_beam, k_ra_brg, k_dec_brg) + f_n = (1/2pi) * sqrt(k_eff / m_eff) + + 2. Overhung rocking (physical cross-check): the payload swings on the *angular* + compliance of the same beams/bearings at lever L_cg. Because the bearing + spans are short, this rotational mode is the true first mode and comes out + lower than the bounce mode -- so the honest verdict is taken on the minimum + of the two. + +We run a counterweight-FREE case (light payload) and a counterweighted case +(payload + counterweights), the latter being the lower-frequency case. +""" + +from __future__ import annotations + +import math + +from . import params as P +from . import stiffness as S +from .budget import BudgetResult, verdict_from_sf + +_TWO_PI = 2.0 * math.pi + + +# -------------------------------------------------------------------------- +# Stiffness terms (reuse stiffness.py; add the pier). +# -------------------------------------------------------------------------- +def pier_lateral_stiffness() -> float: + """Concrete pier as a lateral cantilever: k = 3*E*I/H^3 (N/m). + + Solid circular section I = pi*D^4/64, height = exposed pier height. + """ + mat = P.PIER.material + I = math.pi * P.PIER.diameter_m ** 4 / 64.0 + return 3.0 * mat.E * I / P.PIER.height_above_m ** 3 + + +def _series(*ks: float) -> float: + return 1.0 / sum(1.0 / k for k in ks) + + +def effective_stiffness(ra_housing: P.Housing, dec_housing: P.Housing) -> float: + """Series translational stiffness of the whole head on the pier (N/m).""" + k_pier = pier_lateral_stiffness() + k_ra_beam = S.beam_translational_stiffness_N_per_m(ra_housing) + k_dec_beam = S.beam_translational_stiffness_N_per_m(dec_housing) + k_ra_brg = S.bearing_pair_stiffness_N_per_m(S.K_RA_BEARING) + k_dec_brg = S.bearing_pair_stiffness_N_per_m(S.K_DEC_BEARING) + return _series(k_pier, k_ra_beam, k_dec_beam, k_ra_brg, k_dec_brg) + + +def effective_mass(ota: P.OTA, counterweighted: bool) -> float: + """Modal mass (kg): payload alone, or payload + counterweights.""" + m = S.payload_mass(ota) + if counterweighted: + m += P.COUNTERWEIGHTS.weights_available_kg + return m + + +def first_mode_hz(ota: P.OTA, ra_housing: P.Housing, dec_housing: P.Housing, + counterweighted: bool = False) -> float: + """Translational bounce mode f_n = (1/2pi) sqrt(k_eff/m_eff) (Hz).""" + k = effective_stiffness(ra_housing, dec_housing) + m = effective_mass(ota, counterweighted) + return math.sqrt(k / m) / _TWO_PI + + +# -------------------------------------------------------------------------- +# Rocking cross-check (angular version of the same beam/bearing model). +# -------------------------------------------------------------------------- +def beam_angular_stiffness(housing: P.Housing) -> float: + """M/theta for a box cantilever under an end moment: E*I/L (N.m/rad).""" + return housing.material.E * S.box_section_I(housing) / housing.depth_m + + +def bearing_angular_stiffness(span: float, k: float) -> float: + """M/theta for a bearing pair reacting a moment couple: k*span^2/2 (N.m/rad).""" + return k * span ** 2 / 2.0 + + +def support_angular_stiffness(ra_housing: P.Housing, dec_housing: P.Housing) -> float: + """Series rotational stiffness of the overhung load path (N.m/rad).""" + return _series( + beam_angular_stiffness(dec_housing), + beam_angular_stiffness(ra_housing), + bearing_angular_stiffness(dec_housing.depth_m, S.K_DEC_BEARING), + bearing_angular_stiffness(ra_housing.depth_m, S.K_RA_BEARING), + ) + + +def rocking_mode_hz(ota: P.OTA, ra_housing: P.Housing, dec_housing: P.Housing) -> float: + """Overhung rocking mode of the payload about the support (Hz). + + J = m * L_cg^2 (payload as a point mass at its gravity lever). + """ + k_theta = support_angular_stiffness(ra_housing, dec_housing) + J = S.payload_mass(ota) * S.cg_lever_m(ota) ** 2 + return math.sqrt(k_theta / J) / _TWO_PI + + +def governing_first_mode_hz(ota: P.OTA, ra_housing: P.Housing, + dec_housing: P.Housing, counterweighted: bool = False) -> float: + """The honest first mode = lower of the bounce and rocking modes (Hz).""" + return min(first_mode_hz(ota, ra_housing, dec_housing, counterweighted), + rocking_mode_hz(ota, ra_housing, dec_housing)) + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + # Bold 12 mm housings are the recommended build; use them for the head beams. + ra_h, dec_h = P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM + + k_eff = effective_stiffness(ra_h, dec_h) + k_pier = pier_lateral_stiffness() + + f_bounce_free = first_mode_hz(ota, ra_h, dec_h, counterweighted=False) + f_bounce_cw = first_mode_hz(ota, ra_h, dec_h, counterweighted=True) + f_rock = rocking_mode_hz(ota, ra_h, dec_h) + + # Governing = lowest of every mode/mass case we examined. + f_governing = min(f_bounce_free, f_bounce_cw, f_rock) + tgt = P.NATURAL_FREQ_TARGET_HZ + sf = f_governing / tgt + verdict = verdict_from_sf(sf, pass_at=1.5, marginal_at=1.0) + + res = BudgetResult( + key="dynamics", + title="First natural frequency (structural first mode)", + verdict=verdict, + headline=( + f"{ota.name}: governing first mode {f_governing:.0f} Hz " + f"(rocking {f_rock:.0f} Hz, bounce {f_bounce_cw:.0f}-{f_bounce_free:.0f} Hz) " + f"vs {tgt:.0f} Hz target -- SF {sf:.1f}, clear of the <2 Hz wind and " + f"1-5 Hz servo bands." + ), + target=f"First mode > {tgt:.0f} Hz (Hedrick), SF>=1.5 to PASS; must clear 1-5 Hz servo", + safety_factor=sf, + ) + res.add("Pier lateral stiffness", k_pier, "N/m", "concrete cantilever 3EI/H^3") + res.add("Head series stiffness k_eff", k_eff, "N/m", "pier+beams+bearings in series") + res.add("Payload mass (CW-free)", effective_mass(ota, False), "kg") + res.add("Payload + counterweights", effective_mass(ota, True), "kg") + res.add("Bounce mode, CW-free", f_bounce_free, "Hz", "translational") + res.add("Bounce mode, counterweighted", f_bounce_cw, "Hz", "heavier -> lower f") + res.add("Rocking mode (governing)", f_rock, "Hz", "overhung on bearing angular stiffness") + res.add("Governing first mode", f_governing, "Hz", "min of all modes") + res.add("Governing / target (SF)", sf, "x", verdict.value) + res.add("Wind-gust excitation band", 2.0, "Hz", "must stay below f_n") + res.add("Servo/guide bandwidth", 5.0, "Hz", "must stay below f_n") + + res.assumptions = [ + "Pier modelled as a solid concrete lateral cantilever (I = pi*D^4/64); its own " + "mass and soil/footing compliance are neglected (stiffer, non-conservative for " + "the pier term but it is not governing).", + "Bearing radial stiffness 6008~250 N/um, 6006~180 N/um (ASSUMED, shared with " + "stiffness.py); rocking uses the derived angular stiffness k*span^2/2.", + "Modal mass = payload (+ 12.5 kg counterweights); head/drive masses and pier " + "participation neglected -> the true modes are slightly lower.", + "Rocking inertia J = m*L_cg^2 with L_cg the same gravity lever as stiffness.py " + "(233 mm CG + 50 mm saddle); point-mass approximation.", + "12 mm bold housings used for the head beams (recommended build); beam terms are " + "negligible vs pier and bearing terms either way.", + "Verdict taken on the lowest mode found (rocking), not the higher bounce mode -- " + "the honest first mode.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) + for a in r.assumptions: + print(" -", a) diff --git a/design/mechanical/calc/enclosure.py b/design/mechanical/calc/enclosure.py new file mode 100644 index 0000000..e6037c2 --- /dev/null +++ b/design/mechanical/calc/enclosure.py @@ -0,0 +1,244 @@ +""" +Roll-off roof mechanics — can the drive move the roof against rolling friction +AND the 35 mph emergency-close gust within the open/close time, and what does the +closed-roof snow load do to the structure? + +Two questions, one honest answer each: + +Drive +----- +The moving element is the framed roof panel (P.ENCLOSURE.roof_mass_kg) on level +track. The tractive force the drive must overcome is: + + F_move = mu * m * g (rolling resistance, level track) + + 0.5*rho*V^2*Cd*A (along-track wind drag on the roof end/fascia) + +evaluated at the 35 mph gust (P.ENV.wind_gust_close_ms) because the roof MUST be +able to *close* against the worst wind it is allowed to be open in. Drive torque +at the wheel is F_move * wheel_radius. A residential garage-door-opener-class +chain drive (~500 N pull) is the reference capacity. The finding: at 35 mph the +wind drag alone is ~60% of the required tractive force — the drive is sized by +wind, not by the roof's own weight — so the margin over a garage-door opener is +only ~2x, sensitive to the roof's frontal height. + +Snow +---- +The CLOSED flat roof carries the full ground snow load: + + F_snow = P.ENV.ground_snow_load_Pa * roof_area + +At 25 psf over a 3x3 m roof this is ~10.8 kN (~1.1 tonne), ~6x the roof's own +dead weight — the snow case, not the wind case, is the structural design driver +for the roof panel and its supports. A FLAT roll-off roof must carry all of it; a +SLOPED, slippery roof sheds most of it (ASCE 7 slope factor Cs -> ~0 for a steep +metal roof), which is a strong argument for a pitched roof or a positive snow +interlock. A fully snow-laden roof also cannot be driven (rolling force exceeds +the opener), so the roof must never be commanded open under snow. + +Headline verdict is on the DRIVE (the mechanism this module can size from params); +the snow load is reported as the governing structural finding + interlock need, +the same way bearings.py PASSes on load capacity while flagging the real driver. +""" + +from __future__ import annotations + +from . import params as P +from . import units as u +from .budget import BudgetResult, Verdict, verdict_from_sf + +# -------------------------------------------------------------------------- +# Modelled constants (params.py is silent on roof drive geometry). +# -------------------------------------------------------------------------- +# Rolling-resistance coefficient, wheel-on-track. Ideal steel-on-steel is +# ~0.001-0.005, but a real roll-off with V-groove wheels, misalignment, seal drag +# and dirt runs far higher; 0.05 is a conservative ASSUMED value. ASSUMED. +ROLLING_MU = 0.05 + +# Drive-wheel (or friction/chain sprocket) pitch radius. ASSUMED (stated: 0.05 m). +DRIVE_WHEEL_RADIUS_M = 0.05 + +# Frontal height of the roof structure (fascia + rafter depth) that presents an +# along-track area to a head/tail wind while the roof is moving. The repo does not +# specify roof section; 0.30 m is an ASSUMED framed-panel depth. ASSUMED. +ROOF_FASCIA_HEIGHT_M = 0.30 + +# Bluff flat-panel drag coefficient for the roof end. ENV.cd_cylinder (1.1) is for +# a round tube; a flat/square bluff face is higher (~1.2-2.0). 1.3 ASSUMED. +CD_ROOF_FLAT = 1.3 + +# Reference drive capacity: a residential garage-door-opener-class chain drive +# pulls ~500 N. Conservative COTS figure; commercial roof drives are stronger. +# ASSUMED (reference capacity). +DRIVE_FORCE_CAP_N = 500.0 + +# Comfortable / safe maximum roof traverse speed (people & pinch hazard). The +# required speed must sit well under this. 0.30 m/s ASSUMED. +FEASIBLE_ROOF_SPEED_MS = 0.30 + +# Pass gate on the drive safety factor (capacity / required tractive force). +DRIVE_SF_PASS = 2.0 + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these). +# -------------------------------------------------------------------------- +def roof_area_m2() -> float: + """Plan area of the roof (m^2), from the assumed footprint in params.""" + return P.ENCLOSURE.roof_span_m * P.ENCLOSURE.roof_length_m + + +def roof_frontal_area_m2() -> float: + """Along-track frontal area the wind pushes on while the roof moves (m^2).""" + return P.ENCLOSURE.roof_span_m * ROOF_FASCIA_HEIGHT_M + + +def roof_rolling_force_N(mass_kg: float | None = None) -> float: + """Rolling resistance on level track: mu * m * g (N).""" + m = P.ENCLOSURE.roof_mass_kg if mass_kg is None else mass_kg + return ROLLING_MU * m * u.G0 + + +def roof_wind_drag_N(wind_ms: float) -> float: + """Along-track wind drag on the roof end/fascia: 0.5*rho*V^2*Cd*A (N).""" + return 0.5 * P.SITE.air_density * wind_ms ** 2 * CD_ROOF_FLAT * roof_frontal_area_m2() + + +def roof_move_force_N(wind_ms: float) -> float: + """ + Total tractive force to move the roof at a given wind speed (N): + rolling resistance + along-track wind drag. At wind=0 this is rolling only. + """ + return roof_rolling_force_N() + roof_wind_drag_N(wind_ms) + + +def roof_drive_torque_Nm(wind_ms: float) -> float: + """Drive torque at the wheel to produce roof_move_force_N: F * wheel_radius.""" + return roof_move_force_N(wind_ms) * DRIVE_WHEEL_RADIUS_M + + +def roof_required_speed_ms() -> float: + """Average roof speed to traverse roof_length within open_time (m/s).""" + return P.ENCLOSURE.roof_length_m / P.ENCLOSURE.open_time_s + + +def roof_snow_load_N() -> float: + """Total closed-roof snow force = ground snow pressure * roof plan area (N).""" + return P.ENV.ground_snow_load_Pa * roof_area_m2() + + +def snow_laden_rolling_force_N() -> float: + """Rolling force to move the roof with the full design snow load sitting on it: + mu * (m_roof + m_snow) * g (N). If this exceeds the drive, the roof cannot be + opened while snow-laden -> a snow interlock is mandatory.""" + m_snow = roof_snow_load_N() / u.G0 + return roof_rolling_force_N(P.ENCLOSURE.roof_mass_kg + m_snow) + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate() -> BudgetResult: + gust = P.ENV.wind_gust_close_ms # 35 mph emergency-close + + f_roll = roof_rolling_force_N() + f_wind = roof_wind_drag_N(gust) + f_move = roof_move_force_N(gust) + torque = roof_drive_torque_Nm(gust) + wind_frac = f_wind / f_move + + v_req = roof_required_speed_ms() + speed_ok = v_req < FEASIBLE_ROOF_SPEED_MS + + sf_drive = DRIVE_FORCE_CAP_N / f_move + + # Snow (structural + operational). + snow_N = roof_snow_load_N() + snow_mass = snow_N / u.G0 + roof_weight = u.weight_N(P.ENCLOSURE.roof_mass_kg) + snow_over_dead = snow_N / roof_weight + f_snow_roll = snow_laden_rolling_force_N() + snow_stalls_drive = f_snow_roll > DRIVE_FORCE_CAP_N + + # Module verdict is the DRIVE feasibility (the mechanism sizeable from params); + # a too-slow-to-be-safe roof would degrade it. Snow is the loud structural + # finding carried in the headline/assumptions (no roof section in params to + # certify against — see notes), mirroring bearings.py. + verdict = verdict_from_sf(sf_drive, pass_at=DRIVE_SF_PASS, marginal_at=1.0) + if not speed_ok and verdict is Verdict.PASS: + verdict = Verdict.MARGINAL + + res = BudgetResult( + key="enclosure", + title="Roll-off roof drive + snow load", + verdict=verdict, + headline=( + f"Drive {verdict.value}: moving the {P.ENCLOSURE.roof_mass_kg:.0f} kg roof against rolling + " + f"the 35 mph close gust needs {f_move:.0f} N ({torque:.1f} Nm @ {DRIVE_WHEEL_RADIUS_M*100:.0f} cm " + f"wheel), SF {sf_drive:.1f} vs a ~{DRIVE_FORCE_CAP_N:.0f} N garage-door-class drive — and wind is " + f"{wind_frac*100:.0f}% of that load, so the drive is WIND-sized. FINDING: the closed flat roof " + f"carries {snow_N/1000:.1f} kN of snow (~{snow_over_dead:.1f}x its dead weight) — the snow case is " + f"the roof's structural design driver; a sloped roof sheds it, and a snow-laden roof " + f"{'CANNOT' if snow_stalls_drive else 'can'} be driven -> snow interlock required." + ), + target=f"Roof drive overcomes rolling + 35 mph gust with SF >= {DRIVE_SF_PASS:.0f} and traverses within open_time", + safety_factor=sf_drive, + ) + + # Drive. + res.add("Roof mass", P.ENCLOSURE.roof_mass_kg, "kg", "ASSUMED (params)") + res.add("Rolling resistance force", f_roll, "N", f"mu={ROLLING_MU} * m * g") + res.add("Wind drag @ 35 mph gust", f_wind, "N", f"0.5*rho*V^2*Cd*A, Cd={CD_ROOF_FLAT}") + res.add("Frontal area (wind)", roof_frontal_area_m2(), "m^2", f"span x {ROOF_FASCIA_HEIGHT_M} m fascia") + res.add("Total tractive force", f_move, "N", "rolling + wind") + res.add("Wind fraction of tractive force", wind_frac * 100.0, "%", "drive is wind-sized") + res.add("Drive torque @ wheel", torque, "Nm", f"F * {DRIVE_WHEEL_RADIUS_M} m") + res.add("Drive capacity (reference)", DRIVE_FORCE_CAP_N, "N", "garage-door-opener class, ASSUMED") + res.add("Drive safety factor", sf_drive, "x", f"capacity / tractive (pass >= {DRIVE_SF_PASS:.0f})") + res.add("Required roof speed", v_req, "m/s", f"{P.ENCLOSURE.roof_length_m:.0f} m / {P.ENCLOSURE.open_time_s:.0f} s") + res.add("Feasible speed ceiling", FEASIBLE_ROOF_SPEED_MS, "m/s", f"speed ok = {speed_ok}") + res.add("Open time vs motor timeout", P.ENCLOSURE.open_time_s, "s", f"< {P.ENCLOSURE.motor_timeout_s:.0f} s timeout") + # Snow. + res.add("Ground snow pressure", P.ENV.ground_snow_load_Pa, "Pa", "25 psf, ASSUMED (params)") + res.add("Roof plan area", roof_area_m2(), "m^2", "span x length") + res.add("Closed-roof snow load", snow_N, "N", "pressure x area") + res.add("Snow mass on roof", snow_mass, "kg", "~1.1 tonne") + res.add("Snow / roof dead weight", snow_over_dead, "x", "snow dominates the structure") + res.add("Snow-laden rolling force", f_snow_roll, "N", f"mu*(m_roof+m_snow)*g {'> drive' if snow_stalls_drive else '< drive'}") + + res.assumptions = [ + f"Rolling resistance coefficient mu = {ROLLING_MU} (ASSUMED): far above ideal steel-on-steel " + "(~0.001-0.005) to cover V-groove wheels, misalignment, seal drag and grit — conservative.", + f"Drive-wheel radius {DRIVE_WHEEL_RADIUS_M} m (ASSUMED, stated). Drive torque = F * radius.", + f"Wind drag uses the 35 mph emergency-close gust (P.ENV.wind_gust_close_ms, SOURCED) on a frontal " + f"area = roof span x {ROOF_FASCIA_HEIGHT_M} m fascia height (ASSUMED) with a flat-panel Cd={CD_ROOF_FLAT} " + f"(ASSUMED; ENV.cd_cylinder=1.1 is for a round tube). Air density {P.SITE.air_density:.3f} kg/m^3 at " + "1800 m (DERIVED). The result is sensitive to the assumed fascia height: a taller roof profile pushes " + "the drive SF toward MARGINAL.", + f"Drive capacity {DRIVE_FORCE_CAP_N:.0f} N is a residential garage-door-opener-class chain drive " + "(ASSUMED reference); a commercial roof drive is stronger. FINDING: at 35 mph the WIND is " + f"{wind_frac*100:.0f}% of the tractive load, so the drive is sized by wind, not roof weight — a " + "positive close against the gust is the governing drive requirement.", + f"Required roof speed {v_req:.3f} m/s ({P.ENCLOSURE.roof_length_m:.0f} m in {P.ENCLOSURE.open_time_s:.0f} s) " + f"is well under the {FEASIBLE_ROOF_SPEED_MS:.2f} m/s comfort/safety ceiling (ASSUMED), and open_time " + f"{P.ENCLOSURE.open_time_s:.0f} s < {P.ENCLOSURE.motor_timeout_s:.0f} s motor timeout (SOURCED), so travel " + "time is not the constraint.", + f"SNOW (structural): the closed flat roof carries the full ground snow load " + f"({P.ENV.ground_snow_load_Pa/u.PSF_TO_PA:.0f} psf, ASSUMED) = {snow_N/1000:.1f} kN over {roof_area_m2():.0f} m^2, " + f"~{snow_over_dead:.1f}x the roof's own {roof_weight/1000:.1f} kN dead weight. The snow case, not wind, is " + "the roof panel/support design driver. A SLOPED, slippery roof sheds most of it (ASCE 7 slope factor " + "Cs -> ~0 for a steep metal roof); a FLAT roll-off roof must carry all of it.", + f"SNOW (operational): a fully snow-laden roof needs {f_snow_roll:.0f} N to roll, which " + f"{'EXCEEDS' if snow_stalls_drive else 'is within'} the {DRIVE_FORCE_CAP_N:.0f} N drive -> the roof must " + "never be commanded open under snow. A snow/ice interlock (or a pitched shedding roof) is required; the " + "safety monitor should hold the roof CLOSED while snow-loaded.", + "OPEN ITEM: params.py specifies no roof structural section, so the snow load here is reported as the " + "governing DESIGN LOAD, not certified against a computed roof capacity — see notes (recommend a POS " + "Enclosure roof-structure entry).", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/encoder.py b/design/mechanical/calc/encoder.py new file mode 100644 index 0000000..1a79c11 --- /dev/null +++ b/design/mechanical/calc/encoder.py @@ -0,0 +1,202 @@ +""" +Tracking-error (pointing) budget — the SHARP proof of the whole mount. + +The repo's headline aspiration is "< 1 arcsec RMS" tracking (Hedrick review, +``P.TRACKING_RMS_TARGET_ARCSEC``). This module asks the uncomfortable question: +does the *as-specified* encoder chain actually reach it? It does not — and that +is the point of the proof. + +Two schemes are compared: + +Baseline (as specified in the Build Package) + A dual-encoder scheme: a fine AMT103 on the *motor* shaft (8192 PPR x100 => + ~1.58" to the axis) plus a cheap AS5600 12-bit absolute "on-axis" chip + (~316" resolution). The servo can only close its loop on the motor encoder, + because the AS5600's quantisation (~91" RMS) is 90x the entire error budget + and is fit only for coarse homing, not tracking. So everything the motor + encoder cannot *see* leaks straight into pointing: + * harmonic-drive periodic error between motor and axis (PE residual), and + * structural / drivetrain flexure between motor and star. + Baseline RMS = RSS(motor_quant, PE_residual, flexure). + +Proposed (v2) + Close the servo loop on a genuine on-axis high-resolution *absolute* ring + (Renishaw RESA-class, ``P.ENC_AXIS_RESA``, sub-arcsec LSB). Sitting on the + axis, it observes and corrects the harmonic PE and every drivetrain/mount + flexure *upstream* of the axis; only the servo following error and the + residual OTA/mirror-cell flexure *downstream* of the ring survive. + Proposed RMS = RSS(resa_quant, servo_following, residual_flexure). + +Headline decision: the baseline chain FAILS < 1 arcsec (~5 arcsec RMS); an +on-axis high-resolution absolute encoder is REQUIRED to reach sub-arcsecond. +""" + +from __future__ import annotations + +import math + +from . import params as P +from .budget import BudgetResult, verdict_from_sf + +# -------------------------------------------------------------------------- +# Modelled error terms. None of these exist in params.py, so they are defined +# and tagged here (DERIVED/ASSUMED) and echoed into result.assumptions. +# -------------------------------------------------------------------------- + +# Harmonic-drive periodic-error residual that the MOTOR encoder cannot observe. +# ASSUMED 4.0" RMS — consistent with the CSF units' 6-8" peak repeatability +# (P.RA_DRIVE.repeatability_arcsec = 6", P.DEC_DRIVE = 8"): a quasi-sinusoidal +# PE of ~6-8" peak-to-peak lands near ~4" RMS once the strain-wave 2/rev term +# and gravity-dependent lost motion are folded in. ASSUMED. +PE_RESIDUAL_ARCSEC = 4.0 + +# Drivetrain + mount flexure between the motor encoder and the star, none of it +# seen by a motor-side encoder. From a stated ~3" structural flexure budget +# (bearing preload, housing wall, harmonic wind-up). ASSUMED. +FLEXURE_ARCSEC = 3.0 + +# On-axis closed-loop servo following error once the loop is closed on a +# high-resolution axis encoder (finite loop bandwidth vs wind/seeing input). +# ASSUMED 0.2" RMS — routine for a stiff sub-kHz servo with a fine axis sensor. +SERVO_FOLLOWING_ARCSEC = 0.2 + +# Residual flexure that even an on-axis ring CANNOT correct: the OTA / focuser / +# mirror-cell flex that lies *downstream* of the encoder ring, between the axis +# and the focal plane. Much smaller than the full 3" chain flexure because the +# ring corrects everything upstream of the axis. ASSUMED 0.5" RMS. +FLEXURE_RESIDUAL_ARCSEC = 0.5 + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these directly). +# -------------------------------------------------------------------------- + + +def quant_rms_arcsec(enc: P.Encoder) -> float: + """RMS error of an ideal uniform quantiser = LSB / sqrt(12) (arcsec).""" + return enc.resolution_arcsec / math.sqrt(12.0) + + +def _rss(*terms: float) -> float: + return math.sqrt(sum(t * t for t in terms)) + + +def tracking_components(scheme: str = "baseline") -> dict[str, float]: + """Return the RMS error terms (arcsec) that RSS into the tracking budget.""" + if scheme == "baseline": + return { + "quant": quant_rms_arcsec(P.ENC_MOTOR_AMT103), # servo on motor encoder + "pe_residual": PE_RESIDUAL_ARCSEC, # unseen by motor encoder + "flexure": FLEXURE_ARCSEC, # unseen by motor encoder + } + if scheme == "proposed": + return { + "quant": quant_rms_arcsec(P.ENC_AXIS_RESA), # servo on on-axis ring + "servo": SERVO_FOLLOWING_ARCSEC, # finite-bandwidth following + "flexure": FLEXURE_RESIDUAL_ARCSEC, # OTA flex downstream of ring + } + raise ValueError(f"unknown scheme {scheme!r} (want 'baseline' or 'proposed')") + + +def tracking_rms_arcsec(scheme: str = "baseline") -> float: + """Total pointing/tracking error (arcsec RMS) for a scheme, via RSS.""" + return _rss(*tracking_components(scheme).values()) + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- + + +def evaluate(scheme: str = "compare") -> BudgetResult: + target = P.TRACKING_RMS_TARGET_ARCSEC + + base = tracking_rms_arcsec("baseline") + prop = tracking_rms_arcsec("proposed") + base_sf = target / base # >1 => meets target with margin + prop_sf = target / prop + + base_c = tracking_components("baseline") + prop_c = tracking_components("proposed") + + base_verdict = verdict_from_sf(base_sf, pass_at=1.5, marginal_at=1.0) + prop_verdict = verdict_from_sf(prop_sf, pass_at=1.5, marginal_at=1.0) + + if scheme == "baseline": + verdict, sf = base_verdict, base_sf + headline = ( + f"Baseline dual-encoder chain: {base:.2f} arcsec RMS vs {target:.1f} arcsec " + f"target (SF {base_sf:.2f}) — FAILS; the motor encoder cannot see harmonic " + f"PE or mount flexure." + ) + elif scheme == "proposed": + verdict, sf = prop_verdict, prop_sf + headline = ( + f"On-axis RESA ring closed loop: {prop:.2f} arcsec RMS vs {target:.1f} arcsec " + f"target (SF {prop_sf:.2f}) — MEETS sub-arcsecond." + ) + elif scheme == "compare": + # The design AS SPECIFIED is the baseline, so the honest module verdict + # is the baseline's: it FAILS, and the proposed ring is the remedy. + verdict, sf = base_verdict, base_sf + headline = ( + f"Baseline dual-encoder chain reaches only {base:.2f} arcsec RMS, FAILING the " + f"{target:.1f} arcsec target; the on-axis RESA ring reaches {prop:.2f} arcsec RMS. " + f"An on-axis high-resolution absolute encoder is REQUIRED to reach sub-arcsecond." + ) + else: + raise ValueError(f"unknown scheme {scheme!r}") + + res = BudgetResult( + key="encoder", + title="Tracking-error (pointing) budget", + verdict=verdict, + headline=headline, + target=f"Tracking error < {target:.1f} arcsec RMS (P.TRACKING_RMS_TARGET_ARCSEC)", + safety_factor=sf, + ) + + # --- Encoder resolutions (LSB to the axis) --- + res.add("AMT103 motor-side resolution (to axis)", P.ENC_MOTOR_AMT103.resolution_arcsec, + "arcsec/LSB", "8192 PPR x100 upstream ratio") + res.add("AS5600 on-axis resolution", P.ENC_AXIS_AS5600.resolution_arcsec, + "arcsec/LSB", "12-bit => quant RMS 91\", homing-grade only") + res.add("RESA on-axis resolution (proposed)", P.ENC_AXIS_RESA.resolution_arcsec, + "arcsec/LSB", "absolute ring, sub-arcsec") + + # --- Baseline chain (servo locked to the motor encoder) --- + res.add("Baseline: motor quant RMS", base_c["quant"], "arcsec", "res/sqrt(12)") + res.add("Baseline: harmonic PE residual", base_c["pe_residual"], "arcsec", "unseen by motor enc") + res.add("Baseline: drivetrain/mount flexure", base_c["flexure"], "arcsec", "unseen by motor enc") + res.add("Baseline total tracking RMS", base, "arcsec", "RSS of the three") + res.add("Baseline margin (target/achieved)", base_sf, "x", "<1 => fails") + + # --- Proposed chain (servo locked to the on-axis ring) --- + res.add("Proposed: RESA quant RMS", prop_c["quant"], "arcsec", "res/sqrt(12)") + res.add("Proposed: servo following error", prop_c["servo"], "arcsec", "on-axis closed loop") + res.add("Proposed: residual OTA flexure", prop_c["flexure"], "arcsec", "downstream of ring") + res.add("Proposed total tracking RMS", prop, "arcsec", "RSS of the three") + res.add("Proposed margin (target/achieved)", prop_sf, "x", ">1 => meets") + + res.assumptions = [ + "Quantisation RMS = LSB/sqrt(12) (ideal uniform quantiser).", + f"Baseline servo closes on the MOTOR encoder ({P.ENC_MOTOR_AMT103.resolution_arcsec:.2f}\" " + f"to axis); the AS5600's {quant_rms_arcsec(P.ENC_AXIS_AS5600):.0f}\" quant RMS makes it " + f"a homing reference only, so harmonic PE and mount flexure leak into pointing. SOURCED chain.", + f"Harmonic PE residual = {PE_RESIDUAL_ARCSEC:.1f}\" RMS (ASSUMED), consistent with CSF " + f"repeatability P.RA_DRIVE={P.RA_DRIVE.repeatability_arcsec:.0f}\" / " + f"P.DEC_DRIVE={P.DEC_DRIVE.repeatability_arcsec:.0f}\".", + f"Drivetrain + mount flexure = {FLEXURE_ARCSEC:.1f}\" RMS (ASSUMED, stated structural budget), " + f"entirely upstream of the axis and thus invisible to a motor encoder.", + f"Proposed on-axis ring corrects everything upstream of the axis; only servo following " + f"({SERVO_FOLLOWING_ARCSEC:.1f}\" RMS, ASSUMED) and residual OTA/focuser flexure " + f"({FLEXURE_RESIDUAL_ARCSEC:.1f}\" RMS, ASSUMED) downstream of the ring remain.", + "Terms combine in RSS (independent, zero-mean error sources).", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/params.py b/design/mechanical/calc/params.py new file mode 100644 index 0000000..0c2e6f5 --- /dev/null +++ b/design/mechanical/calc/params.py @@ -0,0 +1,407 @@ +""" +NIGHTWATCH mechanical-design parameters — the single source of truth. + +Every budget module imports from here, so the numbers can no longer disagree +file-to-file the way they do across the repo today (MN76 vs MN78 mass, 2048 vs +8192 PPR, 10/15/20 deg horizon limits, temp in C vs F ...). + +All values are SI (m, kg, s, N, Pa, K, rad). Imperial/astronomy figures from +the docs are converted on the way in via ``units``. Each field is tagged in +``PROVENANCE`` as: + + S = sourced (stated in a repo file — path given) + D = derived (computed from sourced values) + A = assumed (repo silent — a stated engineering assumption) + +The three ASSUMED structural load cases (survival wind, snow, seismic) and the +DGX thermal figure are the repo's governing voids; they are labelled so nothing +masquerades as measured or specified data. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from . import units as u + +# ========================================================================== +# Materials (E = Young's modulus Pa, sigma_y = yield Pa, rho = kg/m^3, +# cte = 1/K, Sut for fatigue where relevant) +# ========================================================================== + + +@dataclass(frozen=True) +class Material: + name: str + E: float # Young's modulus, Pa + sigma_y: float # yield strength, Pa + rho: float # density, kg/m^3 + cte: float # coeff. thermal expansion, 1/K + G: float = 0.0 # shear modulus, Pa (0 => derive as E/2.6) + + @property + def shear_modulus(self) -> float: + return self.G if self.G > 0 else self.E / 2.6 + + +MATERIALS: dict[str, Material] = { + # 6061-T6 aluminium — the frame material specified throughout the repo. + "6061-T6": Material("6061-T6 aluminium", E=68.9e9, sigma_y=276e6, rho=2700.0, cte=23.6e-6, G=26.0e9), + # 303 stainless — the counterweight shaft. + "303-SS": Material("303 stainless", E=193e9, sigma_y=240e6, rho=8000.0, cte=17.3e-6, G=77.0e9), + # A36 mild steel — pier top plate / steel-pier alternative. + "A36-steel": Material("A36 steel", E=200e9, sigma_y=250e6, rho=7850.0, cte=11.7e-6, G=79.3e9), + # 4000 psi fibre-reinforced concrete — the pier. Ec = 57000*sqrt(f'c[psi]) psi. + "concrete-4ksi": Material("4000 psi concrete", E=24.86e9, sigma_y=27.6e6, rho=2400.0, cte=10.0e-6, G=10.4e9), +} + + +# ========================================================================== +# Optical tube assemblies — the unresolved MN76 / MN78 fork, both carried. +# ========================================================================== + + +@dataclass(frozen=True) +class OTA: + name: str + aperture_m: float + focal_length_m: float + f_ratio: float + mass_kg: float + tube_length_m: float + tube_od_m: float # outer tube diameter (for wind area & inertia) + central_obstruction: float # fraction of aperture diameter + + @property + def cg_from_saddle_m(self) -> float: + """ + CG height above the dovetail saddle plane. A closed Mak-Newt tube is + roughly uniform, so CG ~ mid-tube; the saddle sits ~1/3 up the tube for + balance, giving a modest lever arm. DERIVED (geometric estimate). + """ + return 0.5 * self.tube_length_m - (self.tube_length_m / 3.0) + + +# Case A: MN76 as specified in NIGHTWATCH_Build_Package.md (primary spec table). +MN76 = OTA( + name="Intes-Micro MN76 (f/6)", + aperture_m=u.mm(178.0), + focal_length_m=u.mm(1068.0), + f_ratio=6.0, + mass_kg=9.0, + tube_length_m=u.mm(700.0), + tube_od_m=u.mm(205.0), + central_obstruction=0.25, +) + +# Case B: MN78 as specified in docs/INTES_MICRO_HISTORY.md ("selected", f/8, 14 kg). +MN78 = OTA( + name="Intes-Micro MN78 (f/8)", + aperture_m=u.mm(180.0), + focal_length_m=u.mm(1440.0), + f_ratio=8.0, + mass_kg=14.0, + tube_length_m=u.mm(1400.0), + tube_od_m=u.mm(210.0), + central_obstruction=0.134, +) + +OTA_CASES: dict[str, OTA] = {"MN76": MN76, "MN78": MN78} + + +@dataclass(frozen=True) +class ImagingTrain: + """Lumped rear accessories: camera + ADC + Powermate + diagonal + focuser.""" + mass_kg: float = 4.0 # Musser load analysis (lumped) + offset_behind_tube_m: float = u.mm(150.0) # rear stack lever from tube back, DERIVED + + +IMAGING_TRAIN = ImagingTrain() + + +# ========================================================================== +# Harmonic (strain-wave) drives — Harmonic Drive LLC CSF series. +# ========================================================================== + + +@dataclass(frozen=True) +class HarmonicDrive: + name: str + ratio: float + rated_torque_Nm: float # continuous + peak_torque_Nm: float # momentary + repeatability_arcsec: float + backlash_arcmin: float + mass_kg: float + bore_m: float + + +RA_DRIVE = HarmonicDrive( + name="CSF-32-100-2A-GR", ratio=100.0, rated_torque_Nm=127.0, peak_torque_Nm=343.0, + repeatability_arcsec=6.0, backlash_arcmin=1.0, mass_kg=1.6, bore_m=u.mm(80.0), +) +DEC_DRIVE = HarmonicDrive( + name="CSF-25-80-2A-GR", ratio=80.0, rated_torque_Nm=70.0, peak_torque_Nm=186.0, + repeatability_arcsec=8.0, backlash_arcmin=1.0, mass_kg=1.0, bore_m=u.mm(64.0), +) + + +# ========================================================================== +# Motor / driver train (NEMA17 + planetary + harmonic). +# ========================================================================== + + +@dataclass(frozen=True) +class MotorDrive: + step_angle_deg: float = 1.8 # NEMA17 full step + microsteps: int = 16 # TMC5160 + planetary_ratio: float = 27.0 # StepperOnline planetary gearhead + holding_torque_Nm: float = 0.45 # NEMA17 typical + irun_A: float = 1.5 # TMC5160 run current + igoto_A: float = 2.0 # TMC5160 goto current + slew_rate_dps: float = 4.0 # firmware Config.h goto rate + accel_dps2: float = 2.0 # firmware Config.h goto accel + + +MOTOR = MotorDrive() + + +def steps_per_degree(drive: HarmonicDrive, motor: MotorDrive = MOTOR) -> float: + """Full mechanical resolution to the axis (steps/deg). DERIVED.""" + steps_per_motor_rev = (360.0 / motor.step_angle_deg) * motor.microsteps + axis_ratio = motor.planetary_ratio * drive.ratio + return steps_per_motor_rev * axis_ratio / 360.0 + + +# ========================================================================== +# Encoder chains — the crux of the sub-arcsecond contradiction. +# ========================================================================== + + +@dataclass(frozen=True) +class Encoder: + name: str + counts_per_rev_native: float # counts per revolution of the thing it's on + on_axis: bool # True => already on the telescope axis + upstream_ratio: float = 1.0 # gear ratio between encoder and axis (motor-side) + + @property + def axis_counts_per_rev(self) -> float: + return self.counts_per_rev_native * (self.upstream_ratio if not self.on_axis else 1.0) + + @property + def resolution_arcsec(self) -> float: + return u.ARCSEC_PER_REV / self.axis_counts_per_rev + + +# Baseline dual-encoder scheme from the Build Package / Hedrick review. +ENC_MOTOR_AMT103 = Encoder("AMT103-V motor-side", counts_per_rev_native=8192.0, on_axis=False, upstream_ratio=100.0) +ENC_AXIS_AS5600 = Encoder("AS5600 12-bit on-axis", counts_per_rev_native=4096.0, on_axis=True) + +# Bold v2 proposal: on-axis absolute tape ring (Renishaw RESA-class) sized for sub-arcsec. +# A ~200 mm ring (circumference pi*200 = 628 mm) at ~15 um signal pitch gives ~41,900 +# periods, and Renishaw x5000-class interpolation yields ~2.3e8 counts/rev (~0.0055 +# arcsec/LSB). We use that delivered figure and let the encoder budget check it. +ENC_AXIS_RESA = Encoder("Renishaw RESA ring on-axis (proposed)", counts_per_rev_native=234_000_000.0, on_axis=True) + + +# ========================================================================== +# Frame housings (6061-T6, CNC). Dimensions from the Build Package table. +# ========================================================================== + + +@dataclass(frozen=True) +class Housing: + name: str + outer_x_m: float + outer_y_m: float + depth_m: float # along the axis => sets bearing separation + wall_m: float # structural wall thickness + material_key: str = "6061-T6" + + @property + def material(self) -> Material: + return MATERIALS[self.material_key] + + +# Baseline walls 8 mm (Hedrick min); bold review raised to 12 mm. +RA_HOUSING = Housing("RA housing", outer_x_m=u.inch(8), outer_y_m=u.inch(8), depth_m=u.inch(3), wall_m=u.mm(8)) +DEC_HOUSING = Housing("DEC housing", outer_x_m=u.inch(6), outer_y_m=u.inch(6), depth_m=u.inch(2.5), wall_m=u.mm(8)) +RA_HOUSING_12MM = Housing("RA housing (12 mm wall)", outer_x_m=u.inch(8), outer_y_m=u.inch(8), depth_m=u.inch(3), wall_m=u.mm(12)) +DEC_HOUSING_12MM = Housing("DEC housing (12 mm wall)", outer_x_m=u.inch(6), outer_y_m=u.inch(6), depth_m=u.inch(2.5), wall_m=u.mm(12)) + + +# ========================================================================== +# Bearings (deep-groove values; repo labels them "angular contact" — flagged). +# C = dynamic load rating (N), C0 = static (N). +# ========================================================================== + + +@dataclass(frozen=True) +class Bearing: + name: str + bore_m: float + od_m: float + C_dynamic_N: float + C0_static_N: float + + +BRG_RA_6008 = Bearing("6008-2RS (RA output)", bore_m=u.mm(40), od_m=u.mm(68), C_dynamic_N=16_800.0, C0_static_N=11_600.0) +BRG_DEC_6006 = Bearing("6006-2RS (DEC output)", bore_m=u.mm(30), od_m=u.mm(55), C_dynamic_N=13_300.0, C0_static_N=8_300.0) + + +# ========================================================================== +# Counterweight system (303 SS shaft + weights). +# ========================================================================== + + +@dataclass(frozen=True) +class CounterweightSystem: + shaft_dia_m: float = u.inch(1.25) + shaft_len_m: float = u.inch(18.0) + weights_available_kg: float = 12.5 # 2x5 + 1x2.5 kg + material_key: str = "303-SS" + + @property + def material(self) -> Material: + return MATERIALS[self.material_key] + + +COUNTERWEIGHTS = CounterweightSystem() + + +# ========================================================================== +# Pier (concrete Sonotube) + steel top plate. +# ========================================================================== + + +@dataclass(frozen=True) +class Pier: + diameter_m: float = u.inch(12.0) + height_above_m: float = u.inch(36.0) + embed_depth_m: float = u.inch(36.0) + fc_Pa: float = u.psi(4000.0) + material_key: str = "concrete-4ksi" + top_plate_thk_m: float = u.inch(0.375) + top_plate_side_m: float = u.inch(12.0) + + @property + def material(self) -> Material: + return MATERIALS[self.material_key] + + +PIER = Pier() + + +# ========================================================================== +# Enclosure (roll-off roof). +# ========================================================================== + + +@dataclass(frozen=True) +class Enclosure: + kind: str = "roll-off-roof" + open_time_s: float = 45.0 # POS sim + close_time_s: float = 45.0 + motor_timeout_s: float = 60.0 + # Roof geometry is unspecified in the repo -> assumed footprint for load calcs. + roof_span_m: float = 3.0 # ASSUMED + roof_length_m: float = 3.0 # ASSUMED + roof_mass_kg: float = 180.0 # ASSUMED (framed + panelled roll-off roof) + track_incline_deg: float = 0.0 + + +ENCLOSURE = Enclosure() + + +# ========================================================================== +# Site & environment. +# ========================================================================== + + +@dataclass(frozen=True) +class Site: + latitude_deg: float = 38.9 + longitude_deg: float = -117.4 + elevation_m: float = 1800.0 + + @property + def air_density(self) -> float: + return u.isa_air_density(self.elevation_m) + + +SITE = Site() + + +@dataclass(frozen=True) +class Environment: + # --- operational interlocks (SOURCED: safety monitor / constants) --- + wind_park_ms: float = u.mph(25.0) # park threshold + wind_gust_close_ms: float = u.mph(35.0) # emergency close + temp_min_c: float = u.f_to_c(20.0) # code TEMP_MIN_F + temp_max_c: float = u.f_to_c(100.0) # code TEMP_MAX_F + humidity_max_pct: float = 85.0 + diurnal_swing_c: float = (40.0 - 0.0) * 5.0 / 9.0 # "30-40 F swing" -> ~22 C, DERIVED + # --- structural survival loads (ASSUMED — repo is silent) --- + survival_wind_ms: float = u.mph(105.0) # ASCE 7 basic wind, central NV, Risk Cat I + ground_snow_load_Pa: float = u.psf(25.0) # ASSUMED high-desert @ 6000 ft + seismic_sds_g: float = 0.50 # ASSUMED S_DS, Walker Lane vicinity + # --- aerodynamics --- + cd_cylinder: float = 1.1 # tube broadside drag coefficient + roof_uplift_gcp: float = 0.9 # ASCE net uplift coefficient (windward) + + +ENV = Environment() + + +# ========================================================================== +# Optical / focus (for the thermal budget). +# ========================================================================== + + +@dataclass(frozen=True) +class Optical: + wavelength_m: float = 0.55e-6 # visual band + astrositall_cte: float = 1.5e-7 # +/- over -60..+60 C (mirror substrate) + + +OPTICAL = Optical() + + +# ========================================================================== +# Tracking requirement (the "< 1 arcsec RMS" aspiration made a hard target). +# ========================================================================== +TRACKING_RMS_TARGET_ARCSEC = 1.0 # SOURCED aspiration (Hedrick review) +DEFLECTION_TARGET_ARCSEC = 5.0 # SOURCED target (Hedrick review) +NATURAL_FREQ_TARGET_HZ = 10.0 # SOURCED target (Hedrick review) + + +# ========================================================================== +# Provenance registry — keyed by dotted path; feeds the report's honesty table. +# ========================================================================== +PROVENANCE: dict[str, tuple[u.Provenance, str]] = { + "MN76.mass_kg": (u.Provenance.SOURCED, "NIGHTWATCH_Build_Package.md spec table"), + "MN78.mass_kg": (u.Provenance.SOURCED, "docs/INTES_MICRO_HISTORY.md ('selected')"), + "OTA.cg_from_saddle_m": (u.Provenance.DERIVED, "geometric mid-tube estimate"), + "IMAGING_TRAIN.mass_kg": (u.Provenance.SOURCED, "pos/agents/05_walton_musser.md load analysis"), + "RA_DRIVE": (u.Provenance.SOURCED, "Build_Package.md / Musser (CSF-32-100)"), + "DEC_DRIVE": (u.Provenance.SOURCED, "Build_Package.md / Musser (CSF-25-80)"), + "MOTOR.slew_rate_dps": (u.Provenance.SOURCED, "firmware/onstepx_config/Config.h"), + "ENC_MOTOR_AMT103": (u.Provenance.SOURCED, "Build_Package.md / Hedrick (8192 PPR x100)"), + "ENC_AXIS_AS5600": (u.Provenance.SOURCED, "Build_Package.md / Hedrick (12-bit on-axis)"), + "ENC_AXIS_RESA": (u.Provenance.ASSUMED, "proposed on-axis ring to meet sub-arcsec"), + "RA_HOUSING.wall_m": (u.Provenance.SOURCED, "Hedrick min 8 mm (12 mm bold review)"), + "BRG_RA_6008": (u.Provenance.SOURCED, "Build_Package.md bearing table (deep-groove C ratings)"), + "COUNTERWEIGHTS": (u.Provenance.SOURCED, "Build_Package.md counterweight table"), + "PIER": (u.Provenance.SOURCED, "Build_Package.md concrete pier spec"), + "ENCLOSURE.roof_mass_kg": (u.Provenance.ASSUMED, "roof geometry unspecified in repo"), + "SITE": (u.Provenance.SOURCED, "config.py / Config.h (central Nevada, 1800 m)"), + "SITE.air_density": (u.Provenance.DERIVED, "ISA at 1800 m"), + "ENV.wind_park_ms": (u.Provenance.SOURCED, "services/safety_monitor/monitor.py"), + "ENV.survival_wind_ms": (u.Provenance.ASSUMED, "ASCE 7 basic wind speed, central NV"), + "ENV.ground_snow_load_Pa": (u.Provenance.ASSUMED, "high-desert @ 6000 ft"), + "ENV.seismic_sds_g": (u.Provenance.ASSUMED, "Walker Lane seismicity"), + "TRACKING_RMS_TARGET_ARCSEC": (u.Provenance.SOURCED, "Hedrick '< 1 arcsec RMS'"), + "DEFLECTION_TARGET_ARCSEC": (u.Provenance.SOURCED, "Hedrick '< 5 arcsec @ 25 kg'"), + "NATURAL_FREQ_TARGET_HZ": (u.Provenance.SOURCED, "Hedrick '> 10 Hz'"), +} diff --git a/design/mechanical/calc/pier.py b/design/mechanical/calc/pier.py new file mode 100644 index 0000000..226fbed --- /dev/null +++ b/design/mechanical/calc/pier.py @@ -0,0 +1,311 @@ +""" +Pier / foundation structural proof — is the concrete pier stiff enough (pointing +tilt), high enough in frequency, and stable enough (seismic overturning + soil +embedment + concrete stress) to carry the mount? + +The pier is modelled as a solid circular concrete cantilever fixed at grade: + I = pi*d^4/64, k = 3*E*I/L^3 (L = exposed height above the top plate) + +Four independent checks, each with its own safety factor; the module reports the +governing (minimum) one: + + 1. TILT — pointing rotation of the pier top under the operational (35 mph) + OTA drag. Must be a small fraction of the 5 arcsec pointing budget. + 2. FREQ — pier-substructure first mode f = (1/2pi) sqrt(k/m_tip) with the + full tip mass (head + payload + counterweights). Must clear 10 Hz. + (The coupled head/bearing rocking mode is dynamics.py's job; here we + confirm the pier itself is not the soft element.) + 3. SEISMIC — simplified equivalent-lateral-force base shear V = S_DS*W, its + overturning moment, and stability from soil embedment + dead weight. + 4. CONCRETE — axial bearing + flexural extreme-fibre stress vs f'c and the + derived modulus of rupture (tension/cracking). + +Plus a frost-depth note: the 0.914 m embedment vs the assumed central-NV frost +line (0.3-0.6 m). + +Honest expectation: a squat 12"x36" concrete pier is very stiff and stable — all +four checks PASS — but seismic overturning relies on soil embedment: dead weight +ALONE would not resist it (SF < 1), which is the one result worth stating plainly. +""" + +from __future__ import annotations + +import math + +from . import params as P +from . import units as u +from . import wind +from .budget import BudgetResult, verdict_from_sf + +_TWO_PI = 2.0 * math.pi + +# -------------------------------------------------------------------------- +# Modelled constants the repo does not provide (tagged in .assumptions too). +# -------------------------------------------------------------------------- +# Mount head mechanical mass (RA+DEC housings, drives, saddle, top plate) as a +# lumped figure — the repo dimensions the parts but gives no assembled head mass. +MOUNT_HEAD_MASS_KG = 12.0 # ASSUMED + +# Soil for the embedded-pier stability check (repo gives no geotech data). +SOIL_UNIT_WEIGHT_N_M3 = 18000.0 # ASSUMED medium dense granular (~18 kN/m^3) +SOIL_KP = 3.0 # ASSUMED Rankine passive coeff (phi ~ 30 deg) + +# Assumed central-Nevada high-desert frost penetration depth range. +FROST_DEPTH_TYPICAL_M = 0.6 # ASSUMED conservative upper end (0.3-0.6 m) + +# Concrete modulus-of-rupture coefficient (ACI: f_r = 0.62*sqrt(f'c[MPa]) MPa). +MODULUS_OF_RUPTURE_COEFF = 0.62 # ASSUMED (code value) + + +# -------------------------------------------------------------------------- +# Pier section / stiffness helpers. +# -------------------------------------------------------------------------- +def pier_second_moment_m4(pier: P.Pier = P.PIER) -> float: + """Second moment of area of the solid circular pier (m^4): I = pi*d^4/64.""" + return math.pi * pier.diameter_m ** 4 / 64.0 + + +def pier_area_m2(pier: P.Pier = P.PIER) -> float: + return math.pi * (pier.diameter_m / 2.0) ** 2 + + +def pier_lateral_stiffness_N_per_m(pier: P.Pier = P.PIER) -> float: + """Exposed pier as a lateral cantilever: k = 3*E*I/L^3 (N/m).""" + E = pier.material.E + return 3.0 * E * pier_second_moment_m4(pier) / pier.height_above_m ** 3 + + +# -------------------------------------------------------------------------- +# Tilt (pointing) helpers. +# -------------------------------------------------------------------------- +def pier_tip_deflection_m(force_N: float, pier: P.Pier = P.PIER) -> float: + """Cantilever tip deflection under a tip force: delta = F*L^3/(3EI) (m).""" + E = pier.material.E + I = pier_second_moment_m4(pier) + return force_N * pier.height_above_m ** 3 / (3.0 * E * I) + + +def pier_tilt_arcsec(force_N: float, pier: P.Pier = P.PIER) -> float: + """Pointing rotation of the pier top under a tip force (arcsec). + + Uses the cantilever tip *slope* theta = F*L^2/(2EI) — the angle the mount + base (and hence the optical axis) actually rotates through. This is the + physically correct, conservative pointing metric (it is 1.5x the naive + delta/L estimate of the same deflection). + """ + E = pier.material.E + I = pier_second_moment_m4(pier) + theta = force_N * pier.height_above_m ** 2 / (2.0 * E * I) + return u.rad_to_arcsec(theta) + + +# -------------------------------------------------------------------------- +# Mass / frequency helpers. +# -------------------------------------------------------------------------- +def counterweight_shaft_mass_kg(cw: P.CounterweightSystem = P.COUNTERWEIGHTS) -> float: + """Solid 303-SS shaft mass derived from its geometry (kg).""" + r = cw.shaft_dia_m / 2.0 + volume = math.pi * r ** 2 * cw.shaft_len_m + return volume * cw.material.rho + + +def tip_mass_kg(ota: P.OTA, counterweighted: bool = True) -> float: + """Lumped mass riding on the pier top (kg): head + payload (+ counterweights).""" + m = MOUNT_HEAD_MASS_KG + ota.mass_kg + P.IMAGING_TRAIN.mass_kg + if counterweighted: + m += counterweight_shaft_mass_kg() + P.COUNTERWEIGHTS.weights_available_kg + return m + + +def pier_first_mode_hz(m_tip_kg: float, pier: P.Pier = P.PIER) -> float: + """Pier-substructure first mode f = (1/2pi) sqrt(k/m_tip) (Hz).""" + k = pier_lateral_stiffness_N_per_m(pier) + return math.sqrt(k / m_tip_kg) / _TWO_PI + + +# -------------------------------------------------------------------------- +# Mass bookkeeping for seismic / stress. +# -------------------------------------------------------------------------- +def pier_exposed_mass_kg(pier: P.Pier = P.PIER) -> float: + return pier_area_m2(pier) * pier.height_above_m * pier.material.rho + + +def pier_total_mass_kg(pier: P.Pier = P.PIER) -> float: + return pier_area_m2(pier) * (pier.height_above_m + pier.embed_depth_m) * pier.material.rho + + +# -------------------------------------------------------------------------- +# Seismic (simplified equivalent lateral force). +# -------------------------------------------------------------------------- +def seismic_base_shear_N(w_total_kg: float, env: P.Environment = P.ENV) -> float: + """Simplified ELF base shear V = S_DS * W (N), W = seismic weight.""" + return env.seismic_sds_g * u.weight_N(w_total_kg) + + +def seismic_overturning_Nm(ota: P.OTA, pier: P.Pier = P.PIER, + env: P.Environment = P.ENV) -> float: + """Overturning moment about grade from the ELF distribution (N.m). + + Two lumped masses: the exposed pier at its mid-height and the tip mass at the + OTA height above grade. M = S_DS * g * sum(m_i * h_i). + """ + m_tip = tip_mass_kg(ota, counterweighted=True) + m_pier = pier_exposed_mass_kg(pier) + h_tip = pier.height_above_m + wind.MOUNT_HEAD_HEIGHT_M + h_pier = pier.height_above_m / 2.0 + return env.seismic_sds_g * u.G0 * (m_pier * h_pier + m_tip * h_tip) + + +def soil_passive_resultant_N(pier: P.Pier = P.PIER) -> float: + """Rankine passive resistance developed over the embedded length (N): + Pp = 1/2 * Kp * gamma * D^2 * b (triangular, resultant at 2D/3 depth).""" + D = pier.embed_depth_m + b = pier.diameter_m + return 0.5 * SOIL_KP * SOIL_UNIT_WEIGHT_N_M3 * D ** 2 * b + + +# -------------------------------------------------------------------------- +# Concrete stress. +# -------------------------------------------------------------------------- +def modulus_of_rupture_Pa(pier: P.Pier = P.PIER) -> float: + """Concrete flexural tensile strength f_r = 0.62*sqrt(f'c[MPa]) (Pa).""" + fc_MPa = pier.fc_Pa / 1.0e6 + return MODULUS_OF_RUPTURE_COEFF * math.sqrt(fc_MPa) * 1.0e6 + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + + k = pier_lateral_stiffness_N_per_m() + I = pier_second_moment_m4() + A = pier_area_m2() + + # 1) Tilt under operational (35 mph gust) OTA drag. The drag acts at the + # elevated mount head, so it applies to the pier cantilever both a tip + # force AND a tip moment M = F*h_head; include the moment slope M*L/(EI). + f_wind = wind.drag_force_N(P.ENV.wind_gust_close_ms, ota) + E = P.PIER.material.E + L = P.PIER.height_above_m + moment_slope_arcsec = u.rad_to_arcsec(f_wind * wind.MOUNT_HEAD_HEIGHT_M * L / (E * I)) + tilt = pier_tilt_arcsec(f_wind) + moment_slope_arcsec + delta = pier_tip_deflection_m(f_wind) + sf_tilt = P.DEFLECTION_TARGET_ARCSEC / tilt + + # 2) First-mode frequency with full tip mass. + m_tip = tip_mass_kg(ota, counterweighted=True) + f_pier = pier_first_mode_hz(m_tip) + sf_freq = f_pier / P.NATURAL_FREQ_TARGET_HZ + + # 3) Seismic base shear + overturning stability. + w_seismic_kg = m_tip + pier_exposed_mass_kg() + V_shear = seismic_base_shear_N(w_seismic_kg) + M_ot = seismic_overturning_Nm(ota) + Pp = soil_passive_resultant_N() + D = P.PIER.embed_depth_m + M_resist_passive = Pp * (2.0 * D / 3.0) # about grade + W_total = u.weight_N(pier_total_mass_kg() + m_tip) + M_resist_weight = W_total * (P.PIER.diameter_m / 2.0) # dead-weight about toe + sf_seismic_ot = (M_resist_passive + M_resist_weight) / M_ot + sf_seismic_ot_deadwt = M_resist_weight / M_ot # embedment-neglected + sf_seismic_shear = Pp / V_shear + + # 4) Concrete stress: axial bearing + flexure from the governing lateral + # moment (seismic overturning governs; the OTA is shielded at survival wind). + M_flex = M_ot + sigma_axial = u.weight_N(m_tip) / A + sigma_bend = M_flex * (P.PIER.diameter_m / 2.0) / I + sigma_comp = sigma_axial + sigma_bend + sigma_tens = sigma_bend - sigma_axial + f_r = modulus_of_rupture_Pa() + sf_conc_comp = P.PIER.fc_Pa / sigma_comp + sf_conc_tens = f_r / sigma_tens + + # Frost embedment adequacy. + sf_frost = P.PIER.embed_depth_m / FROST_DEPTH_TYPICAL_M + + # Governing = worst of the four structural checks (frost reported separately). + governing_sf = min(sf_tilt, sf_freq, sf_seismic_shear, sf_seismic_ot, sf_conc_tens) + verdict = verdict_from_sf(governing_sf, pass_at=2.0, marginal_at=1.0) + + res = BudgetResult( + key="pier", + title="Pier / foundation (tilt, frequency, seismic, concrete)", + verdict=verdict, + headline=( + f"12\"x36\" concrete pier: tilt {tilt:.2f}\" (SF {sf_tilt:.0f}), " + f"f_n {f_pier:.0f} Hz (SF {sf_freq:.0f}), seismic overturning SF " + f"{sf_seismic_ot:.1f} (dead-weight-only {sf_seismic_ot_deadwt:.2f} -> embedment " + f"governs), concrete SF {min(sf_conc_comp, sf_conc_tens):.0f}. Governing SF " + f"{governing_sf:.1f} ({verdict.value})." + ), + target="Tilt << 5\"; f_n > 10 Hz; seismic stable (embed+weight); " + "concrete < f'c & modulus of rupture", + safety_factor=governing_sf, + ) + res.add("Pier I (pi d^4/64)", I, "m^4") + res.add("Pier lateral stiffness k", k, "N/m", "3EI/L^3 cantilever") + res.add("-- tilt (35 mph gust) --", 0.0, "", "") + res.add("OTA drag @ 35 mph", f_wind, "N", "from wind.py") + res.add("Pier tip deflection", delta * 1e6, "um", "F L^3/3EI") + res.add("Pier pointing tilt", tilt, "arcsec", "tip slope F L^2/2EI") + res.add("Tilt budget / actual (SF)", sf_tilt, "x", "vs 5\" pointing budget") + res.add("-- frequency --", 0.0, "", "") + res.add("Tip mass (head+payload+CW)", m_tip, "kg") + res.add("Pier first mode", f_pier, "Hz", "sqrt(k/m)/2pi") + res.add("f_n / target (SF)", sf_freq, "x", "vs 10 Hz") + res.add("-- seismic (S_DS=0.5) --", 0.0, "", "") + res.add("Seismic weight W", u.weight_N(w_seismic_kg), "N", "tip + exposed pier") + res.add("Base shear V = S_DS W", V_shear, "N") + res.add("Overturning moment", M_ot, "Nm", "about grade") + res.add("Soil passive resultant Pp", Pp, "N", "assumed granular, Kp=3") + res.add("Overturning SF (embed+weight)", sf_seismic_ot, "x") + res.add("Overturning SF (dead-weight only)", sf_seismic_ot_deadwt, "x", + "< 1 -> embedment required") + res.add("Sliding SF (Pp / V)", sf_seismic_shear, "x") + res.add("-- concrete stress --", 0.0, "", "") + res.add("Axial bearing stress", sigma_axial, "Pa") + res.add("Flexural stress (seismic)", sigma_bend, "Pa", "M c / I") + res.add("Net compression / f'c (SF)", sf_conc_comp, "x") + res.add("Net tension / modulus rupture (SF)", sf_conc_tens, "x", "cracking check") + res.add("-- frost --", 0.0, "", "") + res.add("Embedment / frost depth (SF)", sf_frost, "x", + f"0.914 m vs assumed {FROST_DEPTH_TYPICAL_M} m frost") + res.add("Governing safety factor", governing_sf, "x", verdict.value) + + res.assumptions = [ + "Pier modelled as a solid circular concrete cantilever fixed at grade " + "(I = pi*d^4/64, k = 3EI/L^3); soil-spring base fixity and pier self-mass in " + "the frequency term are neglected (slightly non-conservative on stiffness).", + "Pointing tilt uses the cantilever tip SLOPE F*L^2/2EI (the true mount-base " + "rotation), 1.5x the delta/L estimate of the tip deflection F*L^3/3EI.", + "Tilt driven by the 35 mph operational gust (roof-open max); the OTA is shielded " + "at survival wind, so wind never governs the pier structurally.", + f"Tip mass = assumed {MOUNT_HEAD_MASS_KG:.0f} kg head + payload " + f"({ota.mass_kg + P.IMAGING_TRAIN.mass_kg:.0f} kg) + counterweights " + f"({counterweight_shaft_mass_kg() + P.COUNTERWEIGHTS.weights_available_kg:.1f} kg, " + "shaft mass DERIVED from geometry); head mass ASSUMED.", + f"Seismic S_DS = {P.ENV.seismic_sds_g} g is ASSUMED (Walker Lane vicinity; repo " + "silent). Simplified ELF: V = S_DS*W, two-mass overturning distribution.", + f"Soil ASSUMED medium-dense granular: gamma = {SOIL_UNIT_WEIGHT_N_M3/1000:.0f} kN/m^3, " + f"Kp = {SOIL_KP} (phi ~ 30 deg); passive resultant over the {P.PIER.embed_depth_m:.3f} m " + "embedment provides the overturning resistance — dead weight alone would not.", + f"Concrete f'c = {P.PIER.fc_Pa/1e6:.1f} MPa (SOURCED); modulus of rupture " + f"f_r = 0.62*sqrt(f'c) = {modulus_of_rupture_Pa()/1e6:.2f} MPa (ASSUMED code value) " + "used for the tension/cracking check.", + f"Frost line ASSUMED 0.3-0.6 m for central-NV high desert; the {P.PIER.embed_depth_m:.3f} m " + "embedment (SOURCED) exceeds it.", + "Coupled head/bearing rocking (the true system first mode) is owned by dynamics.py; " + "this module only confirms the pier itself is not the soft/low-frequency element.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) + for a in r.assumptions: + print(" -", a) diff --git a/design/mechanical/calc/power.py b/design/mechanical/calc/power.py new file mode 100644 index 0000000..487ef60 --- /dev/null +++ b/design/mechanical/calc/power.py @@ -0,0 +1,294 @@ +""" +Night-time energy budget — the repo's open power question, resolved. + +The observatory's power story has three layers, and this module sizes each and +gives the honest verdict: + +Load tally +---------- +Every night-time draw, stated with provenance: + + mount (idle/track) 12 V * IRUN * 2 axes (params: MOTOR.irun_A) + camera ~36 W (cooled CMOS + TEC, ASSUMED) + focuser ~5 W (ASSUMED) + controllers / Pi ~15 W (OnStepX + Raspberry Pi, ASSUMED) + weather / sensors ~10 W (ASSUMED) + network ~15 W (switch/router/PoE, ASSUMED) + DGX Spark ~170 W (Grace+Blackwell class, ASSUMED) + +The DGX Spark figure is the single biggest number and is ASSUMED — the repo names +the box (README "NVIDIA DGX Spark") but gives no wattage. It alone is ~60% of the +whole night load, so it dominates every downstream sizing decision. + +UPS (graceful shutdown, SOURCED spec) +------------------------------------- +A 1500 VA / 900 W UPS with ~30 min runtime rides the load only long enough to +park + close + flush (~2 min) — a ~15x energy margin on the shutdown, but it does +NOT run a full ~10 h night (it holds ~5% of one). This matches the power_manager +design (park at 50%, emergency close at 20%). + +Solar + battery (autonomy) +-------------------------- +A 400 W panel + 100 Ah LiFePO4. Battery energy depends on pack voltage, which the +repo does NOT state: 12 V -> 1.2 kWh, 48 V -> 4.8 kWh. Autonomy = usable Wh / +night load. At 12 V the pack gives only ~3.3 h — it FAILS a 10 h winter night; +at 48 V it gives ~13 h and passes. The pack voltage is therefore a decisive, +unspecified design parameter, and the 170 W DGX is why it matters. + +DGX thermal +----------- +170 W dumped into the enclosure raises the interior ~14 K above ambient at a +natural ~2 ACH — enough to wreck local seeing and invite dew — so the DGX needs +forced ventilation or, better, to live OUTSIDE the optical enclosure. + +Headline verdict is on AUTONOMY (the open question): with the specified 12 V pack +and the assumed DGX load the observatory CANNOT run a winter night off-grid. That +is the honest, important result — and it is fixed by a 48 V pack and/or by +duty-cycling the DGX instead of idling it all night. +""" + +from __future__ import annotations + +from . import params as P +from .budget import BudgetResult, verdict_from_sf + +# -------------------------------------------------------------------------- +# Load model (params has no power dataclass -> modelled here; see notes). +# -------------------------------------------------------------------------- +# Motor bus voltage. Repo firmware runs the TMC5160/NEMA17 rail at 12 V but this +# is not in params.py. ASSUMED. (V_bus * I_run over-estimates supply draw because +# the driver chops the phase current, so the mount figures are conservative.) +MOTOR_BUS_V = 12.0 # ASSUMED + +# RA + DEC = two axis motors (the two HarmonicDrive entries in params). DERIVED. +N_AXIS_MOTORS = 2 + +LOAD_CAMERA_W = 36.0 # ASSUMED: cooled CMOS with TEC at full cooling +LOAD_FOCUSER_W = 5.0 # ASSUMED +LOAD_CONTROLLERS_W = 15.0 # ASSUMED: OnStepX controller + Raspberry Pi host +LOAD_WEATHER_W = 10.0 # ASSUMED: weather station + sky/safety sensors +LOAD_NETWORK_W = 15.0 # ASSUMED: managed switch / router / PoE +DGX_SPARK_W = 170.0 # ASSUMED: NVIDIA DGX Spark (Grace+Blackwell class); +# repo names the box (README) but gives NO wattage. + +# -------------------------------------------------------------------------- +# UPS (COTS design spec; not in params.py -> see notes). Tagged SOURCED (spec). +# -------------------------------------------------------------------------- +UPS_VA = 1500.0 # SOURCED (UPS spec) +UPS_W = 900.0 # SOURCED (UPS spec) +UPS_RUNTIME_MIN_RATED = 30.0 # SOURCED (UPS spec, nominal) +# Usable battery energy of this UPS class (~2x 12 V 9 Ah AGM, ~80% usable). ASSUMED; +# cross-checked below: 150 Wh / night-load ~ 31 min, consistent with the 30-min spec. +UPS_USABLE_WH = 150.0 # ASSUMED +# Time to park + close + flush before power-down. ASSUMED (power_manager sequence). +SHUTDOWN_TIME_MIN = 2.0 # ASSUMED + +# -------------------------------------------------------------------------- +# Solar + battery (COTS design spec; not in params.py -> see notes). +# -------------------------------------------------------------------------- +PANEL_W = 400.0 # SOURCED (spec) +BATTERY_AH = 100.0 # SOURCED (spec) +DEFAULT_PACK_V = 12.0 # ASSUMED: repo silent; 48 V would be 4x the energy +BATTERY_USABLE_FRAC = 0.80 # ASSUMED: LiFePO4 depth-of-discharge for cycle life +WINTER_PEAK_SUN_HR = 4.5 # ASSUMED: central-Nevada winter peak-sun-hours +PV_DERATE = 0.75 # ASSUMED: temperature + soiling + MPPT + wiring +WINTER_NIGHT_HR = 10.0 # ASSUMED: imaging-night length to cover (astro dark +# is ~14 h in Dec, so 10 h is a generous target) +AUTONOMY_SF_PASS = 1.0 # cover exactly one night to PASS +AUTONOMY_SF_MARGINAL = 0.8 + +# -------------------------------------------------------------------------- +# DGX thermal (enclosure heat load). +# -------------------------------------------------------------------------- +ENCLOSURE_WALL_HEIGHT_M = 2.4 # ASSUMED wall height (roof footprint from params) +AIR_EXCHANGE_ACH = 2.0 # ASSUMED natural air changes per hour +CP_AIR = 1005.0 # ASSUMED specific heat of air (J/kg-K) + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these). +# -------------------------------------------------------------------------- +def mount_power_W(slewing: bool = False) -> float: + """Mount electrical draw: V_bus * I * n_axes, I = IGOTO slewing else IRUN.""" + current = P.MOTOR.igoto_A if slewing else P.MOTOR.irun_A + return MOTOR_BUS_V * current * N_AXIS_MOTORS + + +def load_breakdown_W(slewing: bool = False) -> dict[str, float]: + """Every night-time load in watts, keyed by name.""" + return { + "mount": mount_power_W(slewing), + "camera": LOAD_CAMERA_W, + "focuser": LOAD_FOCUSER_W, + "controllers": LOAD_CONTROLLERS_W, + "weather": LOAD_WEATHER_W, + "network": LOAD_NETWORK_W, + "dgx": DGX_SPARK_W, + } + + +def night_load_W(slewing: bool = False) -> float: + """Steady night-time load (W). slewing=True swaps mount idle -> goto current.""" + return sum(load_breakdown_W(slewing).values()) + + +def ups_shutdown_margin() -> float: + """UPS usable energy / energy needed to park+close+flush = shutdown margin (x).""" + shutdown_wh = night_load_W() * (SHUTDOWN_TIME_MIN / 60.0) + return UPS_USABLE_WH / shutdown_wh + + +def ups_runtime_min(load_W: float | None = None) -> float: + """Approximate UPS runtime at a given load (min); default = night load.""" + load = night_load_W() if load_W is None else load_W + return UPS_USABLE_WH / load * 60.0 + + +def battery_usable_wh(pack_v: float = DEFAULT_PACK_V) -> float: + """Usable battery energy = Ah * V * usable-fraction (Wh).""" + return BATTERY_AH * pack_v * BATTERY_USABLE_FRAC + + +def solar_autonomy_hours(pack_v: float = DEFAULT_PACK_V) -> float: + """Battery-only autonomy at the night load (h) for a given pack voltage.""" + return battery_usable_wh(pack_v) / night_load_W() + + +def solar_daily_harvest_wh() -> float: + """Energy a 400 W panel harvests on a winter day (Wh) = W * PSH * derate.""" + return PANEL_W * WINTER_PEAK_SUN_HR * PV_DERATE + + +def enclosure_volume_m3() -> float: + """Enclosure air volume from the roof footprint x assumed wall height (m^3).""" + return P.ENCLOSURE.roof_span_m * P.ENCLOSURE.roof_length_m * ENCLOSURE_WALL_HEIGHT_M + + +def dgx_ventilation_dT(ach: float = AIR_EXCHANGE_ACH, heat_W: float = DGX_SPARK_W) -> float: + """ + Steady interior temperature rise from a heat load with a given air-change + rate: dT = Q / (rho * cp * Vdot), Vdot = ACH * volume / 3600 (K). + """ + vdot = ach * enclosure_volume_m3() / 3600.0 + return heat_W / (P.SITE.air_density * CP_AIR * vdot) + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate() -> BudgetResult: + loads = load_breakdown_W(slewing=False) + load = night_load_W() # steady tracking load + load_slew = night_load_W(slewing=True) # transient during a goto + dgx_frac = DGX_SPARK_W / load + + ups_margin = ups_shutdown_margin() + ups_rt = ups_runtime_min() + night_need_wh = load * WINTER_NIGHT_HR + ups_night_frac = UPS_USABLE_WH / night_need_wh + + autonomy_12 = solar_autonomy_hours(12.0) + autonomy_48 = solar_autonomy_hours(48.0) + harvest = solar_daily_harvest_wh() + harvest_frac = harvest / night_need_wh + + dT = dgx_ventilation_dT() + + # Governing verdict = AUTONOMY on the specified (12 V) pack: this is the repo's + # open question and the honest failure. UPS-shutdown is a separate PASS. + autonomy_sf = autonomy_12 / WINTER_NIGHT_HR + verdict = verdict_from_sf(autonomy_sf, pass_at=AUTONOMY_SF_PASS, marginal_at=AUTONOMY_SF_MARGINAL) + + res = BudgetResult( + key="power", + title="Night-time energy budget (UPS + solar autonomy)", + verdict=verdict, + headline=( + f"Night load ~{load:.0f} W, {dgx_frac*100:.0f}% of it the ASSUMED {DGX_SPARK_W:.0f} W DGX Spark. " + f"The 1500 VA/900 W UPS rides shutdown with {ups_margin:.0f}x margin (~{ups_rt:.0f} min) but only " + f"~{ups_night_frac*100:.0f}% of a night. Off-grid AUTONOMY {verdict.value}: the specified 400 W + " + f"100 Ah pack gives only {autonomy_12:.1f} h at 12 V (< {WINTER_NIGHT_HR:.0f} h night) — a 48 V pack " + f"gives {autonomy_48:.1f} h — and the panel harvests ~{harvest/1000:.1f} kWh/day vs ~{night_need_wh/1000:.1f} " + f"kWh needed. Drop/duty-cycle the DGX or go 48 V for true autonomy. (DGX dumps ~{dT:.0f} K into the " + f"enclosure at {AIR_EXCHANGE_ACH:.0f} ACH -> ventilate or locate it outside.)" + ), + target=f"Solar+battery sustains the ~{WINTER_NIGHT_HR:.0f} h winter night on the specified pack (SF = autonomy / night)", + safety_factor=autonomy_sf, + ) + + # Load tally. + res.add("Mount (idle/track)", loads["mount"], "W", f"{MOTOR_BUS_V:.0f} V * {P.MOTOR.irun_A} A * {N_AXIS_MOTORS} axes") + res.add("Mount (goto slew)", mount_power_W(True), "W", f"IGOTO {P.MOTOR.igoto_A} A (transient)") + res.add("Camera", loads["camera"], "W", "cooled CMOS + TEC, ASSUMED") + res.add("Focuser", loads["focuser"], "W", "ASSUMED") + res.add("Controllers / Pi", loads["controllers"], "W", "ASSUMED") + res.add("Weather / sensors", loads["weather"], "W", "ASSUMED") + res.add("Network", loads["network"], "W", "ASSUMED") + res.add("DGX Spark", loads["dgx"], "W", "ASSUMED (repo gives no wattage)") + res.add("Night load (steady)", load, "W", "sum, mount idle") + res.add("Night load (during goto)", load_slew, "W", "mount at IGOTO") + res.add("DGX share of night load", dgx_frac * 100.0, "%", "the load is DGX-dominated") + # UPS. + res.add("UPS rating", UPS_W, "W", f"{UPS_VA:.0f} VA, SOURCED spec") + res.add("UPS usable energy", UPS_USABLE_WH, "Wh", "~2x12V9Ah class, ASSUMED") + res.add("UPS runtime @ night load", ups_rt, "min", f"validates ~{UPS_RUNTIME_MIN_RATED:.0f} min spec") + res.add("Shutdown time (park+close)", SHUTDOWN_TIME_MIN, "min", "ASSUMED") + res.add("UPS shutdown margin", ups_margin, "x", "usable / shutdown energy") + res.add("UPS fraction of a night", ups_night_frac * 100.0, "%", f"of {WINTER_NIGHT_HR:.0f} h -> NOT overnight") + # Solar + battery. + res.add("Solar panel", PANEL_W, "W", "SOURCED spec") + res.add("Battery capacity", BATTERY_AH, "Ah", "LiFePO4, SOURCED spec") + res.add("Battery energy @ 12 V", battery_usable_wh(12.0), "Wh", f"{BATTERY_USABLE_FRAC*100:.0f}% usable") + res.add("Battery energy @ 48 V", battery_usable_wh(48.0), "Wh", "4x -> voltage is decisive") + res.add("Autonomy @ 12 V", autonomy_12, "h", f"< {WINTER_NIGHT_HR:.0f} h night -> FAILS") + res.add("Autonomy @ 48 V", autonomy_48, "h", f"> {WINTER_NIGHT_HR:.0f} h night -> passes") + res.add("Daily solar harvest (winter)", harvest, "Wh", f"{PANEL_W:.0f}W*{WINTER_PEAK_SUN_HR}PSH*{PV_DERATE}") + res.add("Night energy need", night_need_wh, "Wh", f"load * {WINTER_NIGHT_HR:.0f} h") + res.add("Harvest / night need", harvest_frac * 100.0, "%", "< 100% -> not fully self-sustaining") + # DGX thermal. + res.add("Enclosure volume", enclosure_volume_m3(), "m^3", f"footprint x {ENCLOSURE_WALL_HEIGHT_M} m") + res.add("DGX interior dT @ 2 ACH", dT, "K", "forced ventilation needed") + + res.assumptions = [ + f"Motor bus {MOTOR_BUS_V:.0f} V (ASSUMED; not in params) x IRUN {P.MOTOR.irun_A} A / IGOTO " + f"{P.MOTOR.igoto_A} A (params) x {N_AXIS_MOTORS} axes. V_bus*I over-estimates supply draw (the TMC " + "driver chops the phase current, and DEC holds at reduced current), so the mount figure is conservative.", + "Device wattages ASSUMED (repo gives none): camera 36 W (cooled CMOS+TEC), focuser 5 W, " + "controllers/Pi 15 W, weather/sensors 10 W, network 15 W. These sum to ~117 W of non-DGX load.", + f"DGX Spark {DGX_SPARK_W:.0f} W is ASSUMED — the README names 'NVIDIA DGX Spark' (Grace+Blackwell class) " + f"but states NO wattage. It is ~{dgx_frac*100:.0f}% of the whole night load and therefore dominates the UPS, " + "battery and thermal sizing. Every autonomy conclusion below hinges on this one assumed number.", + f"UPS 1500 VA / 900 W, ~{UPS_RUNTIME_MIN_RATED:.0f} min runtime (SOURCED spec, not in params). Usable " + f"energy {UPS_USABLE_WH:.0f} Wh (ASSUMED, ~2x12V9Ah at 80% DoD); cross-check: {UPS_USABLE_WH:.0f} Wh / " + f"{night_load_W():.0f} W ~ {ups_rt:.0f} min, consistent with the 30-min spec. Park+close+flush " + f"{SHUTDOWN_TIME_MIN:.0f} min (ASSUMED) -> {ups_margin:.0f}x shutdown margin, but the UPS holds only " + f"~{ups_night_frac*100:.0f}% of a {WINTER_NIGHT_HR:.0f} h night: it is a graceful-shutdown store, not an " + "overnight supply (matches power_manager: park at 50%, emergency close at 20%).", + f"Battery energy = {BATTERY_AH:.0f} Ah x pack V x {BATTERY_USABLE_FRAC*100:.0f}% usable. Pack voltage is " + "UNSPECIFIED in the repo and DECISIVE: 12 V -> 1.2 kWh (~3.3 h autonomy, FAILS the night); 48 V -> 4.8 kWh " + "(~13 h, passes). The headline verdict uses the lower, specified-by-default 12 V case — flag and resolve " + "the pack voltage.", + f"Autonomy = usable Wh / night load; target = one {WINTER_NIGHT_HR:.0f} h winter imaging night (ASSUMED; " + "astronomical dark is ~14 h in December, so 10 h is a generous target). SF = autonomy / night; pass at " + f"{AUTONOMY_SF_PASS:.1f}. At 12 V SF = {autonomy_sf:.2f} -> FAIL.", + f"Winter solar harvest = {PANEL_W:.0f} W x {WINTER_PEAK_SUN_HR} PSH x {PV_DERATE} derate = {harvest:.0f} Wh/day " + f"(all ASSUMED), vs {night_need_wh:.0f} Wh/night -> harvest is only ~{harvest_frac*100:.0f}% of a night's " + "energy, so even a bigger battery drains over successive nights without grid. The 400 W array cannot " + "sustain the DGX-dominated load off-grid; duty-cycling the DGX (run inference on demand, not idle all " + "night) is the highest-leverage fix.", + f"DGX thermal: {DGX_SPARK_W:.0f} W into a {enclosure_volume_m3():.0f} m^3 enclosure (footprint x " + f"{ENCLOSURE_WALL_HEIGHT_M} m, ASSUMED) at {AIR_EXCHANGE_ACH:.0f} natural ACH (ASSUMED) gives " + f"dT = Q/(rho*cp*Vdot) ~ {dT:.0f} K rise — enough to spoil local seeing and drive dew. Force ~10 ACH of " + "ventilation, or locate the DGX OUTSIDE the optical enclosure. cp_air 1005 J/kg-K, rho " + f"{P.SITE.air_density:.3f} kg/m^3 at 1800 m (DERIVED).", + "CONCLUSION (open question resolved): graceful shutdown is comfortably covered; full off-grid autonomy " + "on the specified 12 V / 400 W system is NOT — the assumed 170 W DGX Spark is the reason. Specify the " + "pack at 48 V and/or duty-cycle the DGX to make a winter night off-grid feasible.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/report.py b/design/mechanical/calc/report.py new file mode 100644 index 0000000..104705e --- /dev/null +++ b/design/mechanical/calc/report.py @@ -0,0 +1,212 @@ +""" +Report generator — assembles design/mechanical/MECHANICAL_DESIGN.md from the +calculator so every number in the report is *generated*, never hand-typed. + +Run: python3 -m design.mechanical.calc.report (writes the .md) +A test (test_report.py) regenerates and byte-compares, so the report can never +drift from the proofs. +""" + +from __future__ import annotations + +import pathlib + +from . import ( + balance, + bearings, + dynamics, + enclosure, + encoder, + pier, + power, + stiffness, + thermal, + torque, + wind, +) +from . import params as P +from .budget import Verdict + +# Ordered proof modules (each exposes evaluate()). +PROOFS = [ + ("1", torque), ("2", stiffness), ("3", dynamics), ("4", encoder), + ("5", balance), ("6", bearings), ("7", wind), ("8", pier), + ("9", thermal), ("10", enclosure), ("11", power), +] + +_HERE = pathlib.Path(__file__).resolve().parents[1] # design/mechanical/ +OUT = _HERE / "MECHANICAL_DESIGN.md" +TRADE_SECTION = _HERE / "tradestudy" / "SECTION.md" +CAD_SECTION = _HERE / "cad" / "SECTION.md" + + +FRONT_MATTER = """# NIGHTWATCH — Observatory Mechanical Design: Rigorous Proof-Out + +> **Generated artifact.** Every number below is computed by the `design/mechanical/calc/` +> package and regenerated by `python3 -m design.mechanical.calc.report`. The test +> `test_report.py` fails if this file drifts from the calculator, so the figures cannot +> silently disagree the way the repo's docs currently do. +> +> **Honesty labels.** Every input is tagged **S** (sourced — stated in a repo file), +> **D** (derived — computed here), or **A** (assumed — the repo is silent; an engineering +> assumption is stated). Nothing here is measured field data; the observatory has never been built. + +## 1. Why this exists + +The repo already documents a mechanical design — `NIGHTWATCH_Build_Package.md` (spec table, costed +BOM, concrete-pier spec) and a `pos/` panel of expert personas (C. Walton Musser on the harmonic +drives, Richard Hedrick on frame stiffness). But it is **assertion, not proof**: the headline +targets are stated, never calculated, and the single most load-bearing fact — which telescope — +contradicts itself. This report turns the prose into computed, self-consistent, margin-carrying +engineering, runs the selection permutations as a real weighted trade study, makes and defends the +bold choices the numbers justify, and fills the CAD and environmental-load voids. + +## 2. Resolved contradictions + +The repo carries numbers that disagree across files. This design pins each to one value in +`params.py` and records the conflict so it is fixed, not buried. + +| # | Contradiction (as found) | Resolution here | +|---|---|---| +| 1 | **OTA identity**: MN76 178 mm f/6 1068 mm **~9 kg** (Build Package) vs MN78 180 mm **f/8** 1440 mm **~14 kg** (`INTES_MICRO_HISTORY.md`, "selected") | Both carried as load cases (`MN76`, `MN78`); the torque + stiffness proofs and the trade study decide (see §Trade Study). | +| 2 | **Encoder PPR**: AMT103 "8192 PPR" everywhere vs "2048 PPR" (`HARDWARE_SETUP.md`) | Pinned to 8192 PPR motor-side; 2048 flagged as a doc error. | +| 3 | **On-axis resolution**: AS5600 "12-bit / 4096" vs `EncoderConfig.resolution=8192` | AS5600 is 12-bit → 4096 counts/rev = ~316 arcsec/count; the 8192 config default is unrelated and misleading. | +| 4 | **Horizon/altitude limit**: 10° (safety monitor) / 15° (`constants.py`) / 20° (scheduler) | Not a mechanical limit — advisory; the mount has no hard slew-altitude cutoff (a noted gap). | +| 5 | **Operating temperature**: −20…40 °C (YAML) vs 20…100 °F (code) | Reconciled to °C internally (−6.7…38 °C from the code Fahrenheit values). | +| 6 | **Site coordinates**: 38.9 / −117.4 (software) vs 39.0 / −117.0 (firmware) | Pinned to 38.9 °N / −117.4 °W, 1800 m for all load derivations. | +| 7 | **Bearings** called "angular contact" but specified as 6008/6006, which are **deep-groove** | Flagged; the bearings + stiffness proofs recommend 7008/7006 angular-contact pairs for moment stiffness. | + +## 3. The three governing voids (repo is silent — filled here, labelled ASSUMED) + +A 6000 ft, seismically-active, remote Nevada site is governed by three structural loads the repo +never states. The software encodes only *operational* interlocks (park at 25 mph, close at 35 mph +gust), which are **not** structural survival ratings. This design makes them first-class: + +| Void | Repo status | Assumed design load (A) | Governs | +|---|---|---|---| +| **Survival wind** | absent (only 25/35 mph operational) | ASCE 7 basic wind ~105 mph 3-sec gust, Risk Cat I | roof anchors, pier overturning | +| **Snow load** | absent entirely | ~25 psf ground snow, high-desert @ 6000 ft | closed-roof structure, roof slope | +| **Seismic** | absent entirely | S_DS ≈ 0.5 g (Walker Lane vicinity) | pier base shear, anchorage | + +Two more the repo flags as open and this design closes: **DGX Spark power/heat** (assumed ~170 W → +energy + enclosure-thermal budget) and the **power/autonomy budget** (solar+battery autonomy hours). +""" + + +def results_at_a_glance(results) -> str: + rows = ["## 4. Results at a glance", "", + "| # | Proof | Verdict | Governing number |", "|---|---|---|---|"] + for num, r in results: + rows.append(f"| {num} | {r.title} | {r.verdict.symbol} {r.verdict.value} | {r.headline} |") + passes = sum(1 for _, r in results if r.verdict is Verdict.PASS) + fails = sum(1 for _, r in results if r.verdict is Verdict.FAIL) + marg = sum(1 for _, r in results if r.verdict is Verdict.MARGINAL) + rows += ["", + f"**{passes} PASS · {marg} MARGINAL · {fails} FAIL.** The FAILs are the value: each is a " + "real gap in the repo's design that assertion had hidden — the bearing-limited stiffness, " + "the sub-arcsec-incapable encoder chain, the missing survival-wind anchors, passive focus " + "over the diurnal swing, and off-grid autonomy. Every FAIL carries a computed remedy below."] + return "\n".join(rows) + + +def proof_section(num: str, r) -> str: + parts = [f"### {num}. {r.title} — {r.verdict.symbol} {r.verdict.value}", "", + f"**{r.headline}**", ""] + if r.target: + parts.append(f"*Target:* {r.target} ") + if r.safety_factor is not None: + parts.append(f"*Governing safety factor:* {r.safety_factor:.2f}×") + parts += ["", r.markdown_table(), ""] + if r.assumptions: + parts.append("_Assumptions:_") + for a in r.assumptions: + parts.append(f"- {a}") + parts.append("") + return "\n".join(parts) + + +def provenance_table() -> str: + rows = ["## 6. Provenance ledger", "", + "Where each key input comes from — the honesty table the repo lacks.", "", + "| Parameter | Provenance | Source / basis |", "|---|:--:|---|"] + for key, (prov, src) in P.PROVENANCE.items(): + rows.append(f"| `{key}` | **{prov.marker}** | {src} |") + rows += ["", "**S** = sourced (repo file) · **D** = derived (computed) · **A** = assumed (repo silent)."] + return "\n".join(rows) + + +SYNTHESIS = """## 9. Selected configuration, cost delta, and roadmap + +The trade study's selected permutation, with the cost delta against the repo's **$8,305** BOM +(DGX excluded) and a build sequencing that ships a working v1 before the sub-arcsec v2. + +| Subsystem | Selected | Why | Cost delta | +|---|---|---|---| +| OTA | **MN78 f/8** (science override) | f/8 scale + 0.134 obstruction for Mars/lucky imaging; MN76 is the mechanical hedge | baseline | +| Topology | **Counterweight-FREE GEM** (bold) | torque proof SF 2.5/2.6; deletes 15.4 kg + 29% RA inertia | −$110 (no CW set) | +| Drive | NEMA17 + 27:1 + harmonic | PASS; planetary PE mooted by on-axis encoder | baseline | +| Encoder | **Hybrid → on-axis absolute** (mandatory) | baseline DISQUALIFIED (5.02″ RMS); on-axis ring 0.54″ | +$1,500–5,000 | +| Bearings | **Angular-contact 7008/7006** (fix) | deep-groove 6008/6006 are 98% of the stiffness FAIL | +$80 | +| Pier | Concrete Sonotube | PASS SF 6.8; not the constraint | baseline | +| Enclosure | **Roll-off + active thermal** (bold) | closes thermal FAIL + DGX heat | +$300 | +| Anchors | **4× 2 klbf hold-downs** (fix) | survival-wind uplift SF 0.19 → 3.9 | +$120 | +| Power | Grid + UPS (v1); **48 V pack** for autonomy | 12 V pack gives 3.3 h vs 10 h night | +$400 (v2) | + +**Roadmap.** +- **v1 (buildable now):** MN78 counterweight-free GEM on the concrete pier, roll-off roof with the + mandatory hold-down anchors and snow interlock, angular-contact bearings, grid + UPS power, + temperature-compensated focus. Meets every structural and thermal requirement; tracks to ~5″ + (motor-encoder + PE), which is fine for the wide-field/lucky-imaging use with plate-solve recentering. +- **v2 (sub-arcsecond):** add the on-axis absolute ring (closes the 1″ tracking target), active-thermal + enclosure, and — if precision is weighted above cost — the torque-motor direct drive. Off-grid + autonomy via the 48 V battery + duty-cycled DGX. + +## 10. Risk register + +| Risk | Severity | Mitigation (computed) | +|---|---|---| +| Frame stiffness FAIL (43″) | High | Angular-contact 7008/7006 back-to-back on max spans; verify with the stiffness proof once bearing stiffness is vendor-confirmed | +| Sub-arcsec unmet by baseline encoder | High | On-axis absolute ring (proof: 0.54″); v1 relies on plate-solve recentering | +| Survival-wind roof uplift (SF 0.19) | High | 4× 2 klbf hold-down anchors (SF 3.9) + snow interlock | +| Passive focus drift over diurnal swing | Medium | Temperature-compensated focuser (repo already has −2.5 steps/°C) | +| Off-grid autonomy shortfall | Medium | 48 V pack (13.4 h) or DGX duty-cycle; v1 stays on grid + UPS | +| Bearing-stiffness assumption (250/180 N/µm) | Medium | Confirm vendor radial stiffness; it scales the deflection linearly | +| DGX watts/heat unspecified (assumed 170 W) | Low | Ventilate or locate the DGX outside the OTA enclosure | + +--- + +_Generated by `design/mechanical/calc/report.py` from the proof modules. Regenerate with +`python3 -m design.mechanical.calc.report`; `test_report.py` guards against drift._ +""" + + +def build_markdown() -> str: + results = [(num, mod.evaluate()) for num, mod in PROOFS] + blocks = [FRONT_MATTER.rstrip(), "", results_at_a_glance(results), "", + "## 5. The proofs", ""] + for num, r in results: + blocks.append(proof_section(num, r)) + blocks.append(provenance_table()) + blocks.append("") + # Trade study (section 7) and CAD (section 8): read the versioned narratives verbatim, + # re-numbering their top heading into this document's outline. + trade = TRADE_SECTION.read_text().strip() + trade = trade.replace("## Trade Study (Phase B)", "## 7. Trade Study (Phase B)", 1) + cad = CAD_SECTION.read_text().strip() + cad = cad.replace("## Mechanical CAD (Phase D)", "## 8. Mechanical CAD (Phase D)", 1) + blocks.append(trade) + blocks.append("") + blocks.append(cad) + blocks.append("") + blocks.append(SYNTHESIS.strip()) + text = "\n".join(b for b in blocks if b is not None) + return text.rstrip() + "\n" + + +def main() -> None: + OUT.write_text(build_markdown()) + print(f"wrote {OUT} ({OUT.stat().st_size} bytes)") + + +if __name__ == "__main__": + main() diff --git a/design/mechanical/calc/stiffness.py b/design/mechanical/calc/stiffness.py new file mode 100644 index 0000000..8ea85a3 --- /dev/null +++ b/design/mechanical/calc/stiffness.py @@ -0,0 +1,232 @@ +""" +Static pointing-deflection proof — does the mount head hold the optical axis to +within P.DEFLECTION_TARGET_ARCSEC (5 arcsec) under gravity? + +Worst case: the OTA is horizontal (pointing at the horizon), so gravity acts +perpendicular to the optical axis and the payload weight hangs off the DEC axis +at its full CG lever. That gravity moment bends the load path, and every micron +of compliance at the tiny bearing spans (63-76 mm) is amplified into arcseconds +of pointing error. + +Load path modelled as a series compliance chain (each term adds pointing error): + + payload weight W --(lever L_cg)--> moment M at the DEC axis + |-- DEC housing box-beam bends theta = M*L / (E*I) + |-- DEC bearing pair deflects radially F = M/span, theta = 2*delta/span + |-- RA housing box-beam bends (overhung) theta = M*L / (E*I) + |-- RA bearing pair deflects radially F = M/span, theta = 2*delta/span + +Sum the four angular terms, convert rad -> arcsec, compare to the 5 arcsec +target for BOTH the 8 mm baseline housings and the 12 mm bold-review variants. + +Honest expectation: the deep-groove bearings the repo specifies, at the 63-76 mm +spans set by the housing depths, are the governing compliance and the beam-wall +upgrade (8 -> 12 mm) barely moves the number. This is where the repo's asserted +"< 5 arcsec" is finally computed rather than assumed. +""" + +from __future__ import annotations + +from . import params as P +from . import units as u +from .budget import BudgetResult, Verdict, verdict_from_sf + +# -------------------------------------------------------------------------- +# Modelled constants the repo does not provide (tagged in .assumptions too). +# -------------------------------------------------------------------------- +# Radial stiffness of the output bearings. The repo lists only load ratings +# (C, C0), not stiffness, so these are ASSUMED from typical small deep-groove +# ball-bearing radial stiffness: 6008 ~ 250 N/um, 6006 ~ 180 N/um. +BEARING_RADIAL_STIFFNESS_N_PER_M: dict[str, float] = { + "6008": 250.0e6, # 250 N/um (RA output) -- ASSUMED + "6006": 180.0e6, # 180 N/um (DEC output) -- ASSUMED +} +K_RA_BEARING = BEARING_RADIAL_STIFFNESS_N_PER_M["6008"] +K_DEC_BEARING = BEARING_RADIAL_STIFFNESS_N_PER_M["6006"] + +# The DEC rotation axis sits below the tube/CG plane by the dovetail + saddle +# stack. The repo does not dimension the saddle, so this radial offset is +# ASSUMED at 50 mm and added to the along-tube CG lever to form a worst-case +# combined gravity lever L_cg. DERIVED/ASSUMED geometry. +SADDLE_LEVER_M = u.mm(50.0) + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these directly). +# -------------------------------------------------------------------------- +def box_section_I(housing: P.Housing) -> float: + """Second moment of area of a rectangular box section (m^4). + + I = (b*h^3 - (b-2t)*(h-2t)^3)/12 for outer b x h and wall t. + """ + b, h, t = housing.outer_x_m, housing.outer_y_m, housing.wall_m + return (b * h ** 3 - (b - 2.0 * t) * (h - 2.0 * t) ** 3) / 12.0 + + +def payload_mass(ota: P.OTA) -> float: + """OTA tube + lumped rear imaging train (kg).""" + return ota.mass_kg + P.IMAGING_TRAIN.mass_kg + + +def cg_lever_m(ota: P.OTA) -> float: + """Worst-case gravity lever from the DEC axis to the payload CG (m). + + Along-tube CG offset (OTA.cg_from_saddle_m) + assumed saddle radial offset. + """ + return ota.cg_from_saddle_m + SADDLE_LEVER_M + + +def payload_moment_Nm(ota: P.OTA) -> float: + """Gravity moment about the DEC axis with the tube horizontal (N.m).""" + return u.weight_N(payload_mass(ota)) * cg_lever_m(ota) + + +def beam_angular_deflection_rad(M: float, housing: P.Housing) -> float: + """Tip rotation of a box cantilever of length = bearing span under end + moment M: theta = M*L/(E*I) (rad).""" + E = housing.material.E + I = box_section_I(housing) + return M * housing.depth_m / (E * I) + + +def bearing_angular_deflection_rad(M: float, span: float, k: float) -> float: + """Angular tilt from a bearing pair reacting moment M as a force couple. + + F = M/span in each bearing, radial deflection delta = F/k, and the housing + tilts by (delta + delta)/span = 2*delta/span (rad). + """ + F = M / span + delta = F / k + return 2.0 * delta / span + + +def deflection_breakdown(ota: P.OTA, ra_housing: P.Housing, + dec_housing: P.Housing) -> dict[str, float]: + """Every angular term (rad) plus the arcsec total for one configuration.""" + M = payload_moment_Nm(ota) + dec_beam = beam_angular_deflection_rad(M, dec_housing) + ra_beam = beam_angular_deflection_rad(M, ra_housing) + dec_brg = bearing_angular_deflection_rad(M, dec_housing.depth_m, K_DEC_BEARING) + ra_brg = bearing_angular_deflection_rad(M, ra_housing.depth_m, K_RA_BEARING) + total = dec_beam + ra_beam + dec_brg + ra_brg + return { + "moment_Nm": M, + "dec_beam_rad": dec_beam, + "ra_beam_rad": ra_beam, + "dec_bearing_rad": dec_brg, + "ra_bearing_rad": ra_brg, + "beam_rad": dec_beam + ra_beam, + "bearing_rad": dec_brg + ra_brg, + "total_rad": total, + "total_arcsec": u.rad_to_arcsec(total), + } + + +def total_deflection_arcsec(ota: P.OTA, ra_housing: P.Housing, + dec_housing: P.Housing) -> float: + """Summed static pointing deflection (arcsec) for the given configuration.""" + return deflection_breakdown(ota, ra_housing, dec_housing)["total_arcsec"] + + +# -------------------------------------------------------------------------- +# Translational helpers reused by dynamics.py (same beam/bearing model). +# -------------------------------------------------------------------------- +def beam_translational_stiffness_N_per_m(housing: P.Housing) -> float: + """Cantilever tip translational stiffness k = 3*E*I/L^3 (N/m).""" + E = housing.material.E + I = box_section_I(housing) + return 3.0 * E * I / housing.depth_m ** 3 + + +def bearing_pair_stiffness_N_per_m(k_single: float) -> float: + """Two bearings share a lateral load in parallel -> 2x radial stiffness.""" + return 2.0 * k_single + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def _sf_and_verdict(total_arcsec: float) -> tuple[float, Verdict]: + sf = P.DEFLECTION_TARGET_ARCSEC / total_arcsec + # PASS only with a real 1.5x pointing margin; MARGINAL if merely inside 5". + return sf, verdict_from_sf(sf, pass_at=1.5, marginal_at=1.0) + + +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + base = deflection_breakdown(ota, P.RA_HOUSING, P.DEC_HOUSING) + bold = deflection_breakdown(ota, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + + sf_base, v_base = _sf_and_verdict(base["total_arcsec"]) + sf_bold, v_bold = _sf_and_verdict(bold["total_arcsec"]) + + # The bold 12 mm build is the design recommendation -> governing verdict, + # but the headline reports both so a FAIL cannot hide behind the upgrade. + verdict = v_bold + tgt = P.DEFLECTION_TARGET_ARCSEC + + res = BudgetResult( + key="stiffness", + title="Static pointing deflection (OTA horizontal, worst case)", + verdict=verdict, + headline=( + f"{ota.name}: 8 mm baseline {base['total_arcsec']:.1f}\" " + f"({v_base.value}), 12 mm bold {bold['total_arcsec']:.1f}\" " + f"({v_bold.value}) vs {tgt:.0f}\" target -- bearing compliance at the " + f"{dec_span_mm(P.DEC_HOUSING):.1f}/{ra_span_mm(P.RA_HOUSING):.1f} mm " + f"spans governs." + ), + target=f"Summed gravity deflection < {tgt:.0f} arcsec (Hedrick), SF>=1.5 to PASS", + safety_factor=sf_bold, + ) + res.add("Payload mass (OTA + train)", payload_mass(ota), "kg") + res.add("Gravity lever L_cg (cg + saddle)", cg_lever_m(ota), "m", + "along-tube CG + assumed 50 mm saddle offset") + res.add("Gravity moment at DEC axis", base["moment_Nm"], "Nm", "W x L_cg, tube horizontal") + res.add("-- 8 mm baseline housings --", 0.0, "", "") + res.add("DEC housing beam deflection", u.rad_to_arcsec(base["dec_beam_rad"]), "arcsec") + res.add("RA housing beam deflection", u.rad_to_arcsec(base["ra_beam_rad"]), "arcsec") + res.add("DEC bearing-pair tilt", u.rad_to_arcsec(base["dec_bearing_rad"]), "arcsec", + "6006 @ 180 N/um, 63.5 mm span") + res.add("RA bearing-pair tilt", u.rad_to_arcsec(base["ra_bearing_rad"]), "arcsec", + "6008 @ 250 N/um, 76.2 mm span") + res.add("8 mm TOTAL deflection", base["total_arcsec"], "arcsec") + res.add("8 mm target / actual (SF)", sf_base, "x", v_base.value) + res.add("-- 12 mm bold housings --", 0.0, "", "") + res.add("12 mm beam deflection (DEC+RA)", u.rad_to_arcsec(bold["beam_rad"]), "arcsec") + res.add("12 mm bearing tilt (DEC+RA)", u.rad_to_arcsec(bold["bearing_rad"]), "arcsec", + "unchanged -- wall does not stiffen bearings") + res.add("12 mm TOTAL deflection", bold["total_arcsec"], "arcsec") + res.add("12 mm target / actual (SF)", sf_bold, "x", v_bold.value) + res.add("Bearing share of total (8 mm)", 100.0 * base["bearing_rad"] / base["total_rad"], "%") + + res.assumptions = [ + "Worst case: OTA horizontal, gravity perpendicular to the optical axis (SOURCED as the design case).", + f"L_cg = OTA.cg_from_saddle ({ota.cg_from_saddle_m*1000:.0f} mm) + saddle offset " + f"{SADDLE_LEVER_M*1000:.0f} mm (saddle undimensioned in repo, ASSUMED).", + "Bearing radial stiffness 6008~250 N/um, 6006~180 N/um (repo lists only load " + "ratings, not stiffness) -- ASSUMED.", + "Bearing span = housing depth (bearings at the box faces): DEC 63.5 mm, RA 76.2 mm.", + "The full payload moment is applied through BOTH the DEC and RA load paths " + "(overhung bending on the RA bearings persists even when torque-balanced) -- " + "a bounding assumption.", + "Deep-groove bearings modelled; the repo labels them 'angular contact' (flagged " + "in params) -- a preloaded angular-contact pair would raise stiffness materially.", + ] + return res + + +def ra_span_mm(h: P.Housing) -> float: + return h.depth_m * 1000.0 + + +def dec_span_mm(h: P.Housing) -> float: + return h.depth_m * 1000.0 + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) + for a in r.assumptions: + print(" -", a) diff --git a/design/mechanical/calc/thermal.py b/design/mechanical/calc/thermal.py new file mode 100644 index 0000000..8c93b18 --- /dev/null +++ b/design/mechanical/calc/thermal.py @@ -0,0 +1,245 @@ +""" +Thermal focus-stability proof — over the site's diurnal temperature swing, does +the focus hold PASSIVELY, or is an active temperature-compensated focuser +required? And can a small dew heater keep the corrector above dewpoint? + +Physics +------- +Depth of focus (the axial in-focus window, one-sided): + + DoF = +/- 2 * lambda * N**2 (lambda = wavelength, N = f-ratio) + +The f/8 MN78 has a LARGER DoF than the f/6 MN76 (DoF scales as N^2), which is a +genuine point in the slower system's favour. + +Tube-expansion defocus. The 6061-T6 aluminium tube is the metering structure +that separates the primary mirror cell from the focuser. When it grows by +dL = CTE_al * L_tube * dT the focuser (and the sensor bolted to it) walks away +from the fixed image plane by ~dL to first order (a Newtonian/Mak-Newt has no +strong secondary magnification, so the primary-to-focuser spacing maps ~1:1 to +defocus). We use the *tube length* rather than the focal length as the aluminium +metering length: it is the physical aluminium that expands, and for the folded +Mak-Newts the tube (700 / 1400 mm) is the honest — and less alarming — figure +(focal length would be 1068 / 1440 mm). + + defocus_per_K = CTE_al * L_tube + +Astrositall mirror. The mirror substrate CTE (~1.5e-7 /K) is ~157x smaller than +aluminium, so the mirror's own focal-shift term (f * CTE_mirror * dT) is +negligible: the ENTIRE thermal focus error is the aluminium tube. That is the +whole reason the primary is low-expansion glass. + +Dew. Heater power to hold the corrector a few K above dewpoint, P = h*A*dT with +h a lumped convective+radiative coefficient and A the corrector area from the +aperture; compared against a 2 W all-sky ring reference. + +Conclusion (headline): over the ~22 K diurnal swing the aluminium tube walks the +focus by ~9-10 depths of focus (only ~2 K to leave best focus), so PASSIVE focus +is NOT enough — a temperature-compensated focuser is REQUIRED. The repo's focuser +already carries a temperature coefficient (-2.5 steps/C), so the fix exists; this +proof shows it is not optional. +""" + +from __future__ import annotations + +import math + +from . import params as P +from .budget import BudgetResult, verdict_from_sf + +# -------------------------------------------------------------------------- +# Modelled constants (the repo's params.py is silent on these). +# -------------------------------------------------------------------------- +# The aluminium tube is the metering structure; its CTE comes from params. +TUBE_MATERIAL_KEY = "6061-T6" + +# Lumped heat-transfer coefficient corrector-glass -> still night air (natural +# convection + radiation to sky). 5-15 W/m^2K is the usual still-air band; 10 is +# a central ASSUMED value. ASSUMED. +HEAT_TRANSFER_COEFF_WM2K = 10.0 + +# Hold the corrector this many K above the dewpoint to suppress dew. ASSUMED. +DEW_MARGIN_K = 5.0 + +# Reference dew-heater ring power to compare against. A small all-sky / corrector +# dew ring is ~2 W. Stated engineering reference figure. ASSUMED (reference). +ALL_SKY_RING_W = 2.0 + +# Repo focuser default temperature coefficient (steps/C). NOT in params.py — it +# lives in the focuser service (tests/unit/test_focuser_service.py default +# -2.5; docs/API + telescope_tools note ~2.5 steps/C). SOURCED (repo, elsewhere). +FOCUSER_TEMP_COEFF_STEPS_PER_C = -2.5 + +# The POS retreat sim models MN78 defocus as 2.0 microns/C +# (pos/POS_RETREAT_SIMULATION.md TEMP_COEFFICIENT). First principles below give +# ~33 um/K for a BARE aluminium tube -> the POS figure implicitly assumes a +# low-CTE (carbon / compensated) tube or is optimistic. SOURCED (reference). +POS_TEMP_COEFF_UM_PER_C = 2.0 + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these). +# -------------------------------------------------------------------------- +def depth_of_focus_um(ota: P.OTA) -> float: + """ + One-sided depth of focus (+/-), micrometres: DoF = 2 * lambda * N^2. + The full in-focus window is twice this value. Grows as N^2, so the slower + f/8 tube tolerates more axial error than the f/6. + """ + return 2.0 * P.OPTICAL.wavelength_m * ota.f_ratio ** 2 * 1.0e6 + + +def defocus_per_kelvin_um(ota: P.OTA) -> float: + """ + Focus shift per kelvin from the aluminium tube, micrometres/K: + defocus_per_K = CTE_al * L_tube (1:1 tube-growth -> defocus, first order). + """ + cte_al = P.MATERIALS[TUBE_MATERIAL_KEY].cte + return cte_al * ota.tube_length_m * 1.0e6 + + +def tube_defocus_um(ota: P.OTA, delta_t_c: float) -> float: + """Total aluminium-tube defocus (micrometres) over a temperature change.""" + return defocus_per_kelvin_um(ota) * delta_t_c + + +def kelvin_to_defocus(ota: P.OTA) -> float: + """ + Kelvin of tube temperature change to walk from best focus to the EDGE of the + depth of focus, i.e. "how many K before out of focus": + K_edge = DoF / defocus_per_K + A small number here means passive focus is fragile. + """ + return depth_of_focus_um(ota) / defocus_per_kelvin_um(ota) + + +def mirror_defocus_per_kelvin_um(ota: P.OTA) -> float: + """ + Astrositall primary's own focus shift per kelvin, micrometres/K: + ~ f * CTE_mirror (the radius, hence focal length, scales with CTE_mirror). + Uses the system focal length as a conservative (upper-bound) proxy for the + primary's focal length. Should be ~2 orders of magnitude below the tube term. + """ + return P.OPTICAL.astrositall_cte * ota.focal_length_m * 1.0e6 + + +def corrector_area_m2(ota: P.OTA) -> float: + """Corrector clear area from the aperture (m^2). The meniscus glass is a few + percent larger than the clear aperture, so this slightly under-estimates.""" + return math.pi * (ota.aperture_m / 2.0) ** 2 + + +def dew_heater_power_w( + ota: P.OTA, + h: float = HEAT_TRANSFER_COEFF_WM2K, + dT: float = DEW_MARGIN_K, +) -> float: + """Steady heater power to hold the corrector dT above ambient: P = h*A*dT.""" + return h * corrector_area_m2(ota) * dT + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + swing = P.ENV.diurnal_swing_c + + # Depth of focus, both cases (the f/8 vs f/6 comparison is a design point). + dof_mn76 = depth_of_focus_um(P.MN76) + dof_mn78 = depth_of_focus_um(P.MN78) + dof = depth_of_focus_um(ota) + + per_k = defocus_per_kelvin_um(ota) + k_edge = kelvin_to_defocus(ota) # K to leave best focus + defocus_swing = tube_defocus_um(ota, swing) # um over the full swing + n_dof = defocus_swing / dof # how many DoF the tube walks + + mir_per_k = mirror_defocus_per_kelvin_um(ota) + mir_swing = mir_per_k * swing + tube_over_mirror = per_k / mir_per_k + + heater_w = dew_heater_power_w(ota) + + # Passive-focus safety factor = allowable dT (to edge of DoF) / actual swing. + # < 1 means the swing blows through the depth of focus -> passive fails. + passive_sf = k_edge / swing + verdict = verdict_from_sf(passive_sf, pass_at=2.0, marginal_at=1.0) + + res = BudgetResult( + key="thermal", + title="Thermal focus stability over the diurnal swing", + verdict=verdict, + headline=( + f"Passive focus is NOT enough: over the {swing:.0f} K diurnal swing the 6061-T6 tube " + f"walks focus ~{n_dof:.0f}x the f/{ota.f_ratio:.0f} depth of focus (only {k_edge:.1f} K " + f"to leave best focus) -> a temperature-compensated focuser is REQUIRED (the repo focuser " + f"has a {FOCUSER_TEMP_COEFF_STEPS_PER_C:.1f} steps/C coefficient). The Astrositall mirror " + f"adds only ~{mir_swing:.0f} um ({100.0/tube_over_mirror:.1f}% of the tube term); a " + f"~{heater_w:.1f} W corrector heater beats the {ALL_SKY_RING_W:.0f} W all-sky ring." + ), + target=f"Passive focus stays within +/- DoF over the {swing:.0f} K swing (SF = K_to_edge / swing)", + safety_factor=passive_sf, + ) + + # Depth of focus (f/8 wins). + res.add("Depth of focus, MN76 f/6 (+/-)", dof_mn76, "um", "2*lambda*N^2") + res.add("Depth of focus, MN78 f/8 (+/-)", dof_mn78, "um", "larger -> f/8 more forgiving") + res.add("DoF ratio f8/f6", dof_mn78 / dof_mn76, "x", "~(8/6)^2 = 1.78 (scales as N^2)") + # Tube-driven defocus for the selected OTA. + res.add(f"Tube length ({ota_key})", ota.tube_length_m, "m", "aluminium metering length (choice)") + res.add("Aluminium CTE", P.MATERIALS[TUBE_MATERIAL_KEY].cte, "1/K", "6061-T6 (params)") + res.add("Tube defocus per K", per_k, "um/K", "CTE_al * L_tube") + res.add("K to leave best focus", k_edge, "K", "DoF / (defocus per K)") + res.add("Diurnal swing", swing, "K", "P.ENV.diurnal_swing_c") + res.add("Tube defocus over swing", defocus_swing, "um", f"= {n_dof:.1f} x DoF") + res.add("Passive-focus SF", passive_sf, "x", "K_to_edge / swing (<1 => fails)") + # Mirror residual (the big win). + res.add("Astrositall CTE", P.OPTICAL.astrositall_cte, "1/K", "~157x below aluminium") + res.add("Mirror defocus per K", mir_per_k, "um/K", "f * CTE_mirror") + res.add("Mirror defocus over swing", mir_swing, "um", "negligible vs tube") + res.add("Tube / mirror defocus ratio", tube_over_mirror, "x", "aluminium dominates") + # Cross-check against the POS coefficient. + res.add("POS modelled coeff (MN78)", POS_TEMP_COEFF_UM_PER_C, "um/K", + "vs first-principles bare-Al tube -> optimistic") + # Dew heater. + res.add("Corrector area (from aperture)", corrector_area_m2(ota), "m^2", "pi*(D/2)^2") + res.add(f"Dew heater P (hold +{DEW_MARGIN_K:.0f} K)", heater_w, "W", + f"h={HEAT_TRANSFER_COEFF_WM2K:.0f} W/m^2K") + res.add("All-sky ring reference", ALL_SKY_RING_W, "W", "heater fits inside this") + + res.assumptions = [ + "Depth of focus DoF = 2*lambda*N^2 (one-sided, +/-), lambda = P.OPTICAL.wavelength_m " + "(0.55 um); the full in-focus window is twice this. DoF scales as N^2 so f/8 > f/6.", + "Tube-expansion defocus uses the TUBE LENGTH (P.OTA.tube_length_m) as the aluminium " + "metering length, not the focal length: it is the physical aluminium between the primary " + "cell and the focuser, and for these folded Mak-Newts (700/1400 mm tube vs 1068/1440 mm " + "focal) it is the more physical and less alarming choice. DERIVED.", + "Tube growth maps ~1:1 to defocus (no strong secondary magnification in a Mak-Newt); a " + "catadioptric focus-amplification factor would make passive focus WORSE, so 1:1 is " + "non-conservative-favourable and the FAIL verdict is robust. ASSUMED.", + "Mirror term ~ f * CTE_mirror with CTE_mirror = P.OPTICAL.astrositall_cte (1.5e-7/K); " + "system focal length used as an upper-bound proxy for the primary's focal length. DERIVED.", + f"Dew heater P = h*A*dT with h = {HEAT_TRANSFER_COEFF_WM2K:.0f} W/m^2K (still-air " + f"convection+radiation, ASSUMED), dT = {DEW_MARGIN_K:.0f} K above dewpoint (ASSUMED), " + "A = corrector area from the clear aperture (meniscus is a few % larger -> mild under-estimate).", + f"All-sky dew ring reference = {ALL_SKY_RING_W:.0f} W (stated reference figure, ASSUMED).", + f"Focuser temperature coefficient {FOCUSER_TEMP_COEFF_STEPS_PER_C:.1f} steps/C is SOURCED from " + "the repo focuser service (tests/unit/test_focuser_service.py default; docs/telescope_tools " + "note ~2.5 steps/C) but is NOT in params.py; a focuser step size would be needed to convert " + "steps -> microns and confirm the compensation resolves the tube term. NOT in params (see notes).", + f"FINDING: first-principles bare-6061-T6 defocus is ~{per_k:.0f} um/K, whereas the POS retreat " + f"sim models MN78 at {POS_TEMP_COEFF_UM_PER_C:.0f} um/K " + "(pos/POS_RETREAT_SIMULATION.md TEMP_COEFFICIENT) -> the POS figure implies a low-CTE " + "(carbon-fibre / compensated) tube or is ~16x optimistic for bare aluminium. SOURCED cross-check.", + "CONCLUSION: passive focus FAILS the diurnal swing by ~10x; a temperature-compensated focuser " + "(which the repo has) is REQUIRED, not optional. The Astrositall primary and a <2 W corrector " + "heater are genuine wins that make the residual tractable once the tube term is compensated.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/torque.py b/design/mechanical/calc/torque.py new file mode 100644 index 0000000..07e2ea1 --- /dev/null +++ b/design/mechanical/calc/torque.py @@ -0,0 +1,127 @@ +""" +Axis torque budget — does each harmonic drive have the holding + dynamic torque +to carry the payload, and can the ~9 kg of counterweights be deleted? + +Worst-case axis torque = gravity imbalance + wind + goto inertia + drive/bearing +friction, compared against the CSF drives' rated (continuous) and peak torques. + +Headline decision: a strain-wave drive resists back-drive, so a *counterweight- +free* GEM only needs holding torque >= payload_weight x CG-offset. We compute +that and show the safety factor vs the 127 Nm (RA) / 70 Nm (DEC) ratings — the +same principle the ZWO AM5 / RST-135 exploit to run without counterweights. +""" + +from __future__ import annotations + +import math + +from . import params as P +from . import units as u +from .budget import BudgetResult, verdict_from_sf + +# Modelled geometry (DERIVED estimates; the pier/stiffness modules refine these). +RA_CG_OFFSET_M = 0.20 # horizontal CG offset of payload from the polar axis +DEC_RESIDUAL_LEVER_M = 0.02 # along-tube imbalance left after balancing +DEC_WIND_LEVER_FRAC = 0.34 # center-of-pressure lever about DEC as frac of tube length +BALANCE_ERROR_FRAC = 0.02 # residual imbalance when counterweighted & balanced +FRICTION_FRACTION = 0.05 # harmonic + bearing seal drag, frac of rated torque + + +def _drag_force(V_ms: float, ota: P.OTA, env: P.Environment, site: P.Site) -> float: + """Broadside wind drag on the tube (N). wind.py owns the structural version.""" + area = ota.tube_od_m * ota.tube_length_m + return 0.5 * site.air_density * V_ms ** 2 * env.cd_cylinder * area + + +def _payload_mass(ota: P.OTA) -> float: + return ota.mass_kg + P.IMAGING_TRAIN.mass_kg + + +def ra_required_torque(ota: P.OTA, counterweight_free: bool) -> dict[str, float]: + W = u.weight_N(_payload_mass(ota)) + if counterweight_free: + t_gravity = W * RA_CG_OFFSET_M + else: + t_gravity = W * RA_CG_OFFSET_M * BALANCE_ERROR_FRAC + f_wind = _drag_force(P.ENV.wind_gust_close_ms, ota, P.ENV, P.SITE) + t_wind = f_wind * RA_CG_OFFSET_M + # Inertia about polar axis (payload ~ point mass at offset + tube self-inertia). + i_axis = _payload_mass(ota) * RA_CG_OFFSET_M ** 2 + \ + (1.0 / 12.0) * ota.mass_kg * ota.tube_length_m ** 2 + alpha = math.radians(P.MOTOR.accel_dps2) + t_inertia = i_axis * alpha + t_friction = FRICTION_FRACTION * P.RA_DRIVE.rated_torque_Nm + total = t_gravity + t_wind + t_inertia + t_friction + return {"gravity": t_gravity, "wind": t_wind, "inertia": t_inertia, + "friction": t_friction, "total": total} + + +def dec_required_torque(ota: P.OTA) -> dict[str, float]: + W = u.weight_N(_payload_mass(ota)) + t_gravity = W * DEC_RESIDUAL_LEVER_M + f_wind = _drag_force(P.ENV.wind_gust_close_ms, ota, P.ENV, P.SITE) + t_wind = f_wind * DEC_WIND_LEVER_FRAC * ota.tube_length_m + i_axis = (1.0 / 12.0) * ota.mass_kg * ota.tube_length_m ** 2 + \ + P.IMAGING_TRAIN.mass_kg * (0.5 * ota.tube_length_m) ** 2 + alpha = math.radians(P.MOTOR.accel_dps2) + t_inertia = i_axis * alpha + t_friction = FRICTION_FRACTION * P.DEC_DRIVE.rated_torque_Nm + total = t_gravity + t_wind + t_inertia + t_friction + return {"gravity": t_gravity, "wind": t_wind, "inertia": t_inertia, + "friction": t_friction, "total": total} + + +def evaluate(ota_key: str = "MN78", counterweight_free: bool = True) -> BudgetResult: + ota = P.OTA_CASES[ota_key] + ra = ra_required_torque(ota, counterweight_free) + dec = dec_required_torque(ota) + + ra_sf_rated = P.RA_DRIVE.rated_torque_Nm / ra["total"] + ra_sf_peak = P.RA_DRIVE.peak_torque_Nm / ra["total"] + dec_sf_rated = P.DEC_DRIVE.rated_torque_Nm / dec["total"] + dec_sf_peak = P.DEC_DRIVE.peak_torque_Nm / dec["total"] + + governing_sf = min(ra_sf_rated, dec_sf_rated) + verdict = verdict_from_sf(governing_sf, pass_at=2.0, marginal_at=1.5) + + mode = "counterweight-FREE" if counterweight_free else "counterweighted" + res = BudgetResult( + key="torque", + title="Axis torque budget", + verdict=verdict, + headline=( + f"{ota.name}, {mode}: RA needs {ra['total']:.1f} Nm vs " + f"{P.RA_DRIVE.rated_torque_Nm:.0f} Nm rated (SF {ra_sf_rated:.1f}); " + f"DEC needs {dec['total']:.1f} Nm vs {P.DEC_DRIVE.rated_torque_Nm:.0f} Nm rated " + f"(SF {dec_sf_rated:.1f})." + ), + target="Required axis torque < rated (SF>=2 continuous, peak covers goto)", + safety_factor=governing_sf, + ) + res.add("Payload mass (OTA + train)", _payload_mass(ota), "kg") + res.add("RA gravity imbalance torque", ra["gravity"], "Nm", "payload weight x CG offset") + res.add("RA wind torque @ 35 mph gust", ra["wind"], "Nm") + res.add("RA goto-inertia torque", ra["inertia"], "Nm") + res.add("RA friction torque", ra["friction"], "Nm") + res.add("RA total required", ra["total"], "Nm") + res.add("RA rated / required (SF)", ra_sf_rated, "x") + res.add("RA peak / required (SF)", ra_sf_peak, "x") + res.add("DEC total required", dec["total"], "Nm") + res.add("DEC rated / required (SF)", dec_sf_rated, "x") + res.add("DEC peak / required (SF)", dec_sf_peak, "x") + res.assumptions = [ + f"RA CG offset from polar axis = {RA_CG_OFFSET_M*1000:.0f} mm (GEM geometry estimate, DERIVED)", + f"Balance residual (counterweighted) = {BALANCE_ERROR_FRAC*100:.0f}% (ASSUMED)", + f"Drive+bearing friction = {FRICTION_FRACTION*100:.0f}% of rated torque (ASSUMED)", + f"DEC wind center-of-pressure at {DEC_WIND_LEVER_FRAC*100:.0f}% of tube length " + f"from the DEC pivot (ASSUMED, conservative; governs the DEC total).", + "Wind torque uses the 35 mph emergency-close gust (max wind while open), SOURCED.", + f"Air density {P.SITE.air_density:.3f} kg/m^3 at 1800 m (DERIVED), 17% below sea level.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) diff --git a/design/mechanical/calc/units.py b/design/mechanical/calc/units.py new file mode 100644 index 0000000..6651353 --- /dev/null +++ b/design/mechanical/calc/units.py @@ -0,0 +1,170 @@ +""" +Units, physical constants, and provenance tagging for the NIGHTWATCH +mechanical-design calculator. + +Everything in this package computes in **SI base units** (metre, kilogram, +second, newton, pascal, kelvin, radian). Conversion helpers below turn the +imperial / astronomy figures quoted in the repo's docs into SI so there is a +single internal system and the numbers can no longer disagree file-to-file. + +Provenance +---------- +Every input parameter in ``params.py`` is tagged with a ``Provenance`` so the +report can honestly separate what the repo actually specifies from what we +derived or had to assume (the repo is silent on survival wind, snow, seismic, +and DGX power — those are ASSUMED and labelled as such). +""" + +from __future__ import annotations + +import enum +import math + +# -------------------------------------------------------------------------- +# Provenance +# -------------------------------------------------------------------------- + + +class Provenance(enum.Enum): + """Where a parameter's value comes from.""" + + SOURCED = "sourced" # stated in the repo (a doc, config, or firmware file) + DERIVED = "derived" # computed from sourced values inside this package + ASSUMED = "assumed" # repo is silent; a stated engineering assumption + + @property + def marker(self) -> str: + return {"sourced": "S", "derived": "D", "assumed": "A"}[self.value] + + +# -------------------------------------------------------------------------- +# Length +# -------------------------------------------------------------------------- +IN_TO_M = 0.0254 +FT_TO_M = 0.3048 +MM_TO_M = 1.0e-3 + + +def inch(x: float) -> float: + return x * IN_TO_M + + +def foot(x: float) -> float: + return x * FT_TO_M + + +def mm(x: float) -> float: + return x * MM_TO_M + + +# -------------------------------------------------------------------------- +# Mass / force +# -------------------------------------------------------------------------- +LB_TO_KG = 0.45359237 +G0 = 9.80665 # m/s^2, standard gravity + + +def lb(x: float) -> float: + return x * LB_TO_KG + + +def weight_N(mass_kg: float) -> float: + """Gravitational force (newtons) of a mass at standard gravity.""" + return mass_kg * G0 + + +# -------------------------------------------------------------------------- +# Speed +# -------------------------------------------------------------------------- +MPH_TO_MS = 0.44704 + + +def mph(x: float) -> float: + return x * MPH_TO_MS + + +# -------------------------------------------------------------------------- +# Pressure +# -------------------------------------------------------------------------- +PSI_TO_PA = 6894.757 +PSF_TO_PA = 47.880259 # pounds per square foot -> pascal + + +def psi(x: float) -> float: + return x * PSI_TO_PA + + +def psf(x: float) -> float: + return x * PSF_TO_PA + + +# -------------------------------------------------------------------------- +# Angle (astronomy) +# -------------------------------------------------------------------------- +ARCSEC_PER_REV = 360.0 * 3600.0 # 1_296_000 +ARCSEC_PER_RAD = 180.0 * 3600.0 / math.pi # ~206264.806 + + +def rad_to_arcsec(theta_rad: float) -> float: + return theta_rad * ARCSEC_PER_RAD + + +def arcsec_to_rad(theta_arcsec: float) -> float: + return theta_arcsec / ARCSEC_PER_RAD + + +def deg_to_rad(deg: float) -> float: + return math.radians(deg) + + +# -------------------------------------------------------------------------- +# Temperature +# -------------------------------------------------------------------------- +def f_to_c(f: float) -> float: + return (f - 32.0) * 5.0 / 9.0 + + +def c_to_k(c: float) -> float: + return c + 273.15 + + +# -------------------------------------------------------------------------- +# Atmosphere (International Standard Atmosphere, troposphere) +# -------------------------------------------------------------------------- +def isa_air_density(altitude_m: float, sea_level_density: float = 1.225) -> float: + """ + ISA air density (kg/m^3) at a geopotential altitude. Central Nevada sits at + ~1800 m, where air is ~17% thinner than sea level — this materially reduces + wind drag and is normally ignored in amateur mount design. DERIVED. + """ + # rho = rho0 * (1 - L*h/T0)^(g*M/(R*L) - 1), troposphere lapse form. + return sea_level_density * (1.0 - 2.25577e-5 * altitude_m) ** 4.25588 + + +__all__ = [ + "ARCSEC_PER_RAD", + "ARCSEC_PER_REV", + "FT_TO_M", + "G0", + "IN_TO_M", + "LB_TO_KG", + "MM_TO_M", + "MPH_TO_MS", + "PSF_TO_PA", + "PSI_TO_PA", + "Provenance", + "arcsec_to_rad", + "c_to_k", + "deg_to_rad", + "f_to_c", + "foot", + "inch", + "isa_air_density", + "lb", + "mm", + "mph", + "psf", + "psi", + "rad_to_arcsec", + "weight_N", +] diff --git a/design/mechanical/calc/wind.py b/design/mechanical/calc/wind.py new file mode 100644 index 0000000..3da6b56 --- /dev/null +++ b/design/mechanical/calc/wind.py @@ -0,0 +1,240 @@ +""" +Wind structural-load proof — THE authoritative wind calculation for the whole +package (torque.py deliberately defers its own drag helper to this module). + +Two regimes, two very different jobs: + + 1. OPERATIONAL (roof OPEN, telescope exposed). The safety monitor parks at + 25 mph and emergency-closes at 35 mph, so 35 mph is the most wind the OTA + ever sees while pointing. The broadside drag on the tube and the moment it + puts into the pier top are what torque.py and pier.py consume for the + tracking / pointing budgets. These loads are small. + + 2. SURVIVAL (roof CLOSED, telescope shielded). The governing structural case + is the roll-off enclosure at the ASCE-7 basic wind speed (105 mph, ASSUMED + — the repo is silent). A near-flat roof develops large net UPLIFT, and the + tall box develops large lateral drag. This is where the repo has a genuine + void: no roof hold-down or enclosure anchorage is specified, and the roof's + own weight resists only a fraction of the uplift. + +Dynamic pressure throughout: q = 1/2 rho V^2 with rho = P.SITE.air_density (ISA +at 1800 m, ~1.03 kg/m^3 — 16% below sea level, which honestly *reduces* every +load below the usual sea-level assumption). + +Honest expectation encoded below: SURVIVAL WIND GOVERNS, and the roll-off roof +FAILS to resist its own uplift by dead weight (SF ~0.19) — hold-down anchors are +mandatory and unspecified. That FAIL is the finding worth surfacing, exactly the +kind of structural void params.py flags survival wind as. +""" + +from __future__ import annotations + +from . import params as P +from . import units as u +from .budget import BudgetResult, verdict_from_sf + +# -------------------------------------------------------------------------- +# Modelled geometry / coefficients the repo does not provide. +# (All tagged again in result.assumptions.) +# -------------------------------------------------------------------------- +# OTA optical-axis height above the pier top (mount head build height). The repo +# dimensions neither the head nor the saddle stack, so this is ASSUMED. Used only +# to turn the operational drag force into an overturning moment. +MOUNT_HEAD_HEIGHT_M = 0.50 # ASSUMED + +# Roll-off enclosure box (footprint = roof span x length from params, height and +# cladding mass are unspecified -> ASSUMED). +WALL_HEIGHT_M = 2.4 # ASSUMED enclosure wall height +WALL_AREAL_MASS_KG_M2 = 12.0 # ASSUMED light steel panel + framing +WALL_FORCE_COEFF = 1.2 # ASSUMED net force coefficient, low-rise box (windward+leeward) + +# Hold-down anchorage used to demonstrate a *remediation*, since the repo omits +# it entirely. A common 1/2" wedge anchor into 4 ksi concrete carries ~2000 lbf +# allowable; four (one per roof corner) is the minimum credible scheme. +ROOF_ANCHOR_ALLOW_N = u.weight_N(u.lb(2000.0)) # ASSUMED ~2000 lbf allowable tension per anchor +ROOF_ANCHOR_COUNT = 4 # ASSUMED (one per roof corner) + + +# -------------------------------------------------------------------------- +# Pure helpers (the tests call these directly). +# -------------------------------------------------------------------------- +def dynamic_pressure_Pa(V_ms: float, site: P.Site = P.SITE) -> float: + """Stagnation dynamic pressure q = 1/2 rho V^2 (Pa) at the site air density.""" + return 0.5 * site.air_density * V_ms ** 2 + + +def tube_area_m2(ota: P.OTA) -> float: + """Broadside projected area of the closed tube (m^2) = OD x length.""" + return ota.tube_od_m * ota.tube_length_m + + +def drag_force_N(V_ms: float, ota: P.OTA, + env: P.Environment = P.ENV, site: P.Site = P.SITE) -> float: + """Broadside wind drag on the OTA tube (N): F = q * Cd * A. + + This is the single authoritative drag used across the package (torque.py's + private copy uses the identical formula and parameters). + """ + return dynamic_pressure_Pa(V_ms, site) * env.cd_cylinder * tube_area_m2(ota) + + +def ota_wind_moment_Nm(V_ms: float, ota: P.OTA, + pier: P.Pier = P.PIER) -> float: + """Overturning moment at the pier BASE from OTA drag (N.m). + + Lever = exposed pier height + mount-head height (OTA optical axis above the + pier top). The moment resisted at the pier *top* alone is F*mount_head. + """ + lever = pier.height_above_m + MOUNT_HEAD_HEIGHT_M + return drag_force_N(V_ms, ota) * lever + + +def roof_area_m2(enc: P.Enclosure = P.ENCLOSURE) -> float: + return enc.roof_span_m * enc.roof_length_m + + +def roof_uplift_N(env: P.Environment = P.ENV, enc: P.Enclosure = P.ENCLOSURE, + site: P.Site = P.SITE) -> float: + """Gross survival wind uplift on the (near-flat) roll-off roof (N): + U = q_survival * GCp_uplift * A_roof.""" + q = dynamic_pressure_Pa(env.survival_wind_ms, site) + return q * env.roof_uplift_gcp * roof_area_m2(enc) + + +def roof_weight_N(enc: P.Enclosure = P.ENCLOSURE) -> float: + return u.weight_N(enc.roof_mass_kg) + + +def roof_net_uplift_N() -> float: + """Uplift the anchors must resist after crediting roof dead weight (N).""" + return roof_uplift_N() - roof_weight_N() + + +def enclosure_wall_area_m2(enc: P.Enclosure = P.ENCLOSURE) -> float: + """Windward wall projected area (m^2) = roof length x assumed wall height.""" + return enc.roof_length_m * WALL_HEIGHT_M + + +def enclosure_lateral_force_N(env: P.Environment = P.ENV, enc: P.Enclosure = P.ENCLOSURE, + site: P.Site = P.SITE) -> float: + """Survival lateral drag on the enclosure box (N): q_survival * Cf * A_wall.""" + q = dynamic_pressure_Pa(env.survival_wind_ms, site) + return q * WALL_FORCE_COEFF * enclosure_wall_area_m2(enc) + + +def enclosure_weight_N(enc: P.Enclosure = P.ENCLOSURE) -> float: + """Roof + wall-cladding dead weight of the enclosure (N).""" + perimeter = 2.0 * (enc.roof_span_m + enc.roof_length_m) + wall_mass = perimeter * WALL_HEIGHT_M * WALL_AREAL_MASS_KG_M2 + return u.weight_N(enc.roof_mass_kg + wall_mass) + + +def enclosure_overturning_Nm(enc: P.Enclosure = P.ENCLOSURE) -> float: + """Survival overturning moment of the box about its leeward base edge (N.m). + Lateral resultant applied at wall mid-height.""" + return enclosure_lateral_force_N() * (WALL_HEIGHT_M / 2.0) + + +# -------------------------------------------------------------------------- +# Budget +# -------------------------------------------------------------------------- +def evaluate(ota_key: str = "MN78") -> BudgetResult: + ota = P.OTA_CASES[ota_key] + + # --- operational (roof open): OTA drag at park and gust --- + f_park = drag_force_N(P.ENV.wind_park_ms, ota) + f_gust = drag_force_N(P.ENV.wind_gust_close_ms, ota) + m_gust_base = ota_wind_moment_Nm(P.ENV.wind_gust_close_ms, ota) + m_gust_top = f_gust * MOUNT_HEAD_HEIGHT_M + + # --- survival (roof closed): enclosure loads --- + q_surv = dynamic_pressure_Pa(P.ENV.survival_wind_ms) + uplift = roof_uplift_N() + roof_wt = roof_weight_N() + net_uplift = roof_net_uplift_N() + lateral = enclosure_lateral_force_N() + overturn = enclosure_overturning_Nm() + + # Roof hold-down: dead weight alone vs uplift, then the assumed anchor scheme. + sf_selfweight = roof_wt / uplift + anchor_capacity = ROOF_ANCHOR_COUNT * ROOF_ANCHOR_ALLOW_N + sf_anchored = anchor_capacity / uplift + + # Enclosure overturning: dead-weight-only vs with windward anchors. + enc_stab_weight = enclosure_weight_N() * (min(P.ENCLOSURE.roof_span_m, + P.ENCLOSURE.roof_length_m) / 2.0) + sf_overturn_deadwt = enc_stab_weight / overturn + + # Governing verdict = the roof self-weight hold-down (the starkest survival + # finding). As-specified (no anchors in repo) the roof CANNOT hold itself + # down -> FAIL. The anchor line shows the remediation. + governing_sf = sf_selfweight + verdict = verdict_from_sf(governing_sf, pass_at=1.5, marginal_at=1.0) + + survival_governs = uplift > f_gust and overturn > m_gust_base + + res = BudgetResult( + key="wind", + title="Wind structural loads (operational drag + survival uplift)", + verdict=verdict, + headline=( + f"Survival wind (105 mph) GOVERNS: {uplift/1e3:.1f} kN roof uplift vs " + f"{roof_wt/1e3:.1f} kN roof self-weight (SF {sf_selfweight:.2f}) -- " + f"hold-down anchors are MANDATORY and unspecified in the repo " + f"(4x 2 klbf anchors -> SF {sf_anchored:.1f}). Operational gust drag " + f"on {ota.name} is only {f_gust:.0f} N." + ), + target="Survival roof uplift resisted by hold-down (self-weight insufficient); " + "operational drag feeds torque/pier budgets", + safety_factor=governing_sf, + ) + # Operational block. + res.add("Air density @ 1800 m", P.SITE.air_density, "kg/m^3", "ISA, 16% below sea level") + res.add("q @ 25 mph park", dynamic_pressure_Pa(P.ENV.wind_park_ms), "Pa") + res.add("q @ 35 mph gust", dynamic_pressure_Pa(P.ENV.wind_gust_close_ms), "Pa") + res.add("OTA drag @ 25 mph (park)", f_park, "N") + res.add("OTA drag @ 35 mph (gust)", f_gust, "N", "max wind while open") + res.add("Wind moment at pier TOP @ gust", m_gust_top, "Nm", "F x mount-head height") + res.add("Wind moment at pier BASE @ gust", m_gust_base, "Nm", "F x (pier + head height)") + # Survival block. + res.add("-- survival, roof closed --", 0.0, "", "") + res.add("q @ 105 mph survival", q_surv, "Pa") + res.add("Roof gross uplift", uplift, "N", "q x GCp x A_roof") + res.add("Roof self-weight", roof_wt, "N") + res.add("Roof NET uplift (anchor demand)", net_uplift, "N", "uplift - self-weight") + res.add("Roof self-weight / uplift (SF)", sf_selfweight, "x", verdict.value) + res.add("Assumed anchor capacity (4x)", anchor_capacity, "N") + res.add("Anchored hold-down (SF)", sf_anchored, "x", "remediation") + res.add("Enclosure lateral drag", lateral, "N", "q x Cf x A_wall") + res.add("Enclosure overturning moment", overturn, "Nm", "about leeward base edge") + res.add("Overturning, dead-weight only (SF)", sf_overturn_deadwt, "x", + "anchors required" if sf_overturn_deadwt < 1.0 else "") + res.add("Survival governs vs operational", 1.0 if survival_governs else 0.0, "bool") + + res.assumptions = [ + f"Dynamic pressure uses site air density {P.SITE.air_density:.3f} kg/m^3 " + f"(ISA @ 1800 m, DERIVED) — lower than sea level, so loads are honest not inflated.", + "Operational max wind = 35 mph emergency-close gust (SOURCED safety monitor); the " + "OTA is only exposed with the roof open, so it never sees survival wind.", + f"Survival basic wind {P.ENV.survival_wind_ms/u.MPH_TO_MS:.0f} mph is ASSUMED " + "(ASCE 7 central-NV Risk Cat I; repo is silent — a governing void).", + f"Roof footprint {roof_area_m2():.0f} m^2 and mass {P.ENCLOSURE.roof_mass_kg:.0f} kg " + "are ASSUMED (params flags roof geometry as unspecified).", + f"Net uplift coefficient GCp = {P.ENV.roof_uplift_gcp} (SOURCED param); wall force " + f"coefficient Cf = {WALL_FORCE_COEFF} on an assumed {WALL_HEIGHT_M} m wall (ASSUMED).", + f"Mount-head height {MOUNT_HEAD_HEIGHT_M*1000:.0f} mm (OTA axis above pier top) is " + "ASSUMED; sets the operational overturning lever.", + "Repo specifies NO roof hold-down or enclosure anchorage; the anchor scheme " + "(4x 2 klbf, ASSUMED) is shown only as remediation, not as an existing spec.", + "The pier is assumed structurally isolated from the enclosure (standard observatory " + "practice), so roof uplift loads the enclosure foundation, not the pier.", + ] + return res + + +if __name__ == "__main__": + r = evaluate() + print(r.verdict.symbol, r.headline) + print(r.markdown_table()) + for a in r.assumptions: + print(" -", a) diff --git a/design/mechanical/tender/README.md b/design/mechanical/tender/README.md new file mode 100644 index 0000000..e1f4243 --- /dev/null +++ b/design/mechanical/tender/README.md @@ -0,0 +1,49 @@ +# NIGHTWATCH — Mount & Roll-off "Turret" Fabrication Tender + +**ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +Vendor tender package that turns the proven mechanical design +(`design/mechanical/MECHANICAL_DESIGN.md`) into biddable fabrication documents. +Bid **in parts or whole** — each package below is self-contained. + +## Start here +- **`packages/BP-00_master_dossier/`** — the umbrella: Instructions to Bidders, + general SOW, standards register, drawing register, Inspection & Test Plan, + **pricing/bid form**, T&Cs, and the roll-off/turret-variant appendix. + +## Bid packages (one per trade) +| Pkg | Scope | Trade | Items | +|---|---|---|:-:| +| BP-01 | Mount head machining (housings, saddle) | CNC 6061-T6 | 3 | +| BP-02 | Precision turned parts (spindles, shaft) | Turning, 303 SS | 3 | +| BP-03 | Pier & foundation *(PE-stamp gate)* | RC + structural steel | 4 | +| BP-04 | Roll-off roof "disappearing turret" *(PE-stamp gate)* | Steel fab + rail/drive | 6 | +| BP-05 | COTS procurement schedule | Purchase | 14 | + +## Deliverables (all generated from the proven `calc/params.py`) +- `bom/master_bom.csv`, `bom/cut_list.csv`, `bom/cots_schedule.csv` — dual-unit (mm [in]). +- `bom/NIGHTWATCH_tender_BOM.xlsx` — Excel workbook (Index + BOM + Cut List + COTS). +- `drawings/NW-*.svg` — 15 dimensioned control drawings; `drawings/DRAWING_SET.html` + bundles them all (open in a browser → Print → Save as PDF). +- `docs/NIGHTWATCH_tender_RFQ.docx` — the whole tender as one Word RFQ. + +## Regenerate (single source of truth) +``` +python3 -m design.mechanical.tender.gen.bom # BOM + cut list CSVs +python3 -m design.mechanical.tender.gen.drawing # 15 control drawings +python3 -m design.mechanical.tender.gen.htmlset # print-to-PDF drawing set +python3 -m design.mechanical.tender.gen.xlsx_export # Excel BOM (needs: pip install openpyxl) +python3 -m design.mechanical.tender.gen.docx_export # Word RFQ (needs: pip install python-docx) +``` +`design/mechanical/tests/test_tender.py` gates consistency (dims trace to the +registry; BOM ↔ cut-list ↔ drawings agree; remediations present, baseline absent). + +## Key notes for bidders +- **The five proof-out remediations are baked in:** angular-contact **7008/7006** + bearings (replace deep-groove 6008/6006), **on-axis absolute encoder**, **4× wind + hold-down anchors**, temperature-compensated focuser, 48 V power pack. +- **PE-stamp gate:** BP-03 pier and BP-04 roof are structural (ASCE 7 wind/snow/ + seismic) — a licensed Professional Engineer must stamp the analysis before any + "Issued for Construction" release. +- Values the design does not yet fix (weld/member sizing, rail/drive product, GD&T + values, surface-finish Ra) are marked **ASSUMED design-intent — bidder/PE to confirm**. diff --git a/design/mechanical/tender/__init__.py b/design/mechanical/tender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/design/mechanical/tender/bom/NIGHTWATCH_tender_BOM.xlsx b/design/mechanical/tender/bom/NIGHTWATCH_tender_BOM.xlsx new file mode 100644 index 0000000..3787e5e Binary files /dev/null and b/design/mechanical/tender/bom/NIGHTWATCH_tender_BOM.xlsx differ diff --git a/design/mechanical/tender/bom/cots_schedule.csv b/design/mechanical/tender/bom/cots_schedule.csv new file mode 100644 index 0000000..2d938b4 --- /dev/null +++ b/design/mechanical/tender/bom/cots_schedule.csv @@ -0,0 +1,16 @@ +part_no,name,package,supplier_class,qty,key_specs,provenance,notes +NW-PF-004,Anchor-bolt set + template,BP-03,ASTM F1554 Gr36 galvanized,1,Bolt dia=19.05; Embedment=304.8; Projection=50.0; Count=4; Template=0.25 in plywood/ply-steel,ASSUMED design-intent per ACI 318 Ch.17 (PE to size for wind/seismic uplift ~9.2 kN).,Cast into pier via template. Sizing tied to the wind proof roof/pier uplift — PE to confirm. +NW-CO-001,"Harmonic drive, RA (CSF-32-100-2A-GR)",BP-05,Harmonic Drive LLC,1,Ratio=100; Rated torque (Nm)=127; Peak (Nm)=343; Bore (mm)=80.0,P.RA_DRIVE (SOURCED).,"Strain-wave gear, RA axis." +NW-CO-002,"Harmonic drive, DEC (CSF-25-80-2A-GR)",BP-05,Harmonic Drive LLC,1,Ratio=80; Rated torque (Nm)=70; Peak (Nm)=186; Bore (mm)=64.0,P.DEC_DRIVE (SOURCED).,"Strain-wave gear, DEC axis." +NW-CO-003,Angular-contact bearing 7008 (RA pair),BP-05,"ABEC-7, back-to-back (DB)",2,Bore (mm)=40; OD (mm)=68; Arrangement=DB back-to-back,REMEDIATION of deep-groove 6008 (stiffness proof: bearings were 98% of the 43.5 arcsec FAIL).,Preloaded pair for MOMENT stiffness. Replaces 6008. +NW-CO-004,Angular-contact bearing 7006 (DEC pair),BP-05,"ABEC-7, back-to-back (DB)",2,Bore (mm)=30; OD (mm)=55; Arrangement=DB back-to-back,REMEDIATION of deep-groove 6006.,"Preloaded pair, DEC axis. Replaces 6006." +NW-CO-005,On-axis absolute encoder (RA + DEC),BP-05,"Absolute ring/BiSS-C, sub-arcsec",2,Resolution (arcsec)=<1; Interface=BiSS-C / SSI,REMEDIATION — encoder proof: baseline 5.02 arcsec RMS FAIL; on-axis ring reaches 0.54 arcsec.,MANDATORY for sub-arcsecond. Closes the loop on the axis (corrects harmonic PE). +NW-CO-006,Stepper motor NEMA17 + 27:1 planetary,BP-05,NEMA17 1.8deg + gearhead,2,Step (deg)=1.8; Planetary ratio=27,P.MOTOR (SOURCED).,One per axis. +NW-CO-007,Motor driver (TMC5160) / OnStepX board,BP-05,Trinamic TMC5160,2,Irun (A)=1.5; Igoto (A)=2.0,P.MOTOR (SOURCED).,Microstepping driver. +NW-CO-008,Temperature-compensated focuser,BP-05,e.g. motorised absolute focuser,1,Temp coeff=-2.5 steps/degC,REMEDIATION — thermal proof: passive focus walks ~10x depth-of-focus over 22 K swing.,Compensates the aluminium-tube focus drift. +NW-CO-009,"Counterweights (5 kg x2, 2.5 kg x1)",BP-05,Cast iron,3,Total (kg)=12.5,P.COUNTERWEIGHTS (SOURCED).,OPTIONAL — counterweight-FREE build omits these (torque proof). +NW-CO-010,V-groove track wheels,BP-05,"Steel V-groove, ~4 in",8,Dia (in)=4; Rated (kg each)=>=150,ASSUMED design-intent (BP-04 rail).,8 wheels for the roll-off roof (4 per side). +NW-CO-011,Roof drive (gate operator),BP-05,Sliding-gate operator / rack,1,Move force (N)=235; Drive=rack-and-pinion / chain,"ASSUMED design-intent — enclosure proof: 235 N move force, SF 2.1 vs ~500 N drive.",Sized for 180 kg roof + 35 mph gust. Fail-safe + wind interlock. +NW-CO-012,Metal roofing panel + flashing,BP-05,Standing-seam / corrugated steel,1,Area (m2)=9.0,ASSUMED design-intent (roof cladding).,Weatherproof skin + ridge/edge flashing + gaskets. +NW-CO-013,48 V LiFePO4 battery pack + solar,BP-05,"48 V pack, ~5 kWh",1,Bus (V)=48; Autonomy (h)=13.4,REMEDIATION — power proof: 12 V pack gives 3.3 h vs 10 h night; 48 V gives 13.4 h.,For off-grid autonomy (v2). v1 may run grid + UPS. +NW-CO-014,"Fastener schedule (SHCS, anchors)",BP-05,ASTM A574 SHCS / ASTM F1554 anchors,1,"Grades=A574 (SHCS), F1554 Gr36 (anchors)",ASSUMED design-intent.,Consolidated fastener BOM — see cut list / fastener schedule. diff --git a/design/mechanical/tender/bom/cut_list.csv b/design/mechanical/tender/bom/cut_list.csv new file mode 100644 index 0000000..4e452c8 --- /dev/null +++ b/design/mechanical/tender/bom/cut_list.csv @@ -0,0 +1,16 @@ +part_no,name,package,material,stock_form,stock_size (mm / in),qty,cut_allowance_mm,stock_mass_kg,finish,notes +NW-MH-001,RA (polar) axis housing,BP-01,6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,plate,plate 203.2 mm (8.000 in) x 203.2 mm (8.000 in) x 76.2 mm (3.000 in),1,3.0,8.50,"Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",Bearing seat sized for angular-contact 7008 (remediation of 6008). Pocket-lighten at bidder's option; keep bore/flange datums. +NW-MH-002,DEC axis housing + Losmandy-D saddle,BP-01,6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,plate,plate 152.4 mm (6.000 in) x 152.4 mm (6.000 in) x 63.5 mm (2.500 in),1,3.0,3.98,"Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",Integral Losmandy-D dovetail saddle. Bearing seat sized for angular-contact 7006. +NW-MH-003,DEC saddle clamp bar,BP-01,6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,plate,plate 101.6 mm (4.000 in) x 76.2 mm (3.000 in) x 25.4 mm (1.000 in),1,3.0,0.53,"Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",Pairs with NW-MH-002 saddle to clamp the OTA dovetail. +NW-TP-001,Counterweight shaft,BP-02,303 stainless per ASTM A582,round,round bar Ø31.8 mm (1.250 in) x 457.2 mm (18.000 in),1,3.0,2.90,Passivate per ASTM A967 (nitric),Counterweight-FREE build makes this OPTIONAL (see BP-00 / trade study). Include only if counterweighted variant is bid. +NW-TP-002,RA drive spindle / drawbar adapter,BP-02,303 stainless per ASTM A582,round,round bar Ø88.9 mm (3.500 in) x 101.6 mm (4.000 in),1,3.0,5.05,Passivate per ASTM A967 (nitric),Couples the CSF-32 output to the RA axis; carries the inner race of the 7008 pair. +NW-TP-003,DEC drive spindle / saddle stub,BP-02,303 stainless per ASTM A582,round,round bar Ø76.2 mm (3.000 in) x 88.9 mm (3.500 in),1,3.0,3.24,Passivate per ASTM A967 (nitric),Couples the CSF-25 output to the DEC axis / saddle side. +NW-PF-001,Reinforced-concrete telescope pier,BP-03,Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301,round,round bar Ø304.8 mm (12.000 in) x 1828.8 mm (72.000 in),1,3.0,320.26,none,ISOLATED from any building slab (vibration). Embedment>frost line. Rebar cage + anchor template cast integrally. REQUIRES PE STAMP. +NW-PF-002,Pier top plate,BP-03,ASTM A36 structural steel,plate,plate 304.8 mm (12.000 in) x 304.8 mm (12.000 in) x 9.5 mm (0.375 in),1,3.0,6.94,Hot-dip galvanize per ASTM A123,Grouted onto the pier over the cast-in anchor bolts. +NW-PF-003,Pier-to-mount adapter plate,BP-03,6061-T6 aluminium per ASTM B209,plate,plate 254.0 mm (10.000 in) x 254.0 mm (10.000 in) x 19.1 mm (0.750 in),1,3.0,3.32,"Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)","Levels/orients the RA axis to site latitude 38.9 deg (wedge or shim set, bidder to detail)." +NW-RR-001,Roof frame perimeter (HSS),BP-04,ASTM A500 Gr B HSS (steel),hss,HSS 50.8 mm (2.000 in) sq x 3.2 mm (0.125 in) wall x 3000.0 mm (118.110 in),4,3.0,56.98,Hot-dip galvanize per ASTM A123,Rectangular roll-off roof frame (4 perimeter members). Sizing to be confirmed vs ASCE 7 snow/wind by PE. +NW-RR-002,Roof rafters / purlins,BP-04,ASTM A500 Gr B HSS (steel),hss,HSS 38.1 mm (1.500 in) sq x 3.2 mm (0.125 in) wall x 3000.0 mm (118.110 in),5,3.0,52.23,Hot-dip galvanize per ASTM A123,"Carries the metal roofing panel + snow load (10.8 kN closed-roof snow case, see enclosure proof)." +NW-RR-003,Track rail beams (box track),BP-04,ASTM A36 structural steel,hss,HSS 63.5 mm (2.500 in) sq x 4.0 mm (0.157 in) wall x 6000.0 mm (236.220 in),2,3.0,89.68,Hot-dip galvanize per ASTM A123,Roof travels its own length off the building. Rail brand/section = COTS (BP-05). End stops required. +NW-RR-004,Wheel axle brackets,BP-04,ASTM A36 structural steel,plate,plate 101.6 mm (4.000 in) x 76.2 mm (3.000 in) x 9.5 mm (0.375 in),8,3.0,4.63,Hot-dip galvanize per ASTM A123,Carry the COTS V-groove wheels (BP-05). Roof 180 kg + snow -> size wheels for >=4x. +NW-RR-005,Drive bracket + end stops,BP-04,ASTM A36 structural steel,plate,plate 150.0 mm (5.906 in) x 100.0 mm (3.937 in) x 9.5 mm (0.375 in),4,3.0,4.49,Hot-dip galvanize per ASTM A123,Mounts the COTS drive (BP-05) and hard end stops. Wind-during-motion limit per enclosure proof. +NW-RR-006,Wind hold-down anchor brackets,BP-04,ASTM A36 structural steel,plate,plate 127.0 mm (5.000 in) x 101.6 mm (4.000 in) x 12.7 mm (0.500 in),4,3.0,5.15,Hot-dip galvanize per ASTM A123,MANDATORY. Clamp the roof/building against 105 mph survival uplift. Interlocked with the drive. diff --git a/design/mechanical/tender/bom/master_bom.csv b/design/mechanical/tender/bom/master_bom.csv new file mode 100644 index 0000000..e46fc0c --- /dev/null +++ b/design/mechanical/tender/bom/master_bom.csv @@ -0,0 +1,31 @@ +part_no,name,package,trade,material,process,finish,qty,stock_mass_kg,drawing,provenance +NW-MH-001,RA (polar) axis housing,BP-01,CNC machining (6061-T6 aluminium),6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,"CNC mill, 3-axis","Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",1,8.50,Y,Outer/wall/depth = P.RA_HOUSING (SOURCED); bearing seat=7008 remediation; flange PCD DERIVED ~1.3x bore. +NW-MH-002,DEC axis housing + Losmandy-D saddle,BP-01,CNC machining (6061-T6 aluminium),6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,"CNC mill, 3-axis","Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",1,3.98,Y,Outer/wall/depth = P.DEC_HOUSING (SOURCED); saddle = Losmandy-D ASSUMED std; bearing seat=7006 remediation. +NW-MH-003,DEC saddle clamp bar,BP-01,CNC machining (6061-T6 aluminium),6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11,"CNC mill, 3-axis","Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",1,0.53,Y,ASSUMED design-intent — Losmandy-D clamp not dimensioned in repo. +NW-TP-001,Counterweight shaft,BP-02,CNC / manual turning (303 stainless),303 stainless per ASTM A582,CNC turn,Passivate per ASTM A967 (nitric),1,2.90,Y,Dia/length = P.COUNTERWEIGHTS (SOURCED). +NW-TP-002,RA drive spindle / drawbar adapter,BP-02,CNC / manual turning (303 stainless),303 stainless per ASTM A582,CNC turn + mill,Passivate per ASTM A967 (nitric),1,5.05,Y,Journal = 7008 bore (remediation); CSF-32 register = P.RA_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NW-TP-003,DEC drive spindle / saddle stub,BP-02,CNC / manual turning (303 stainless),303 stainless per ASTM A582,CNC turn + mill,Passivate per ASTM A967 (nitric),1,3.24,Y,Journal = 7006 bore (remediation); CSF-25 register = P.DEC_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NW-PF-001,Reinforced-concrete telescope pier,BP-03,Reinforced concrete + structural steel,Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301,Cast-in-place,none,1,320.26,Y,Ø/height/embed/f'c = P.PIER (SOURCED); rebar schedule ASSUMED design-intent (PE to confirm). +NW-PF-002,Pier top plate,BP-03,Reinforced concrete + structural steel,ASTM A36 structural steel,Laser/waterjet cut + drill,Hot-dip galvanize per ASTM A123,1,6.94,Y,Side/thickness = P.PIER (SOURCED); hole pattern DERIVED. +NW-PF-003,Pier-to-mount adapter plate,BP-03,Reinforced concrete + structural steel,6061-T6 aluminium per ASTM B209,CNC mill,"Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)",1,3.32,Y,ASSUMED design-intent — adapter geometry not in repo (bridges 12x12 plate to 8x8 housing). +NW-PF-004,Anchor-bolt set + template,BP-03,Reinforced concrete + structural steel,ASTM F1554 Gr36 galvanized,Purchase + fabricate template,Hot-dip galvanize per ASTM A123,1,,Y,ASSUMED design-intent per ACI 318 Ch.17 (PE to size for wind/seismic uplift ~9.2 kN). +NW-RR-001,Roof frame perimeter (HSS),BP-04,Structural steel fab + rail/drive install,ASTM A500 Gr B HSS (steel),Cut + weld (AWS D1.1),Hot-dip galvanize per ASTM A123,4,56.98,Y,Roof span/length = P.ENCLOSURE (ASSUMED footprint); 4 perimeter members; size ASSUMED design-intent. +NW-RR-002,Roof rafters / purlins,BP-04,Structural steel fab + rail/drive install,ASTM A500 Gr B HSS (steel),Cut + weld (AWS D1.1),Hot-dip galvanize per ASTM A123,5,52.23,Y,ASSUMED design-intent (5 purlins @ ~600 mm). +NW-RR-003,Track rail beams (box track),BP-04,Structural steel fab + rail/drive install,ASTM A36 structural steel,Cut + weld / bolt to piers,Hot-dip galvanize per ASTM A123,2,89.68,Y,ASSUMED design-intent — rail run = 2x roof length so the roof fully clears the aperture. +NW-RR-004,Wheel axle brackets,BP-04,Structural steel fab + rail/drive install,ASTM A36 structural steel,Laser cut + weld,Hot-dip galvanize per ASTM A123,8,4.63,Y,ASSUMED design-intent — 8 wheels (4 per side). +NW-RR-005,Drive bracket + end stops,BP-04,Structural steel fab + rail/drive install,ASTM A36 structural steel,Laser cut + weld,Hot-dip galvanize per ASTM A123,4,4.49,Y,ASSUMED design-intent — drive = gate operator sized for 180 kg (enclosure proof: 235 N move force). +NW-RR-006,Wind hold-down anchor brackets,BP-04,Structural steel fab + rail/drive install,ASTM A36 structural steel,Laser cut + weld,Hot-dip galvanize per ASTM A123,4,5.15,Y,"REMEDIATION — wind proof: survival uplift 9.2 kN, 4 x 2 klbf anchors -> SF 3.9." +NW-CO-001,"Harmonic drive, RA (CSF-32-100-2A-GR)",BP-05,Purchase (buy-to-print / off-the-shelf),Harmonic Drive LLC,purchase,n-a,1,,N,P.RA_DRIVE (SOURCED). +NW-CO-002,"Harmonic drive, DEC (CSF-25-80-2A-GR)",BP-05,Purchase (buy-to-print / off-the-shelf),Harmonic Drive LLC,purchase,n-a,1,,N,P.DEC_DRIVE (SOURCED). +NW-CO-003,Angular-contact bearing 7008 (RA pair),BP-05,Purchase (buy-to-print / off-the-shelf),"ABEC-7, back-to-back (DB)",purchase,n-a,2,,N,REMEDIATION of deep-groove 6008 (stiffness proof: bearings were 98% of the 43.5 arcsec FAIL). +NW-CO-004,Angular-contact bearing 7006 (DEC pair),BP-05,Purchase (buy-to-print / off-the-shelf),"ABEC-7, back-to-back (DB)",purchase,n-a,2,,N,REMEDIATION of deep-groove 6006. +NW-CO-005,On-axis absolute encoder (RA + DEC),BP-05,Purchase (buy-to-print / off-the-shelf),"Absolute ring/BiSS-C, sub-arcsec",purchase,n-a,2,,N,REMEDIATION — encoder proof: baseline 5.02 arcsec RMS FAIL; on-axis ring reaches 0.54 arcsec. +NW-CO-006,Stepper motor NEMA17 + 27:1 planetary,BP-05,Purchase (buy-to-print / off-the-shelf),NEMA17 1.8deg + gearhead,purchase,n-a,2,,N,P.MOTOR (SOURCED). +NW-CO-007,Motor driver (TMC5160) / OnStepX board,BP-05,Purchase (buy-to-print / off-the-shelf),Trinamic TMC5160,purchase,n-a,2,,N,P.MOTOR (SOURCED). +NW-CO-008,Temperature-compensated focuser,BP-05,Purchase (buy-to-print / off-the-shelf),e.g. motorised absolute focuser,purchase,n-a,1,,N,REMEDIATION — thermal proof: passive focus walks ~10x depth-of-focus over 22 K swing. +NW-CO-009,"Counterweights (5 kg x2, 2.5 kg x1)",BP-05,Purchase (buy-to-print / off-the-shelf),Cast iron,purchase,n-a,3,,N,P.COUNTERWEIGHTS (SOURCED). +NW-CO-010,V-groove track wheels,BP-05,Purchase (buy-to-print / off-the-shelf),"Steel V-groove, ~4 in",purchase,n-a,8,,N,ASSUMED design-intent (BP-04 rail). +NW-CO-011,Roof drive (gate operator),BP-05,Purchase (buy-to-print / off-the-shelf),Sliding-gate operator / rack,purchase,n-a,1,,N,"ASSUMED design-intent — enclosure proof: 235 N move force, SF 2.1 vs ~500 N drive." +NW-CO-012,Metal roofing panel + flashing,BP-05,Purchase (buy-to-print / off-the-shelf),Standing-seam / corrugated steel,purchase,n-a,1,,N,ASSUMED design-intent (roof cladding). +NW-CO-013,48 V LiFePO4 battery pack + solar,BP-05,Purchase (buy-to-print / off-the-shelf),"48 V pack, ~5 kWh",purchase,n-a,1,,N,REMEDIATION — power proof: 12 V pack gives 3.3 h vs 10 h night; 48 V gives 13.4 h. +NW-CO-014,"Fastener schedule (SHCS, anchors)",BP-05,Purchase (buy-to-print / off-the-shelf),ASTM A574 SHCS / ASTM F1554 anchors,purchase,n-a,1,,N,ASSUMED design-intent. diff --git a/design/mechanical/tender/docs/NIGHTWATCH_tender_RFQ.docx b/design/mechanical/tender/docs/NIGHTWATCH_tender_RFQ.docx new file mode 100644 index 0000000..0f0c3bd Binary files /dev/null and b/design/mechanical/tender/docs/NIGHTWATCH_tender_RFQ.docx differ diff --git a/design/mechanical/tender/drawings/DRAWING_SET.html b/design/mechanical/tender/drawings/DRAWING_SET.html new file mode 100644 index 0000000..bd9983d --- /dev/null +++ b/design/mechanical/tender/drawings/DRAWING_SET.html @@ -0,0 +1,573 @@ + +NIGHTWATCH Tender — Drawing Set +

NIGHTWATCH Observatory

Mount & Roll-off 'Turret' — Fabrication Drawing Set

ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION

Control drawings generated from design/mechanical/calc/params.py. The parametric cad/*.scad (and STEP exported from it) is the 3-D machining master; these sheets are the dimensioned, quotable summary. Dimensions are dual-unit (mm [inch]). Rev A.

Drawing register

+ + + + + + + + + + + + + +
DrawingPartPkgMaterialFinishStock kg
NW-MH-001RA (polar) axis housingBP-016061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)8.5
NW-MH-002DEC axis housing + Losmandy-D saddleBP-016061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)4.0
NW-MH-003DEC saddle clamp barBP-016061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)0.5
NW-TP-001Counterweight shaftBP-02303 stainless per ASTM A582Passivate per ASTM A967 (nitric)2.9
NW-TP-002RA drive spindle / drawbar adapterBP-02303 stainless per ASTM A582Passivate per ASTM A967 (nitric)5.0
NW-TP-003DEC drive spindle / saddle stubBP-02303 stainless per ASTM A582Passivate per ASTM A967 (nitric)3.2
NW-PF-001Reinforced-concrete telescope pierBP-03Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301none320.3
NW-PF-002Pier top plateBP-03ASTM A36 structural steelHot-dip galvanize per ASTM A1236.9
NW-PF-003Pier-to-mount adapter plateBP-036061-T6 aluminium per ASTM B209Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)3.3
NW-RR-001Roof frame perimeter (HSS)BP-04ASTM A500 Gr B HSS (steel)Hot-dip galvanize per ASTM A12357.0
NW-RR-002Roof rafters / purlinsBP-04ASTM A500 Gr B HSS (steel)Hot-dip galvanize per ASTM A12352.2
NW-RR-003Track rail beams (box track)BP-04ASTM A36 structural steelHot-dip galvanize per ASTM A12389.7
NW-RR-004Wheel axle bracketsBP-04ASTM A36 structural steelHot-dip galvanize per ASTM A1234.6
NW-RR-005Drive bracket + end stopsBP-04ASTM A36 structural steelHot-dip galvanize per ASTM A1234.5
NW-RR-006Wind hold-down anchor bracketsBP-04ASTM A36 structural steelHot-dip galvanize per ASTM A1235.1
+
+ + +NW-MH-001 — RA (polar) axis housing +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 203.2 mm [8.000 in] +W 203.2 mm [8.000 in] +THK 76.2 mm [3.000 in] +KEY DIMENSIONS & FEATURES +• Outer X: 203.2 mm [8.000 in] +• Outer Y: 203.2 mm [8.000 in] +• Depth (bearing span): 76.2 mm [3.000 in] +• Wall: 8.0 mm [0.315 in] +• Bearing seat bore (7008) H7: 68.0 mm [2.677 in] +• Drive register bore (CSF-32): 80.0 mm [3.150 in] +• Flange bolt circle: 104.0 mm [4.094 in] +• Flange holes: 8 +• Corner holes (M6): 4 +PROVENANCE: Outer/wall/depth = P.RA_HOUSING (SOURCED); bearing seat=7008 remediation; flange PCD DERIVED ~1.3x bore. +NOTE: Bearing seat sized for angular-contact 7008 (remediation of 6008). Pocket-lighten at bidder's option; keep bore/flange datums. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: RA (polar) axis housing +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 1.6 um +DWG NW-MH-001 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-MH-002 — DEC axis housing + Losmandy-D saddle +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 152.4 mm [6.000 in] +W 152.4 mm [6.000 in] +THK 63.5 mm [2.500 in] +KEY DIMENSIONS & FEATURES +• Outer X: 152.4 mm [6.000 in] +• Outer Y: 152.4 mm [6.000 in] +• Depth (bearing span): 63.5 mm [2.500 in] +• Wall: 8.0 mm [0.315 in] +• Bearing seat bore (7006) H7: 55.0 mm [2.165 in] +• Drive register bore (CSF-25): 64.0 mm [2.520 in] +• Flange bolt circle: 83.0 mm [3.268 in] +• Losmandy-D saddle width: 76.2 mm [3.000 in] +• Dovetail angle (deg): 15 +PROVENANCE: Outer/wall/depth = P.DEC_HOUSING (SOURCED); saddle = Losmandy-D ASSUMED std; bearing seat=7006 remediation. +NOTE: Integral Losmandy-D dovetail saddle. Bearing seat sized for angular-contact 7006. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC axis housing + Losmandy-D saddle +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 1.6 um +DWG NW-MH-002 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-MH-003 — DEC saddle clamp bar +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 101.6 mm [4.000 in] +W 76.2 mm [3.000 in] +THK 25.4 mm [1.000 in] +KEY DIMENSIONS & FEATURES +• Length: 101.6 mm [4.000 in] +• Width: 76.2 mm [3.000 in] +• Thickness: 25.4 mm [1.000 in] +• Clamp screws (M8): 2 +• Dovetail angle (deg): 15 +PROVENANCE: ASSUMED design-intent — Losmandy-D clamp not dimensioned in repo. +NOTE: Pairs with NW-MH-002 saddle to clamp the OTA dovetail. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC saddle clamp bar +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 3.2 um +DWG NW-MH-003 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-TP-001 — Counterweight shaft +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 457.2 mm [18.000 in] +Ø 31.8 mm [1.250 in] +Ø31.8 mm [1.250 in] +KEY DIMENSIONS & FEATURES +• Diameter: 31.8 mm [1.250 in] +• Length: 457.2 mm [18.000 in] +• Stud end: M12 x 40 +• Safety-stop thread: M12 +• Root fillet R: 3 +PROVENANCE: Dia/length = P.COUNTERWEIGHTS (SOURCED). +NOTE: Counterweight-FREE build makes this OPTIONAL (see BP-00 / trade study). Include only if counterweighted variant is bid. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Counterweight shaft +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-001 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-TP-002 — RA drive spindle / drawbar adapter +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 101.6 mm [4.000 in] +Ø 88.9 mm [3.500 in] +Ø88.9 mm [3.500 in] +KEY DIMENSIONS & FEATURES +• Body dia: 88.9 mm [3.500 in] +• Length: 101.6 mm [4.000 in] +• Bearing journal (7008) j6: 40.0 mm [1.575 in] +• CSF-32 wave-gen register: 80.0 mm [3.150 in] +• Bolt pattern: 8 x M4 on 104 PCD +PROVENANCE: Journal = 7008 bore (remediation); CSF-32 register = P.RA_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NOTE: Couples the CSF-32 output to the RA axis; carries the inner race of the 7008 pair. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: RA drive spindle / drawbar adapter +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-002 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn + mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-TP-003 — DEC drive spindle / saddle stub +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 88.9 mm [3.500 in] +Ø 76.2 mm [3.000 in] +Ø76.2 mm [3.000 in] +KEY DIMENSIONS & FEATURES +• Body dia: 76.2 mm [3.000 in] +• Length: 88.9 mm [3.500 in] +• Bearing journal (7006) j6: 30.0 mm [1.181 in] +• CSF-25 wave-gen register: 64.0 mm [2.520 in] +• Bolt pattern: 8 x M4 on 83 PCD +PROVENANCE: Journal = 7006 bore (remediation); CSF-25 register = P.DEC_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NOTE: Couples the CSF-25 output to the DEC axis / saddle side. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC drive spindle / saddle stub +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-003 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn + mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-PF-001 — Reinforced-concrete telescope pier +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 1828.8 mm [72.000 in] +Ø 304.8 mm [12.000 in] +Ø304.8 mm [12.000 in] +KEY DIMENSIONS & FEATURES +• Diameter: 304.8 mm [12.000 in] +• Height above grade: 914.4 mm [36.000 in] +• Embedment: 914.4 mm [36.000 in] +• f'c (psi): 4000 +• Vertical rebar: 6 x #4 (A615 Gr60) +• Ties: #3 @ 12 in oc +PROVENANCE: Ø/height/embed/f'c = P.PIER (SOURCED); rebar schedule ASSUMED design-intent (PE to confirm). +NOTE: ISOLATED from any building slab (vibration). Embedment>frost line. Rebar cage + anchor template cast integrally. REQUIRES PE STAMP. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Reinforced-concrete telescope pier +MATERIAL: Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301 +FINISH: none +TOLERANCE: Plumb 1:200; top level 0.5 deg +SURFACE FINISH: n/a +DWG NW-PF-001 +PKG BP-03 — Pier & foundation +PROCESS: Cast-in-place +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-PF-002 — Pier top plate +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 304.8 mm [12.000 in] +W 304.8 mm [12.000 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Side: 304.8 mm [12.000 in] +• Thickness: 9.5 mm [0.375 in] +• Anchor holes: 4 x Ø0.81 (for Ø0.75 F1554) +• Centre pattern: matches NW-PF-003 +PROVENANCE: Side/thickness = P.PIER (SOURCED); hole pattern DERIVED. +NOTE: Grouted onto the pier over the cast-in anchor bolts. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Pier top plate +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: General ISO 2768-mK; hole pattern 0.25 +SURFACE FINISH: Ra 3.2 um +DWG NW-PF-002 +PKG BP-03 — Pier & foundation +PROCESS: Laser/waterjet cut + drill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-PF-003 — Pier-to-mount adapter plate +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 254.0 mm [10.000 in] +W 254.0 mm [10.000 in] +THK 19.1 mm [0.750 in] +KEY DIMENSIONS & FEATURES +• Side: 254.0 mm [10.000 in] +• Thickness: 19.1 mm [0.750 in] +• RA housing pattern: 4 x M6 (matches NW-MH-001) +• Pier plate pattern: 4 x Ø0.44 (matches NW-PF-002) +• Centre bore: 90.0 mm [3.543 in] +PROVENANCE: ASSUMED design-intent — adapter geometry not in repo (bridges 12x12 plate to 8x8 housing). +NOTE: Levels/orients the RA axis to site latitude 38.9 deg (wedge or shim set, bidder to detail). + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Pier-to-mount adapter plate +MATERIAL: 6061-T6 aluminium per ASTM B209 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 3.2 um +DWG NW-PF-003 +PKG BP-03 — Pier & foundation +PROCESS: CNC mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-001 — Roof frame perimeter (HSS) +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 3000.0 mm [118.110 in] +W 50.8 mm [2.000 in] +50.8 mm [2.000 in] sq x 3.2 mm [0.125 in] wall +KEY DIMENSIONS & FEATURES +• Member length (each): 3000.0 mm [118.110 in] +• Members: 4 +• Member: HSS 2x2x1/8 +• Corner joints: fully welded, gusseted +PROVENANCE: Roof span/length = P.ENCLOSURE (ASSUMED footprint); 4 perimeter members; size ASSUMED design-intent. +NOTE: Rectangular roll-off roof frame (4 perimeter members). Sizing to be confirmed vs ASCE 7 snow/wind by PE. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Roof frame perimeter (HSS) +MATERIAL: ASTM A500 Gr B HSS (steel) +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Frame square 3 mm/m; diagonal 5 mm +SURFACE FINISH: n/a +DWG NW-RR-001 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld (AWS D1.1) +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-002 — Roof rafters / purlins +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 3000.0 mm [118.110 in] +W 38.1 mm [1.500 in] +38.1 mm [1.500 in] sq x 3.2 mm [0.125 in] wall +KEY DIMENSIONS & FEATURES +• Member length (each): 3000.0 mm [118.110 in] +• Spacing: 600 +• Member: HSS 2x1x1/8 +• Members: 5 +PROVENANCE: ASSUMED design-intent (5 purlins @ ~600 mm). +NOTE: Carries the metal roofing panel + snow load (10.8 kN closed-roof snow case, see enclosure proof). + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Roof rafters / purlins +MATERIAL: ASTM A500 Gr B HSS (steel) +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: n/a +SURFACE FINISH: n/a +DWG NW-RR-002 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld (AWS D1.1) +QTY: 5 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-003 — Track rail beams (box track) +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 6000.0 mm [236.220 in] +W 63.5 mm [2.500 in] +63.5 mm [2.500 in] sq x 4.0 mm [0.157 in] wall +KEY DIMENSIONS & FEATURES +• Rail length (each): 6000.0 mm [236.220 in] +• Rails: 2 +• Gauge (rail spacing): 3000 +• Section: box track 2.5 in class +PROVENANCE: ASSUMED design-intent — rail run = 2x roof length so the roof fully clears the aperture. +NOTE: Roof travels its own length off the building. Rail brand/section = COTS (BP-05). End stops required. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Track rail beams (box track) +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Rail straightness 3 mm over run; gauge 2 mm +SURFACE FINISH: n/a +DWG NW-RR-003 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld / bolt to piers +QTY: 2 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-004 — Wheel axle brackets +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 101.6 mm [4.000 in] +W 76.2 mm [3.000 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Count: 8 +• Wheel bore: for Ø4 in V-groove wheel (COTS) +• Axle: M16 +PROVENANCE: ASSUMED design-intent — 8 wheels (4 per side). +NOTE: Carry the COTS V-groove wheels (BP-05). Roof 180 kg + snow -> size wheels for >=4x. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Wheel axle brackets +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Hole pattern 0.25 +SURFACE FINISH: n/a +DWG NW-RR-004 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 8 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-005 — Drive bracket + end stops +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 150.0 mm [5.906 in] +W 100.0 mm [3.937 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Drive: gate-operator / rack (COTS) +• End stops: 4.0 mm [0.157 in] +• Bump pads: rubber +PROVENANCE: ASSUMED design-intent — drive = gate operator sized for 180 kg (enclosure proof: 235 N move force). +NOTE: Mounts the COTS drive (BP-05) and hard end stops. Wind-during-motion limit per enclosure proof. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Drive bracket + end stops +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: n/a +SURFACE FINISH: n/a +DWG NW-RR-005 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+
+ + +NW-RR-006 — Wind hold-down anchor brackets +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 127.0 mm [5.000 in] +W 101.6 mm [4.000 in] +THK 12.7 mm [0.500 in] +KEY DIMENSIONS & FEATURES +• Count: 4 +• Anchor: Ø0.75 F1554 (to building/pier) +• Capacity (each): >=2 klbf +PROVENANCE: REMEDIATION — wind proof: survival uplift 9.2 kN, 4 x 2 klbf anchors -> SF 3.9. +NOTE: MANDATORY. Clamp the roof/building against 105 mph survival uplift. Interlocked with the drive. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Wind hold-down anchor brackets +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Hole pattern 0.25 +SURFACE FINISH: n/a +DWG NW-RR-006 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION +
+ \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-MH-001.svg b/design/mechanical/tender/drawings/NW-MH-001.svg new file mode 100644 index 0000000..138c454 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-MH-001.svg @@ -0,0 +1,40 @@ + + + +NW-MH-001 — RA (polar) axis housing +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 203.2 mm [8.000 in] +W 203.2 mm [8.000 in] +THK 76.2 mm [3.000 in] +KEY DIMENSIONS & FEATURES +• Outer X: 203.2 mm [8.000 in] +• Outer Y: 203.2 mm [8.000 in] +• Depth (bearing span): 76.2 mm [3.000 in] +• Wall: 8.0 mm [0.315 in] +• Bearing seat bore (7008) H7: 68.0 mm [2.677 in] +• Drive register bore (CSF-32): 80.0 mm [3.150 in] +• Flange bolt circle: 104.0 mm [4.094 in] +• Flange holes: 8 +• Corner holes (M6): 4 +PROVENANCE: Outer/wall/depth = P.RA_HOUSING (SOURCED); bearing seat=7008 remediation; flange PCD DERIVED ~1.3x bore. +NOTE: Bearing seat sized for angular-contact 7008 (remediation of 6008). Pocket-lighten at bidder's option; keep bore/flange datums. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: RA (polar) axis housing +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 1.6 um +DWG NW-MH-001 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-MH-002.svg b/design/mechanical/tender/drawings/NW-MH-002.svg new file mode 100644 index 0000000..7b48b8d --- /dev/null +++ b/design/mechanical/tender/drawings/NW-MH-002.svg @@ -0,0 +1,40 @@ + + + +NW-MH-002 — DEC axis housing + Losmandy-D saddle +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 152.4 mm [6.000 in] +W 152.4 mm [6.000 in] +THK 63.5 mm [2.500 in] +KEY DIMENSIONS & FEATURES +• Outer X: 152.4 mm [6.000 in] +• Outer Y: 152.4 mm [6.000 in] +• Depth (bearing span): 63.5 mm [2.500 in] +• Wall: 8.0 mm [0.315 in] +• Bearing seat bore (7006) H7: 55.0 mm [2.165 in] +• Drive register bore (CSF-25): 64.0 mm [2.520 in] +• Flange bolt circle: 83.0 mm [3.268 in] +• Losmandy-D saddle width: 76.2 mm [3.000 in] +• Dovetail angle (deg): 15 +PROVENANCE: Outer/wall/depth = P.DEC_HOUSING (SOURCED); saddle = Losmandy-D ASSUMED std; bearing seat=7006 remediation. +NOTE: Integral Losmandy-D dovetail saddle. Bearing seat sized for angular-contact 7006. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC axis housing + Losmandy-D saddle +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 1.6 um +DWG NW-MH-002 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-MH-003.svg b/design/mechanical/tender/drawings/NW-MH-003.svg new file mode 100644 index 0000000..61cfcb1 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-MH-003.svg @@ -0,0 +1,36 @@ + + + +NW-MH-003 — DEC saddle clamp bar +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 101.6 mm [4.000 in] +W 76.2 mm [3.000 in] +THK 25.4 mm [1.000 in] +KEY DIMENSIONS & FEATURES +• Length: 101.6 mm [4.000 in] +• Width: 76.2 mm [3.000 in] +• Thickness: 25.4 mm [1.000 in] +• Clamp screws (M8): 2 +• Dovetail angle (deg): 15 +PROVENANCE: ASSUMED design-intent — Losmandy-D clamp not dimensioned in repo. +NOTE: Pairs with NW-MH-002 saddle to clamp the OTA dovetail. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC saddle clamp bar +MATERIAL: 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 3.2 um +DWG NW-MH-003 +PKG BP-01 — Mount head machining +PROCESS: CNC mill, 3-axis +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-PF-001.svg b/design/mechanical/tender/drawings/NW-PF-001.svg new file mode 100644 index 0000000..238e264 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-PF-001.svg @@ -0,0 +1,38 @@ + + + +NW-PF-001 — Reinforced-concrete telescope pier +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 1828.8 mm [72.000 in] +Ø 304.8 mm [12.000 in] +Ø304.8 mm [12.000 in] +KEY DIMENSIONS & FEATURES +• Diameter: 304.8 mm [12.000 in] +• Height above grade: 914.4 mm [36.000 in] +• Embedment: 914.4 mm [36.000 in] +• f'c (psi): 4000 +• Vertical rebar: 6 x #4 (A615 Gr60) +• Ties: #3 @ 12 in oc +PROVENANCE: Ø/height/embed/f'c = P.PIER (SOURCED); rebar schedule ASSUMED design-intent (PE to confirm). +NOTE: ISOLATED from any building slab (vibration). Embedment>frost line. Rebar cage + anchor template cast integrally. REQUIRES PE STAMP. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Reinforced-concrete telescope pier +MATERIAL: Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301 +FINISH: none +TOLERANCE: Plumb 1:200; top level 0.5 deg +SURFACE FINISH: n/a +DWG NW-PF-001 +PKG BP-03 — Pier & foundation +PROCESS: Cast-in-place +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-PF-002.svg b/design/mechanical/tender/drawings/NW-PF-002.svg new file mode 100644 index 0000000..86a1fbe --- /dev/null +++ b/design/mechanical/tender/drawings/NW-PF-002.svg @@ -0,0 +1,35 @@ + + + +NW-PF-002 — Pier top plate +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 304.8 mm [12.000 in] +W 304.8 mm [12.000 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Side: 304.8 mm [12.000 in] +• Thickness: 9.5 mm [0.375 in] +• Anchor holes: 4 x Ø0.81 (for Ø0.75 F1554) +• Centre pattern: matches NW-PF-003 +PROVENANCE: Side/thickness = P.PIER (SOURCED); hole pattern DERIVED. +NOTE: Grouted onto the pier over the cast-in anchor bolts. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Pier top plate +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: General ISO 2768-mK; hole pattern 0.25 +SURFACE FINISH: Ra 3.2 um +DWG NW-PF-002 +PKG BP-03 — Pier & foundation +PROCESS: Laser/waterjet cut + drill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-PF-003.svg b/design/mechanical/tender/drawings/NW-PF-003.svg new file mode 100644 index 0000000..601c846 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-PF-003.svg @@ -0,0 +1,36 @@ + + + +NW-PF-003 — Pier-to-mount adapter plate +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 254.0 mm [10.000 in] +W 254.0 mm [10.000 in] +THK 19.1 mm [0.750 in] +KEY DIMENSIONS & FEATURES +• Side: 254.0 mm [10.000 in] +• Thickness: 19.1 mm [0.750 in] +• RA housing pattern: 4 x M6 (matches NW-MH-001) +• Pier plate pattern: 4 x Ø0.44 (matches NW-PF-002) +• Centre bore: 90.0 mm [3.543 in] +PROVENANCE: ASSUMED design-intent — adapter geometry not in repo (bridges 12x12 plate to 8x8 housing). +NOTE: Levels/orients the RA axis to site latitude 38.9 deg (wedge or shim set, bidder to detail). + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Pier-to-mount adapter plate +MATERIAL: 6061-T6 aluminium per ASTM B209 +FINISH: Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear) +TOLERANCE: General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05 +SURFACE FINISH: Ra 3.2 um +DWG NW-PF-003 +PKG BP-03 — Pier & foundation +PROCESS: CNC mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-001.svg b/design/mechanical/tender/drawings/NW-RR-001.svg new file mode 100644 index 0000000..156b30d --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-001.svg @@ -0,0 +1,35 @@ + + + +NW-RR-001 — Roof frame perimeter (HSS) +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 3000.0 mm [118.110 in] +W 50.8 mm [2.000 in] +50.8 mm [2.000 in] sq x 3.2 mm [0.125 in] wall +KEY DIMENSIONS & FEATURES +• Member length (each): 3000.0 mm [118.110 in] +• Members: 4 +• Member: HSS 2x2x1/8 +• Corner joints: fully welded, gusseted +PROVENANCE: Roof span/length = P.ENCLOSURE (ASSUMED footprint); 4 perimeter members; size ASSUMED design-intent. +NOTE: Rectangular roll-off roof frame (4 perimeter members). Sizing to be confirmed vs ASCE 7 snow/wind by PE. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Roof frame perimeter (HSS) +MATERIAL: ASTM A500 Gr B HSS (steel) +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Frame square 3 mm/m; diagonal 5 mm +SURFACE FINISH: n/a +DWG NW-RR-001 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld (AWS D1.1) +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-002.svg b/design/mechanical/tender/drawings/NW-RR-002.svg new file mode 100644 index 0000000..cbf1cec --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-002.svg @@ -0,0 +1,35 @@ + + + +NW-RR-002 — Roof rafters / purlins +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 3000.0 mm [118.110 in] +W 38.1 mm [1.500 in] +38.1 mm [1.500 in] sq x 3.2 mm [0.125 in] wall +KEY DIMENSIONS & FEATURES +• Member length (each): 3000.0 mm [118.110 in] +• Spacing: 600 +• Member: HSS 2x1x1/8 +• Members: 5 +PROVENANCE: ASSUMED design-intent (5 purlins @ ~600 mm). +NOTE: Carries the metal roofing panel + snow load (10.8 kN closed-roof snow case, see enclosure proof). + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Roof rafters / purlins +MATERIAL: ASTM A500 Gr B HSS (steel) +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: n/a +SURFACE FINISH: n/a +DWG NW-RR-002 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld (AWS D1.1) +QTY: 5 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-003.svg b/design/mechanical/tender/drawings/NW-RR-003.svg new file mode 100644 index 0000000..55eb188 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-003.svg @@ -0,0 +1,35 @@ + + + +NW-RR-003 — Track rail beams (box track) +Envelope (HSS) — bid control drawing; .scad/STEP is the 3-D master. + +L 6000.0 mm [236.220 in] +W 63.5 mm [2.500 in] +63.5 mm [2.500 in] sq x 4.0 mm [0.157 in] wall +KEY DIMENSIONS & FEATURES +• Rail length (each): 6000.0 mm [236.220 in] +• Rails: 2 +• Gauge (rail spacing): 3000 +• Section: box track 2.5 in class +PROVENANCE: ASSUMED design-intent — rail run = 2x roof length so the roof fully clears the aperture. +NOTE: Roof travels its own length off the building. Rail brand/section = COTS (BP-05). End stops required. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Track rail beams (box track) +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Rail straightness 3 mm over run; gauge 2 mm +SURFACE FINISH: n/a +DWG NW-RR-003 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Cut + weld / bolt to piers +QTY: 2 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-004.svg b/design/mechanical/tender/drawings/NW-RR-004.svg new file mode 100644 index 0000000..b41563c --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-004.svg @@ -0,0 +1,34 @@ + + + +NW-RR-004 — Wheel axle brackets +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 101.6 mm [4.000 in] +W 76.2 mm [3.000 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Count: 8 +• Wheel bore: for Ø4 in V-groove wheel (COTS) +• Axle: M16 +PROVENANCE: ASSUMED design-intent — 8 wheels (4 per side). +NOTE: Carry the COTS V-groove wheels (BP-05). Roof 180 kg + snow -> size wheels for >=4x. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Wheel axle brackets +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Hole pattern 0.25 +SURFACE FINISH: n/a +DWG NW-RR-004 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 8 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-005.svg b/design/mechanical/tender/drawings/NW-RR-005.svg new file mode 100644 index 0000000..c148574 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-005.svg @@ -0,0 +1,34 @@ + + + +NW-RR-005 — Drive bracket + end stops +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 150.0 mm [5.906 in] +W 100.0 mm [3.937 in] +THK 9.5 mm [0.375 in] +KEY DIMENSIONS & FEATURES +• Drive: gate-operator / rack (COTS) +• End stops: 4.0 mm [0.157 in] +• Bump pads: rubber +PROVENANCE: ASSUMED design-intent — drive = gate operator sized for 180 kg (enclosure proof: 235 N move force). +NOTE: Mounts the COTS drive (BP-05) and hard end stops. Wind-during-motion limit per enclosure proof. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Drive bracket + end stops +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: n/a +SURFACE FINISH: n/a +DWG NW-RR-005 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-RR-006.svg b/design/mechanical/tender/drawings/NW-RR-006.svg new file mode 100644 index 0000000..4bef65f --- /dev/null +++ b/design/mechanical/tender/drawings/NW-RR-006.svg @@ -0,0 +1,34 @@ + + + +NW-RR-006 — Wind hold-down anchor brackets +Envelope (PLATE) — bid control drawing; .scad/STEP is the 3-D master. + +L 127.0 mm [5.000 in] +W 101.6 mm [4.000 in] +THK 12.7 mm [0.500 in] +KEY DIMENSIONS & FEATURES +• Count: 4 +• Anchor: Ø0.75 F1554 (to building/pier) +• Capacity (each): >=2 klbf +PROVENANCE: REMEDIATION — wind proof: survival uplift 9.2 kN, 4 x 2 klbf anchors -> SF 3.9. +NOTE: MANDATORY. Clamp the roof/building against 105 mph survival uplift. Interlocked with the drive. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Wind hold-down anchor brackets +MATERIAL: ASTM A36 structural steel +FINISH: Hot-dip galvanize per ASTM A123 +TOLERANCE: Hole pattern 0.25 +SURFACE FINISH: n/a +DWG NW-RR-006 +PKG BP-04 — Roll-off roof ('disappearing turret') +PROCESS: Laser cut + weld +QTY: 4 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-TP-001.svg b/design/mechanical/tender/drawings/NW-TP-001.svg new file mode 100644 index 0000000..623ec17 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-TP-001.svg @@ -0,0 +1,37 @@ + + + +NW-TP-001 — Counterweight shaft +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 457.2 mm [18.000 in] +Ø 31.8 mm [1.250 in] +Ø31.8 mm [1.250 in] +KEY DIMENSIONS & FEATURES +• Diameter: 31.8 mm [1.250 in] +• Length: 457.2 mm [18.000 in] +• Stud end: M12 x 40 +• Safety-stop thread: M12 +• Root fillet R: 3 +PROVENANCE: Dia/length = P.COUNTERWEIGHTS (SOURCED). +NOTE: Counterweight-FREE build makes this OPTIONAL (see BP-00 / trade study). Include only if counterweighted variant is bid. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: Counterweight shaft +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-001 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-TP-002.svg b/design/mechanical/tender/drawings/NW-TP-002.svg new file mode 100644 index 0000000..bfd1025 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-TP-002.svg @@ -0,0 +1,37 @@ + + + +NW-TP-002 — RA drive spindle / drawbar adapter +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 101.6 mm [4.000 in] +Ø 88.9 mm [3.500 in] +Ø88.9 mm [3.500 in] +KEY DIMENSIONS & FEATURES +• Body dia: 88.9 mm [3.500 in] +• Length: 101.6 mm [4.000 in] +• Bearing journal (7008) j6: 40.0 mm [1.575 in] +• CSF-32 wave-gen register: 80.0 mm [3.150 in] +• Bolt pattern: 8 x M4 on 104 PCD +PROVENANCE: Journal = 7008 bore (remediation); CSF-32 register = P.RA_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NOTE: Couples the CSF-32 output to the RA axis; carries the inner race of the 7008 pair. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: RA drive spindle / drawbar adapter +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-002 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn + mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/drawings/NW-TP-003.svg b/design/mechanical/tender/drawings/NW-TP-003.svg new file mode 100644 index 0000000..3994023 --- /dev/null +++ b/design/mechanical/tender/drawings/NW-TP-003.svg @@ -0,0 +1,37 @@ + + + +NW-TP-003 — DEC drive spindle / saddle stub +Envelope (ROUND) — bid control drawing; .scad/STEP is the 3-D master. + + +L 88.9 mm [3.500 in] +Ø 76.2 mm [3.000 in] +Ø76.2 mm [3.000 in] +KEY DIMENSIONS & FEATURES +• Body dia: 76.2 mm [3.000 in] +• Length: 88.9 mm [3.500 in] +• Bearing journal (7006) j6: 30.0 mm [1.181 in] +• CSF-25 wave-gen register: 64.0 mm [2.520 in] +• Bolt pattern: 8 x M4 on 83 PCD +PROVENANCE: Journal = 7006 bore (remediation); CSF-25 register = P.DEC_DRIVE.bore_m (SOURCED). Body dia ASSUMED. +NOTE: Couples the CSF-25 output to the DEC axis / saddle side. + + + + + + +NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER +PART: DEC drive spindle / saddle stub +MATERIAL: 303 stainless per ASTM A582 +FINISH: Passivate per ASTM A967 (nitric) +TOLERANCE: General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR +SURFACE FINISH: Ra 0.8 um +DWG NW-TP-003 +PKG BP-02 — Precision turned parts +PROCESS: CNC turn + mill +QTY: 1 REV: A UNITS: mm [in] +STATUS: +ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION + \ No newline at end of file diff --git a/design/mechanical/tender/gen/__init__.py b/design/mechanical/tender/gen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/design/mechanical/tender/gen/bom.py b/design/mechanical/tender/gen/bom.py new file mode 100644 index 0000000..cdfd3fc --- /dev/null +++ b/design/mechanical/tender/gen/bom.py @@ -0,0 +1,109 @@ +""" +BOM + cut-list generator — turns the parts registry into the vendor-facing +material documents. Emits CSV (which the XLSX step renders to Excel): + + bom/master_bom.csv — every line item, all packages + bom/cut_list.csv — fabricated stock to order & cut (dual inch + mm) + bom/cots_schedule.csv— purchased (off-the-shelf) items + +Dimensions are dual-unit (mm primary, inch in brackets) so a US shop can quote +directly while the SI design stays traceable. Masses are STOCK mass (what you +order), computed from stock volume x material density. +""" + +from __future__ import annotations + +import csv +import pathlib + +from design.mechanical.calc import units as u +from design.mechanical.tender.gen import partspec as ps + +OUT = pathlib.Path(__file__).resolve().parents[1] / "bom" +KERF_MM = 3.0 # ASSUMED saw/laser kerf allowance per cut + + +def _in(mm: float) -> float: + return round(mm / 25.4, 3) + + +def _dual(mm: float) -> str: + return f"{mm:.1f} mm ({_in(mm):.3f} in)" + + +def stock_description(p: ps.Part) -> str: + kind = p.stock[0] + if kind == "plate": + _, L, W, T = p.stock + return f"plate {_dual(L)} x {_dual(W)} x {_dual(T)}" + if kind == "round": + _, dia, length = p.stock + return f"round bar Ø{_dual(dia)} x {_dual(length)}" + if kind == "hss": + _, o, wall, length = p.stock + return f"HSS {_dual(o)} sq x {_dual(wall)} wall x {_dual(length)}" + return "COTS" + + +def write_master_bom() -> pathlib.Path: + path = OUT / "master_bom.csv" + with path.open("w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["part_no", "name", "package", "trade", "material", "process", + "finish", "qty", "stock_mass_kg", "drawing", "provenance"]) + for p in ps.ALL_PARTS: + m = p.stock_mass_kg() + w.writerow([p.part_no, p.name, p.package, ps.PACKAGES[p.package][1], p.material, + p.process, p.finish, p.qty, "" if m is None else f"{m:.2f}", + "Y" if p.drawing else "N", p.provenance]) + return path + + +def write_cut_list() -> pathlib.Path: + path = OUT / "cut_list.csv" + with path.open("w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["part_no", "name", "package", "material", "stock_form", + "stock_size (mm / in)", "qty", "cut_allowance_mm", "stock_mass_kg", "finish", "notes"]) + for p in ps.ALL_PARTS: + if p.stock[0] == "cots": + continue + m = p.stock_mass_kg() + w.writerow([p.part_no, p.name, p.package, p.material, p.stock[0], + stock_description(p), p.qty, KERF_MM, + "" if m is None else f"{m:.2f}", p.finish, p.notes]) + return path + + +def write_cots_schedule() -> pathlib.Path: + path = OUT / "cots_schedule.csv" + with path.open("w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["part_no", "name", "package", "supplier_class", "qty", "key_specs", "provenance", "notes"]) + for p in ps.ALL_PARTS: + if p.stock[0] != "cots": + continue + specs = "; ".join(f"{k}={v}" for k, v in p.key_dims_mm.items()) + w.writerow([p.part_no, p.name, p.package, p.material, p.qty, specs, p.provenance, p.notes]) + return path + + +def totals() -> dict: + fab_mass = sum((p.stock_mass_kg() or 0.0) for p in ps.ALL_PARTS if p.stock[0] != "cots") + return { + "line_items": len(ps.ALL_PARTS), + "fabricated": sum(1 for p in ps.ALL_PARTS if p.stock[0] != "cots"), + "cots": sum(1 for p in ps.ALL_PARTS if p.stock[0] == "cots"), + "fab_stock_mass_kg": round(fab_mass, 1), + } + + +def generate_all() -> list[pathlib.Path]: + OUT.mkdir(parents=True, exist_ok=True) + return [write_master_bom(), write_cut_list(), write_cots_schedule()] + + +if __name__ == "__main__": + for pth in generate_all(): + print("wrote", pth) + print("totals:", totals()) diff --git a/design/mechanical/tender/gen/docx_export.py b/design/mechanical/tender/gen/docx_export.py new file mode 100644 index 0000000..fed7aa7 --- /dev/null +++ b/design/mechanical/tender/gen/docx_export.py @@ -0,0 +1,156 @@ +""" +Vendor DOCX exporter — assembles the authored Markdown tender documents (the +BP-00 master dossier + each bid package's SOW) into ONE Word RFQ a buyer can send. + +Requires python-docx (``pip install python-docx``) — NOT a test dependency; the +Markdown under packages/ is the source of truth and the committed .docx is a +convenience deliverable. Run: python3 -m design.mechanical.tender.gen.docx_export +""" + +from __future__ import annotations + +import pathlib +import re + +from docx import Document +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.shared import Pt, RGBColor + +TENDER = pathlib.Path(__file__).resolve().parents[1] +PACK = TENDER / "packages" +OUT = TENDER / "docs" / "NIGHTWATCH_tender_RFQ.docx" + +NAVY = RGBColor(0x1F, 0x38, 0x64) +RED = RGBColor(0xA0, 0x00, 0x00) +STATUS = "ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION" + +# Ordered assembly: the master dossier first, then each package. +DOSSIER_ORDER = [ + "README.md", "00_instructions_to_bidders.md", "01_statement_of_work_general.md", + "02_standards_register.md", "03_drawing_register.md", "04_inspection_test_plan.md", + "05_bid_form.md", "06_terms_and_conditions.md", +] +PACKAGE_ORDER = ["BP-01_mount_machining", "BP-02_turned_parts", "BP-03_pier_foundation", + "BP-04_rolloff_roof", "BP-05_cots_schedule"] +PACKAGE_FILES = ["README.md", "SOW.md", "acceptance.md", "weld_map.md", "SCHEDULE.md", + "fastener_schedule.md"] + +_BOLD = re.compile(r"\*\*(.+?)\*\*") + + +def _add_runs(paragraph, text: str) -> None: + """Add text to a paragraph, honouring **bold** and stripping `code` ticks.""" + text = text.replace("`", "") + pos = 0 + for m in _BOLD.finditer(text): + if m.start() > pos: + paragraph.add_run(text[pos:m.start()]) + paragraph.add_run(m.group(1)).bold = True + pos = m.end() + if pos < len(text): + paragraph.add_run(text[pos:]) + + +def _flush_table(doc, rows: list[list[str]]) -> None: + rows = [r for r in rows if not all(set(c.strip()) <= {"-", ":", " "} for c in r)] + if not rows: + return + ncols = max(len(r) for r in rows) + table = doc.add_table(rows=0, cols=ncols) + table.style = "Light Grid Accent 1" + for i, r in enumerate(rows): + cells = table.add_row().cells + for j in range(ncols): + val = r[j] if j < len(r) else "" + cells[j].text = "" + _add_runs(cells[j].paragraphs[0], val) + if i == 0: + for run in cells[j].paragraphs[0].runs: + run.bold = True + + +def _render_markdown(doc, md: str) -> None: + table_buf: list[list[str]] = [] + for raw in md.splitlines(): + line = raw.rstrip() + if line.strip().startswith("|") and "|" in line.strip()[1:]: + cells = [c.strip() for c in line.strip().strip("|").split("|")] + table_buf.append(cells) + continue + if table_buf: + _flush_table(doc, table_buf) + table_buf = [] + if not line.strip(): + continue + if line.startswith("#"): + level = len(line) - len(line.lstrip("#")) + doc.add_heading(line.lstrip("#").strip(), level=min(level, 4)) + elif line.lstrip().startswith((">",)): + p = doc.add_paragraph() + r = p.add_run(line.lstrip("> ").strip()) + r.italic = True + elif re.match(r"^\s*[-*•]\s+", line): + p = doc.add_paragraph(style="List Bullet") + _add_runs(p, re.sub(r"^\s*[-*•]\s+", "", line)) + elif re.match(r"^\s*\d+\.\s+", line): + p = doc.add_paragraph(style="List Number") + _add_runs(p, re.sub(r"^\s*\d+\.\s+", "", line)) + else: + _add_runs(doc.add_paragraph(), line) + if table_buf: + _flush_table(doc, table_buf) + + +def _title_page(doc) -> None: + t = doc.add_paragraph() + t.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = t.add_run("NIGHTWATCH OBSERVATORY") + r.bold = True + r.font.size = Pt(26) + r.font.color.rgb = NAVY + s = doc.add_paragraph() + s.alignment = WD_ALIGN_PARAGRAPH.CENTER + r2 = s.add_run("Telescope Mount & Roll-off 'Turret'\nFabrication Tender / Request for Quotation") + r2.font.size = Pt(15) + st = doc.add_paragraph() + st.alignment = WD_ALIGN_PARAGRAPH.CENTER + r3 = st.add_run(STATUS) + r3.bold = True + r3.font.color.rgb = RED + cap = doc.add_paragraph() + cap.alignment = WD_ALIGN_PARAGRAPH.CENTER + cap.add_run("Rev A · dimensions dual-unit (mm [inch]) · generated from the proven design " + "(design/mechanical/calc/params.py)").italic = True + doc.add_page_break() + + +def build() -> pathlib.Path: + OUT.parent.mkdir(parents=True, exist_ok=True) + doc = Document() + _title_page(doc) + + doc.add_heading("Part I — Master Tender Dossier (BP-00)", level=1) + dossier = PACK / "BP-00_master_dossier" + for fname in DOSSIER_ORDER: + f = dossier / fname + if f.exists(): + _render_markdown(doc, f.read_text()) + doc.add_page_break() + + for pkg in PACKAGE_ORDER: + d = PACK / pkg + if not d.exists(): + continue + doc.add_heading(f"Part II — {pkg.replace('_', ' ')}", level=1) + for fname in PACKAGE_FILES: + f = d / fname + if f.exists(): + _render_markdown(doc, f.read_text()) + doc.add_page_break() + + doc.save(OUT) + return OUT + + +if __name__ == "__main__": + print("wrote", build()) diff --git a/design/mechanical/tender/gen/drawing.py b/design/mechanical/tender/gen/drawing.py new file mode 100644 index 0000000..ad467f7 --- /dev/null +++ b/design/mechanical/tender/gen/drawing.py @@ -0,0 +1,204 @@ +""" +Fabrication control-drawing generator (SVG, one per fabricated part). + +Each sheet is a *bid control drawing*: a to-scale envelope of the stock with the +controlling overall dimensions (dual inch + mm), a keyed feature/dimension table, +and an ASME-Y14-style title block carrying material, finish, tolerance, GD&T, and +the "ISSUED FOR BID / FOR PE REVIEW" status. The parametric ``cad/*.scad`` (and the +STEP exported from it) remain the machining master for 3-D features; this sheet is +the dimensioned, quotable summary a shop reviews to price the job. + +Pure stdlib string templating (no CAD app, no third-party libs) so it regenerates +deterministically and a test can diff it against the parts registry. +""" + +from __future__ import annotations + +import html +import pathlib + +from design.mechanical.tender.gen import partspec as ps + +OUT = pathlib.Path(__file__).resolve().parents[1] / "drawings" +REV = "A" +STATUS = "ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION" + +SHEET_W, SHEET_H = 1120, 792 # ~ ANSI B landscape, px +MARGIN = 16 +TITLE_H = 150 + + +def _in(mm: float) -> float: + return round(mm / 25.4, 3) + + +def _dual(mm: float) -> str: + return f"{mm:.1f} mm [{_in(mm):.3f} in]" + + +def _esc(s) -> str: + return html.escape(str(s)) + + +def _envelope_dims(p: ps.Part): + """Return (label, width_mm, height_mm, depth_note) for the drawn envelope.""" + kind = p.stock[0] + if kind == "plate": + _, L, W, T = p.stock + return "PLATE", L, W, f"THK {_dual(T)}" + if kind == "round": + _, dia, length = p.stock + return "ROUND", length, dia, f"Ø{_dual(dia)}" + if kind == "hss": + _, o, wall, length = p.stock + return "HSS", length, o, f"{_dual(o)} sq x {_dual(wall)} wall" + return "ITEM", 100.0, 60.0, "" + + +def _svg_header() -> list[str]: + return [ + f'', + f'', + f'', + ] + + +def _dim_line(x1, y1, x2, y2, label, above=True): + """A dimension line with arrow ticks and a centred label.""" + mid_x = (x1 + x2) / 2 + mid_y = (y1 + y2) / 2 + dy = -6 if above else 14 + return ( + f'' + f'' + f'' + f'{_esc(label)}' + ) + + +def _title_block(p: ps.Part) -> list[str]: + x0 = MARGIN + y0 = SHEET_H - MARGIN - TITLE_H + w = SHEET_W - 2 * MARGIN + lines = [f''] + # horizontal rules + for yy in (y0 + 26, y0 + 26 + 32, y0 + 26 + 64, y0 + 26 + 96): + lines.append(f'') + # vertical split + xr = x0 + w - 300 + lines.append(f'') + + def txt(x, y, s, size=12, weight="normal", fill="black"): + return (f'{_esc(s)}') + + lines.append(txt(x0 + 10, y0 + 18, "NIGHTWATCH OBSERVATORY — MOUNT & ROLL-OFF TENDER", 13, "bold")) + lines.append(txt(x0 + 10, y0 + 46, f"PART: {p.name}", 13, "bold")) + lines.append(txt(x0 + 10, y0 + 46 + 26, f"MATERIAL: {p.material}")) + lines.append(txt(x0 + 10, y0 + 46 + 26 + 26, f"FINISH: {p.finish}")) + lines.append(txt(x0 + 10, y0 + 46 + 26 + 52, f"TOLERANCE: {p.tolerance}")) + lines.append(txt(x0 + 10, y0 + 46 + 26 + 76, + f"SURFACE FINISH: Ra {p.ra_um} um" if p.ra_um else "SURFACE FINISH: n/a", 11)) + # right column + lines.append(txt(xr + 10, y0 + 18, f"DWG {p.part_no}", 13, "bold")) + lines.append(txt(xr + 10, y0 + 44, f"PKG {p.package} — {ps.PACKAGES[p.package][0]}", 10)) + lines.append(txt(xr + 10, y0 + 44 + 26, f"PROCESS: {p.process}", 10)) + lines.append(txt(xr + 10, y0 + 44 + 52, f"QTY: {p.qty} REV: {REV} UNITS: mm [in]", 10)) + lines.append(txt(xr + 10, y0 + 44 + 78, "STATUS:", 9, "bold", "#a00")) + lines.append(txt(xr + 10, y0 + 44 + 92, STATUS, 8, "bold", "#a00")) + return lines + + +# key_dims_mm entries whose *name* contains one of these are NOT linear dimensions +# and must not be rendered as "mm [in]" (they are counts, angles, pressures, etc.). +_NONLENGTH_TOKENS = ( + "hole", "member", "rail", "count", "angle", "deg", "psi", "f'c", "ratio", + "torque", "nm", "(v)", "ach", "period", "obstruction", "(psf)", "spacing", + "kg", "area", "(each)", +) + + +def _is_length_key(key: str) -> bool: + k = key.lower() + # "member length (each)" / "rail length (each)" ARE lengths despite the tokens + if "length" in k: + return True + return not any(tok in k for tok in _NONLENGTH_TOKENS) + + +def _fmt_value(key: str, v) -> str: + if isinstance(v, bool) or not isinstance(v, (int, float)): + return str(v) + if _is_length_key(key) and v > 3: + return _dual(float(v)) + return str(int(v)) if float(v).is_integer() else str(v) + + +def _feature_table(p: ps.Part, x0: int, y0: int) -> list[str]: + lines = [f'KEY DIMENSIONS & FEATURES'] + y = y0 + 14 + for k, v in p.key_dims_mm.items(): + val = _fmt_value(k, v) + lines.append(f'• {_esc(k)}: {_esc(val)}') + y += 18 + lines.append(f'PROVENANCE: {_esc(p.provenance)}') + if p.notes: + lines.append(f'NOTE: {_esc(p.notes)}') + return lines + + +def render_part(p: ps.Part) -> str: + parts = _svg_header() + label, w_mm, h_mm, depth_note = _envelope_dims(p) + + # Scale the envelope into the left drawing zone. + zone_x, zone_y, zone_w, zone_h = 60, 90, 470, 420 + scale = min(zone_w / max(w_mm, 1), zone_h / max(h_mm, 1)) * 0.7 + rw, rh = w_mm * scale, h_mm * scale + rx = zone_x + (zone_w - rw) / 2 + ry = zone_y + (zone_h - rh) / 2 + + parts.append(f'' + f'{_esc(p.part_no)} — {_esc(p.name)}') + parts.append(f'' + f'Envelope ({label}) — bid control drawing; .scad/STEP is the 3-D master.') + + if label == "ROUND": + parts.append(f'') + parts.append(f'') + else: + parts.append(f'') + + # overall dimensions + parts.append(_dim_line(rx, ry - 18, rx + rw, ry - 18, + (f"L {_dual(w_mm)}" if label != "ROUND" else f"L {_dual(w_mm)}"))) + parts.append(_dim_line(rx + rw + 18, ry, rx + rw + 18, ry + rh, + (f"Ø {_dual(h_mm)}" if label == "ROUND" else f"W {_dual(h_mm)}"), above=False)) + parts.append(f'{_esc(depth_note)}') + + parts += _feature_table(p, 560, 120) + parts += _title_block(p) + parts.append("") + return "\n".join(parts) + + +def generate_all() -> list[pathlib.Path]: + OUT.mkdir(parents=True, exist_ok=True) + written = [] + for p in ps.fabricated_parts(): + path = OUT / f"{p.part_no}.svg" + path.write_text(render_part(p)) + written.append(path) + return written + + +if __name__ == "__main__": + for pth in generate_all(): + print("wrote", pth.name) + print(f"{len(list(OUT.glob('*.svg')))} drawings in {OUT}") diff --git a/design/mechanical/tender/gen/htmlset.py b/design/mechanical/tender/gen/htmlset.py new file mode 100644 index 0000000..529ecde --- /dev/null +++ b/design/mechanical/tender/gen/htmlset.py @@ -0,0 +1,83 @@ +""" +Drawing-set bundler — inlines every SVG control drawing into ONE self-contained, +print-to-PDF HTML document (a cover + drawing register + one drawing per page). + +Pure stdlib. This is the single "drawing package" file to hand a vendor: open in +any browser, File > Print > Save as PDF gives an ANSI-B drawing set. Run: + python3 -m design.mechanical.tender.gen.htmlset +""" + +from __future__ import annotations + +import pathlib + +from design.mechanical.tender.gen import partspec as ps + +DRAW = pathlib.Path(__file__).resolve().parents[1] / "drawings" +OUT = DRAW / "DRAWING_SET.html" + +CSS = """ +* { box-sizing: border-box; } +body { font-family: Helvetica, Arial, sans-serif; margin: 0; color: #111; } +.page { page-break-after: always; padding: 24px; } +.cover h1 { color: #1F3864; margin: 0 0 4px; } +.status { color: #a00; font-weight: bold; letter-spacing: .5px; } +table { border-collapse: collapse; width: 100%; font-size: 12px; margin-top: 12px; } +th, td { border: 1px solid #bbb; padding: 5px 7px; text-align: left; vertical-align: top; } +th { background: #1F3864; color: #fff; } +.drawing svg { width: 100%; height: auto; border: 1px solid #ccc; } +.cap { font-size: 12px; color: #555; margin: 6px 0 0; } +@media print { .page { padding: 0; } @page { size: 1120px 792px; margin: 8mm; } } +""" + + +def _register_rows() -> str: + rows = [] + for p in ps.fabricated_parts(): + m = p.stock_mass_kg() + rows.append( + f"{p.part_no}{p.name}{p.package}" + f"{p.material}{p.finish}" + f"{'' if m is None else f'{m:.1f}'}" + ) + return "\n".join(rows) + + +def build() -> pathlib.Path: + parts = [f"", + f"NIGHTWATCH Tender — Drawing Set"] + + # Cover + register + parts.append( + "
" + "

NIGHTWATCH Observatory

" + "

Mount & Roll-off 'Turret' — Fabrication Drawing Set

" + "

ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION

" + "

Control drawings generated from design/mechanical/calc/params.py. " + "The parametric cad/*.scad (and STEP exported from it) is the 3-D machining " + "master; these sheets are the dimensioned, quotable summary. Dimensions are dual-unit " + "(mm [inch]). Rev A.

" + "

Drawing register

" + "" + "" + f"{_register_rows()}
DrawingPartPkgMaterialFinishStock kg
" + "
" + ) + + for p in ps.fabricated_parts(): + svg = (DRAW / f"{p.part_no}.svg") + if not svg.exists(): + continue + body = svg.read_text() + # strip the XML declaration if present so it inlines cleanly + if body.startswith("", 1)[1] + parts.append(f"
{body}
") + + parts.append("") + OUT.write_text("\n".join(parts)) + return OUT + + +if __name__ == "__main__": + print("wrote", build()) diff --git a/design/mechanical/tender/gen/partspec.py b/design/mechanical/tender/gen/partspec.py new file mode 100644 index 0000000..6a949c8 --- /dev/null +++ b/design/mechanical/tender/gen/partspec.py @@ -0,0 +1,368 @@ +""" +NIGHTWATCH tender — the parts registry (single source of truth for the BOM, +cut lists, and fabrication drawings). + +Every fabricated part's controlling dimensions are pulled from the proven +``design/mechanical/calc/params.py`` so the tender cannot drift from the +validated design. Where the design does not yet fix a value a fabricator needs +(roof member sizes, weld sizes, GD&T tolerance values, surface finish), it is +stated here as **ASSUMED design-intent** for the bidder to confirm — never as a +final number. + +Units: dimensions are held in MILLIMETRES (the CAD/shop convention); the drawing +and BOM engines render dual inch + mm. Masses in kg, computed from stock volume x +material density (the quantity you *order*, not the finished-part net mass). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from design.mechanical.calc import params as P +from design.mechanical.calc import units as u + +MM3_PER_M3 = 1.0e9 + +# Trades / bid packages ----------------------------------------------------- +PACKAGES = { + "BP-01": ("Mount head machining", "CNC machining (6061-T6 aluminium)"), + "BP-02": ("Precision turned parts", "CNC / manual turning (303 stainless)"), + "BP-03": ("Pier & foundation", "Reinforced concrete + structural steel"), + "BP-04": ("Roll-off roof ('disappearing turret')", "Structural steel fab + rail/drive install"), + "BP-05": ("COTS procurement schedule", "Purchase (buy-to-print / off-the-shelf)"), +} + + +@dataclass(frozen=True) +class Part: + part_no: str + name: str + package: str # BP-01..05 + material: str # spec string incl. ASTM/AMS callout + stock_form: str # human stock description + stock: tuple # ('plate', L, W, T) | ('round', dia, len) | ('hss', o, wall, len) | ('cots',) + process: str # CNC mill / turn / weld / cast-in-place / purchase + finish: str # anodize / galv / passivate / none / n-a + tolerance: str # general + critical (H7 bores etc.) + ra_um: float | None # surface finish, micrometres (None = n/a) + qty: int + key_dims_mm: dict = field(default_factory=dict) + provenance: str = "" # sourced/derived/assumed trace + notes: str = "" + drawing: bool = True # False => COTS, no fab drawing + + @property + def density(self) -> float | None: + for key in ("6061-T6", "303-SS", "A36-steel", "concrete-4ksi"): + if key.split("-")[0].lower() in self.material.lower() or key in self.material: + return P.MATERIALS[key].rho + if "6061" in self.material: + return P.MATERIALS["6061-T6"].rho + if "303" in self.material or "304" in self.material or "stainless" in self.material.lower(): + return P.MATERIALS["303-SS"].rho + if "a36" in self.material.lower() or "a500" in self.material.lower() or "steel" in self.material.lower(): + return P.MATERIALS["A36-steel"].rho + if "concrete" in self.material.lower(): + return P.MATERIALS["concrete-4ksi"].rho + return None + + def stock_volume_m3(self) -> float | None: + kind = self.stock[0] + if kind == "plate": + _, L, W, T = self.stock + return (L * W * T) / MM3_PER_M3 + if kind == "round": + _, dia, length = self.stock + import math + return (math.pi / 4.0 * dia * dia * length) / MM3_PER_M3 + if kind == "hss": # square/rect hollow section, approx wall*perimeter*len + _, outer, wall, length = self.stock + inner = outer - 2 * wall + area_mm2 = outer * outer - inner * inner + return (area_mm2 * length) / MM3_PER_M3 + return None + + def stock_mass_kg(self) -> float | None: + v = self.stock_volume_m3() + d = self.density + if v is None or d is None: + return None + return v * d * self.qty + + +# Controlling dimensions pulled from params (mm) ---------------------------- +def _mm(m: float) -> float: + return round(m * 1000.0, 2) + + +RA = P.RA_HOUSING +DEC = P.DEC_HOUSING +CW = P.COUNTERWEIGHTS +PIER = P.PIER + +# ========================================================================== +# BP-01 — Mount head machining (6061-T6, Type III hardcoat anodize) +# ========================================================================== +_ANOD = "Type III hardcoat anodize, 0.002 in, per MIL-A-8625F Class 1 (clear)" +_AL = "6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11" +_TOL_MACH = "General ISO 2768-mK; bearing bores H7 (+0.030/0); flange faces flat 0.05" + +BP01 = [ + Part( + "NW-MH-001", "RA (polar) axis housing", "BP-01", _AL, + "6061-T6 plate 8.0 x 8.0 x 3.0 in", ("plate", _mm(RA.outer_x_m), _mm(RA.outer_y_m), _mm(RA.depth_m)), + "CNC mill, 3-axis", _ANOD, _TOL_MACH, 1.6, 1, + key_dims_mm={ + "Outer X": _mm(RA.outer_x_m), "Outer Y": _mm(RA.outer_y_m), "Depth (bearing span)": _mm(RA.depth_m), + "Wall": _mm(RA.wall_m), "Bearing seat bore (7008) H7": 68.0, "Drive register bore (CSF-32)": _mm(P.RA_DRIVE.bore_m), + "Flange bolt circle": 104.0, "Flange holes": 8, "Corner holes (M6)": 4, + }, + provenance="Outer/wall/depth = P.RA_HOUSING (SOURCED); bearing seat=7008 remediation; flange PCD DERIVED ~1.3x bore.", + notes="Bearing seat sized for angular-contact 7008 (remediation of 6008). Pocket-lighten at bidder's option; keep bore/flange datums.", + ), + Part( + "NW-MH-002", "DEC axis housing + Losmandy-D saddle", "BP-01", _AL, + "6061-T6 plate 6.0 x 6.0 x 2.5 in", ("plate", _mm(DEC.outer_x_m), _mm(DEC.outer_y_m), _mm(DEC.depth_m)), + "CNC mill, 3-axis", _ANOD, _TOL_MACH, 1.6, 1, + key_dims_mm={ + "Outer X": _mm(DEC.outer_x_m), "Outer Y": _mm(DEC.outer_y_m), "Depth (bearing span)": _mm(DEC.depth_m), + "Wall": _mm(DEC.wall_m), "Bearing seat bore (7006) H7": 55.0, "Drive register bore (CSF-25)": _mm(P.DEC_DRIVE.bore_m), + "Flange bolt circle": 83.0, "Losmandy-D saddle width": 76.2, "Dovetail angle (deg)": 15, + }, + provenance="Outer/wall/depth = P.DEC_HOUSING (SOURCED); saddle = Losmandy-D ASSUMED std; bearing seat=7006 remediation.", + notes="Integral Losmandy-D dovetail saddle. Bearing seat sized for angular-contact 7006.", + ), + Part( + "NW-MH-003", "DEC saddle clamp bar", "BP-01", _AL, + "6061-T6 bar 3.0 x 1.0 x 4.0 in", ("plate", 101.6, 76.2, 25.4), + "CNC mill, 3-axis", _ANOD, _TOL_MACH, 3.2, 1, + key_dims_mm={"Length": 101.6, "Width": 76.2, "Thickness": 25.4, "Clamp screws (M8)": 2, "Dovetail angle (deg)": 15}, + provenance="ASSUMED design-intent — Losmandy-D clamp not dimensioned in repo.", + notes="Pairs with NW-MH-002 saddle to clamp the OTA dovetail.", + ), +] + +# ========================================================================== +# BP-02 — Precision turned parts (303 SS, passivated) +# ========================================================================== +_SS = "303 stainless per ASTM A582" +_PASS = "Passivate per ASTM A967 (nitric)" +_TOL_TURN = "General ISO 2768-mK; journal diameters h6; concentricity 0.02 TIR" + +BP02 = [ + Part( + "NW-TP-001", "Counterweight shaft", "BP-02", _SS, + "303-SS round bar 1.25 in dia x 18 in", ("round", _mm(CW.shaft_dia_m), _mm(CW.shaft_len_m)), + "CNC turn", _PASS, _TOL_TURN, 0.8, 1, + key_dims_mm={"Diameter": _mm(CW.shaft_dia_m), "Length": _mm(CW.shaft_len_m), "Stud end": "M12 x 40", + "Safety-stop thread": "M12", "Root fillet R": 3.0}, + provenance="Dia/length = P.COUNTERWEIGHTS (SOURCED).", + notes="Counterweight-FREE build makes this OPTIONAL (see BP-00 / trade study). Include only if counterweighted variant is bid.", + ), + Part( + "NW-TP-002", "RA drive spindle / drawbar adapter", "BP-02", _SS, + "303-SS round bar 3.5 in dia x 4 in", ("round", 88.9, 101.6), + "CNC turn + mill", _PASS, _TOL_TURN, 0.8, 1, + key_dims_mm={"Body dia": 88.9, "Length": 101.6, "Bearing journal (7008) j6": 40.0, + "CSF-32 wave-gen register": _mm(P.RA_DRIVE.bore_m), "Bolt pattern": "8 x M4 on 104 PCD"}, + provenance="Journal = 7008 bore (remediation); CSF-32 register = P.RA_DRIVE.bore_m (SOURCED). Body dia ASSUMED.", + notes="Couples the CSF-32 output to the RA axis; carries the inner race of the 7008 pair.", + ), + Part( + "NW-TP-003", "DEC drive spindle / saddle stub", "BP-02", _SS, + "303-SS round bar 3.0 in dia x 3.5 in", ("round", 76.2, 88.9), + "CNC turn + mill", _PASS, _TOL_TURN, 0.8, 1, + key_dims_mm={"Body dia": 76.2, "Length": 88.9, "Bearing journal (7006) j6": 30.0, + "CSF-25 wave-gen register": _mm(P.DEC_DRIVE.bore_m), "Bolt pattern": "8 x M4 on 83 PCD"}, + provenance="Journal = 7006 bore (remediation); CSF-25 register = P.DEC_DRIVE.bore_m (SOURCED). Body dia ASSUMED.", + notes="Couples the CSF-25 output to the DEC axis / saddle side.", + ), +] + +# ========================================================================== +# BP-03 — Pier & foundation (RC + structural steel). PE-STAMP GATE. +# ========================================================================== +_STEEL = "ASTM A36 structural steel" +_GALV = "Hot-dip galvanize per ASTM A123" +_CONC = "Cast-in-place concrete f'c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301" + +BP03 = [ + Part( + "NW-PF-001", "Reinforced-concrete telescope pier", "BP-03", _CONC, + "Drilled/formed pier Ø12 in", ("round", _mm(PIER.diameter_m), _mm(PIER.height_above_m + PIER.embed_depth_m)), + "Cast-in-place", "none", "Plumb 1:200; top level 0.5 deg", None, 1, + key_dims_mm={"Diameter": _mm(PIER.diameter_m), "Height above grade": _mm(PIER.height_above_m), + "Embedment": _mm(PIER.embed_depth_m), "f'c (psi)": 4000, + "Vertical rebar": "6 x #4 (A615 Gr60)", "Ties": "#3 @ 12 in oc"}, + provenance="Ø/height/embed/f'c = P.PIER (SOURCED); rebar schedule ASSUMED design-intent (PE to confirm).", + notes="ISOLATED from any building slab (vibration). Embedment>frost line. Rebar cage + anchor template cast integrally. REQUIRES PE STAMP.", + ), + Part( + "NW-PF-002", "Pier top plate", "BP-03", _STEEL, + "A36 plate 12 x 12 x 0.375 in", ("plate", _mm(PIER.top_plate_side_m), _mm(PIER.top_plate_side_m), _mm(PIER.top_plate_thk_m)), + "Laser/waterjet cut + drill", _GALV, "General ISO 2768-mK; hole pattern 0.25", 3.2, 1, + key_dims_mm={"Side": _mm(PIER.top_plate_side_m), "Thickness": _mm(PIER.top_plate_thk_m), + "Anchor holes": "4 x Ø0.81 (for Ø0.75 F1554)", "Centre pattern": "matches NW-PF-003"}, + provenance="Side/thickness = P.PIER (SOURCED); hole pattern DERIVED.", + notes="Grouted onto the pier over the cast-in anchor bolts.", + ), + Part( + "NW-PF-003", "Pier-to-mount adapter plate", "BP-03", "6061-T6 aluminium per ASTM B209", + "6061-T6 plate 10 x 10 x 0.75 in", ("plate", 254.0, 254.0, 19.05), + "CNC mill", _ANOD, _TOL_MACH, 3.2, 1, + key_dims_mm={"Side": 254.0, "Thickness": 19.05, "RA housing pattern": "4 x M6 (matches NW-MH-001)", + "Pier plate pattern": "4 x Ø0.44 (matches NW-PF-002)", "Centre bore": 90.0}, + provenance="ASSUMED design-intent — adapter geometry not in repo (bridges 12x12 plate to 8x8 housing).", + notes="Levels/orients the RA axis to site latitude 38.9 deg (wedge or shim set, bidder to detail).", + ), + Part( + "NW-PF-004", "Anchor-bolt set + template", "BP-03", "ASTM F1554 Gr36 galvanized", + "4 x Ø0.75 in x 12 in anchor bolts + template", ("cots",), + "Purchase + fabricate template", _GALV, "Template hole 0.02", None, 1, + key_dims_mm={"Bolt dia": 19.05, "Embedment": 304.8, "Projection": 50.0, "Count": 4, "Template": "0.25 in plywood/ply-steel"}, + provenance="ASSUMED design-intent per ACI 318 Ch.17 (PE to size for wind/seismic uplift ~9.2 kN).", + notes="Cast into pier via template. Sizing tied to the wind proof roof/pier uplift — PE to confirm.", + drawing=True, + ), +] + +# ========================================================================== +# BP-04 — Roll-off roof "disappearing turret" (A36 steel; AWS D1.1). +# Member sizes are ASSUMED design-intent (bidder/PE to confirm vs ASCE 7 loads). +# ========================================================================== +_A500 = "ASTM A500 Gr B HSS (steel)" +_WELD = "Welded per AWS D1.1; weld symbols per AWS A2.4" + +def _roof_len_mm() -> float: + return _mm(P.ENCLOSURE.roof_length_m) + +def _roof_span_mm() -> float: + return _mm(P.ENCLOSURE.roof_span_m) + +BP04 = [ + Part( + "NW-RR-001", "Roof frame perimeter (HSS)", "BP-04", _A500, + "HSS 2 x 2 x 1/8 in", ("hss", 50.8, 3.175, _roof_span_mm()), + "Cut + weld (AWS D1.1)", _GALV, "Frame square 3 mm/m; diagonal 5 mm", None, 4, + key_dims_mm={"Member length (each)": _roof_span_mm(), "Members": 4, "Member": "HSS 2x2x1/8", + "Corner joints": "fully welded, gusseted"}, + provenance="Roof span/length = P.ENCLOSURE (ASSUMED footprint); 4 perimeter members; size ASSUMED design-intent.", + notes="Rectangular roll-off roof frame (4 perimeter members). Sizing to be confirmed vs ASCE 7 snow/wind by PE.", + ), + Part( + "NW-RR-002", "Roof rafters / purlins", "BP-04", _A500, + "HSS 2 x 1 x 1/8 in", ("hss", 38.1, 3.175, _roof_span_mm()), + "Cut + weld (AWS D1.1)", _GALV, "n/a", None, 5, + key_dims_mm={"Member length (each)": _roof_span_mm(), "Spacing": 600, "Member": "HSS 2x1x1/8", "Members": 5}, + provenance="ASSUMED design-intent (5 purlins @ ~600 mm).", + notes="Carries the metal roofing panel + snow load (10.8 kN closed-roof snow case, see enclosure proof).", + ), + Part( + "NW-RR-003", "Track rail beams (box track)", "BP-04", _STEEL, + "Box track / channel, ~2x roof length", ("hss", 63.5, 4.0, 2 * _roof_len_mm()), + "Cut + weld / bolt to piers", _GALV, "Rail straightness 3 mm over run; gauge 2 mm", None, 2, + key_dims_mm={"Rail length (each)": 2 * _roof_len_mm(), "Rails": 2, "Gauge (rail spacing)": _roof_span_mm(), + "Section": "box track 2.5 in class"}, + provenance="ASSUMED design-intent — rail run = 2x roof length so the roof fully clears the aperture.", + notes="Roof travels its own length off the building. Rail brand/section = COTS (BP-05). End stops required.", + ), + Part( + "NW-RR-004", "Wheel axle brackets", "BP-04", _STEEL, + "A36 plate 4 x 3 x 3/8 in", ("plate", 101.6, 76.2, 9.525), + "Laser cut + weld", _GALV, "Hole pattern 0.25", None, 8, + key_dims_mm={"Count": 8, "Wheel bore": "for Ø4 in V-groove wheel (COTS)", "Axle": "M16"}, + provenance="ASSUMED design-intent — 8 wheels (4 per side).", + notes="Carry the COTS V-groove wheels (BP-05). Roof 180 kg + snow -> size wheels for >=4x.", + ), + Part( + "NW-RR-005", "Drive bracket + end stops", "BP-04", _STEEL, + "A36 plate + angle assembly", ("plate", 150.0, 100.0, 9.525), + "Laser cut + weld", _GALV, "n/a", None, 4, + key_dims_mm={"Drive": "gate-operator / rack (COTS)", "End stops": 4, "Bump pads": "rubber"}, + provenance="ASSUMED design-intent — drive = gate operator sized for 180 kg (enclosure proof: 235 N move force).", + notes="Mounts the COTS drive (BP-05) and hard end stops. Wind-during-motion limit per enclosure proof.", + ), + Part( + "NW-RR-006", "Wind hold-down anchor brackets", "BP-04", _STEEL, + "A36 plate 5 x 4 x 1/2 in", ("plate", 127.0, 101.6, 12.7), + "Laser cut + weld", _GALV, "Hole pattern 0.25", None, 4, + key_dims_mm={"Count": 4, "Anchor": "Ø0.75 F1554 (to building/pier)", "Capacity (each)": ">=2 klbf"}, + provenance="REMEDIATION — wind proof: survival uplift 9.2 kN, 4 x 2 klbf anchors -> SF 3.9.", + notes="MANDATORY. Clamp the roof/building against 105 mph survival uplift. Interlocked with the drive.", + ), +] + +# ========================================================================== +# BP-05 — COTS procurement schedule (purchase; no fab drawing). +# ========================================================================== +def _cots(part_no, name, material, qty, dims, prov, notes): + return Part(part_no, name, "BP-05", material, "COTS", ("cots",), "purchase", "n-a", "per datasheet", + None, qty, key_dims_mm=dims, provenance=prov, notes=notes, drawing=False) + +BP05 = [ + _cots("NW-CO-001", "Harmonic drive, RA (CSF-32-100-2A-GR)", "Harmonic Drive LLC", 1, + {"Ratio": 100, "Rated torque (Nm)": 127, "Peak (Nm)": 343, "Bore (mm)": _mm(P.RA_DRIVE.bore_m)}, + "P.RA_DRIVE (SOURCED).", "Strain-wave gear, RA axis."), + _cots("NW-CO-002", "Harmonic drive, DEC (CSF-25-80-2A-GR)", "Harmonic Drive LLC", 1, + {"Ratio": 80, "Rated torque (Nm)": 70, "Peak (Nm)": 186, "Bore (mm)": _mm(P.DEC_DRIVE.bore_m)}, + "P.DEC_DRIVE (SOURCED).", "Strain-wave gear, DEC axis."), + _cots("NW-CO-003", "Angular-contact bearing 7008 (RA pair)", "ABEC-7, back-to-back (DB)", 2, + {"Bore (mm)": 40, "OD (mm)": 68, "Arrangement": "DB back-to-back"}, + "REMEDIATION of deep-groove 6008 (stiffness proof: bearings were 98% of the 43.5 arcsec FAIL).", + "Preloaded pair for MOMENT stiffness. Replaces 6008."), + _cots("NW-CO-004", "Angular-contact bearing 7006 (DEC pair)", "ABEC-7, back-to-back (DB)", 2, + {"Bore (mm)": 30, "OD (mm)": 55, "Arrangement": "DB back-to-back"}, + "REMEDIATION of deep-groove 6006.", "Preloaded pair, DEC axis. Replaces 6006."), + _cots("NW-CO-005", "On-axis absolute encoder (RA + DEC)", "Absolute ring/BiSS-C, sub-arcsec", 2, + {"Resolution (arcsec)": "<1", "Interface": "BiSS-C / SSI"}, + "REMEDIATION — encoder proof: baseline 5.02 arcsec RMS FAIL; on-axis ring reaches 0.54 arcsec.", + "MANDATORY for sub-arcsecond. Closes the loop on the axis (corrects harmonic PE)."), + _cots("NW-CO-006", "Stepper motor NEMA17 + 27:1 planetary", "NEMA17 1.8deg + gearhead", 2, + {"Step (deg)": 1.8, "Planetary ratio": 27}, "P.MOTOR (SOURCED).", "One per axis."), + _cots("NW-CO-007", "Motor driver (TMC5160) / OnStepX board", "Trinamic TMC5160", 2, + {"Irun (A)": 1.5, "Igoto (A)": 2.0}, "P.MOTOR (SOURCED).", "Microstepping driver."), + _cots("NW-CO-008", "Temperature-compensated focuser", "e.g. motorised absolute focuser", 1, + {"Temp coeff": "-2.5 steps/degC"}, + "REMEDIATION — thermal proof: passive focus walks ~10x depth-of-focus over 22 K swing.", + "Compensates the aluminium-tube focus drift."), + _cots("NW-CO-009", "Counterweights (5 kg x2, 2.5 kg x1)", "Cast iron", 3, + {"Total (kg)": 12.5}, "P.COUNTERWEIGHTS (SOURCED).", + "OPTIONAL — counterweight-FREE build omits these (torque proof)."), + _cots("NW-CO-010", "V-groove track wheels", "Steel V-groove, ~4 in", 8, + {"Dia (in)": 4, "Rated (kg each)": ">=150"}, "ASSUMED design-intent (BP-04 rail).", + "8 wheels for the roll-off roof (4 per side)."), + _cots("NW-CO-011", "Roof drive (gate operator)", "Sliding-gate operator / rack", 1, + {"Move force (N)": 235, "Drive": "rack-and-pinion / chain"}, + "ASSUMED design-intent — enclosure proof: 235 N move force, SF 2.1 vs ~500 N drive.", + "Sized for 180 kg roof + 35 mph gust. Fail-safe + wind interlock."), + _cots("NW-CO-012", "Metal roofing panel + flashing", "Standing-seam / corrugated steel", 1, + {"Area (m2)": round(P.ENCLOSURE.roof_span_m * P.ENCLOSURE.roof_length_m, 1)}, + "ASSUMED design-intent (roof cladding).", "Weatherproof skin + ridge/edge flashing + gaskets."), + _cots("NW-CO-013", "48 V LiFePO4 battery pack + solar", "48 V pack, ~5 kWh", 1, + {"Bus (V)": 48, "Autonomy (h)": 13.4}, + "REMEDIATION — power proof: 12 V pack gives 3.3 h vs 10 h night; 48 V gives 13.4 h.", + "For off-grid autonomy (v2). v1 may run grid + UPS."), + _cots("NW-CO-014", "Fastener schedule (SHCS, anchors)", "ASTM A574 SHCS / ASTM F1554 anchors", 1, + {"Grades": "A574 (SHCS), F1554 Gr36 (anchors)"}, "ASSUMED design-intent.", + "Consolidated fastener BOM — see cut list / fastener schedule."), +] + +ALL_PARTS: list[Part] = BP01 + BP02 + BP03 + BP04 + BP05 + + +def parts_for(package: str) -> list[Part]: + return [p for p in ALL_PARTS if p.package == package] + + +def fabricated_parts() -> list[Part]: + return [p for p in ALL_PARTS if p.drawing and p.stock[0] != "cots"] + + +if __name__ == "__main__": + for pkg, (title, trade) in PACKAGES.items(): + parts = parts_for(pkg) + print(f"{pkg} {title} ({trade}) — {len(parts)} line items") + for p in parts: + m = p.stock_mass_kg() + mm = f"{m:.1f} kg stock" if m else "COTS" + print(f" {p.part_no} {p.name:42} x{p.qty} {mm}") diff --git a/design/mechanical/tender/gen/xlsx_export.py b/design/mechanical/tender/gen/xlsx_export.py new file mode 100644 index 0000000..301edbb --- /dev/null +++ b/design/mechanical/tender/gen/xlsx_export.py @@ -0,0 +1,104 @@ +""" +Vendor XLSX exporter — renders the generated BOM CSVs into a single formatted +Excel workbook a fabricator can quote directly from. + +Requires openpyxl (``pip install openpyxl``) — it is NOT a test dependency; the +pure-stdlib CSVs in ``bom/`` are the source of truth and the committed workbook is +a convenience deliverable. Run: python3 -m design.mechanical.tender.gen.xlsx_export +""" + +from __future__ import annotations + +import csv +import pathlib + +from openpyxl import Workbook +from openpyxl.styles import Alignment, Border, Font, PatternFill, Side +from openpyxl.utils import get_column_letter + +BOM = pathlib.Path(__file__).resolve().parents[1] / "bom" +OUT = BOM / "NIGHTWATCH_tender_BOM.xlsx" + +HEAD_FILL = PatternFill("solid", fgColor="1F3864") +HEAD_FONT = Font(color="FFFFFF", bold=True, size=11) +TITLE_FONT = Font(bold=True, size=14, color="1F3864") +NOTE_FONT = Font(italic=True, size=9, color="A00000") +THIN = Side(style="thin", color="BBBBBB") +BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) + + +def _style_header(ws, row, ncols): + for c in range(1, ncols + 1): + cell = ws.cell(row=row, column=c) + cell.fill = HEAD_FILL + cell.font = HEAD_FONT + cell.alignment = Alignment(vertical="center", wrap_text=True) + cell.border = BORDER + + +def _autosize(ws, max_w=60): + for col in ws.columns: + width = max((len(str(c.value)) for c in col if c.value is not None), default=10) + ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_w, max(12, width + 2)) + + +def _sheet_from_csv(wb, title, csv_path, banner): + ws = wb.create_sheet(title[:31]) + ws["A1"] = "NIGHTWATCH OBSERVATORY — VENDOR TENDER" + ws["A1"].font = TITLE_FONT + ws["A2"] = banner + ws["A3"] = "ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION" + ws["A3"].font = NOTE_FONT + with csv_path.open() as fh: + rows = list(csv.reader(fh)) + header_row = 5 + for j, name in enumerate(rows[0], start=1): + ws.cell(row=header_row, column=j, value=name.replace("_", " ").title()) + _style_header(ws, header_row, len(rows[0])) + for i, r in enumerate(rows[1:], start=header_row + 1): + for j, val in enumerate(r, start=1): + cell = ws.cell(row=i, column=j, value=val) + cell.border = BORDER + cell.alignment = Alignment(vertical="top", wrap_text=True) + ws.freeze_panes = ws.cell(row=header_row + 1, column=1) + _autosize(ws) + return ws + + +def build() -> pathlib.Path: + wb = Workbook() + wb.remove(wb.active) + + idx = wb.create_sheet("Index") + idx["A1"] = "NIGHTWATCH — Mount & Roll-off 'Turret' Tender BOM" + idx["A1"].font = TITLE_FONT + lines = [ + "", + "This workbook is generated from design/mechanical/tender/bom/*.csv,", + "which are generated from the proven design/mechanical/calc/params.py.", + "", + "Sheets:", + " • Master BOM — every line item, all packages BP-01..BP-05", + " • Cut List — fabricated stock to ORDER & CUT (dual mm / inch)", + " • COTS Schedule — purchased / off-the-shelf items", + "", + "Dimensions are dual-unit (mm primary, inch in brackets).", + "Masses are STOCK mass (what you order), not finished-part net mass.", + "", + "STATUS: ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION.", + "The pier (BP-03) and roll-off roof (BP-04) require a licensed PE stamp.", + ] + for i, ln in enumerate(lines, start=2): + idx.cell(row=i, column=1, value=ln) + idx.column_dimensions["A"].width = 80 + + _sheet_from_csv(wb, "Master BOM", BOM / "master_bom.csv", "Master Bill of Materials — all packages") + _sheet_from_csv(wb, "Cut List", BOM / "cut_list.csv", "Cut List / Material Takeoff — fabricated stock") + _sheet_from_csv(wb, "COTS Schedule", BOM / "cots_schedule.csv", "Purchased / off-the-shelf items") + + wb.save(OUT) + return OUT + + +if __name__ == "__main__": + print("wrote", build()) diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/00_instructions_to_bidders.md b/design/mechanical/tender/packages/BP-00_master_dossier/00_instructions_to_bidders.md new file mode 100644 index 0000000..2e5a151 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/00_instructions_to_bidders.md @@ -0,0 +1,126 @@ +# 00 — Instructions to Bidders (ITB) + +**Project:** NIGHTWATCH Observatory — telescope mount + roll-off roof enclosure +**Document:** BP-00 / 00 — Instructions to Bidders, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets throughout the package. + +This tender is packaged as a **Technical Data Package structured per +MIL-STD-31000A** (product definition data, associated lists, and the "issued for +bid" data management state). The technical baseline is the parts registry +`design/mechanical/tender/gen/partspec.py` and the generated BOM/cut-list/COTS +schedule; the commercial baseline is this ITB together with `06_terms_and_conditions.md`. + +--- + +## 1. Eligibility + +A bidder is eligible to bid one or more packages if it can demonstrate, for each +package bid: + +| Package | Minimum qualification | +|---|---| +| BP-01 Mount head machining | 3-axis CNC milling of 6061-T6 aluminium; ability to hold **ISO 286 H7** bearing bores and produce a CMM inspection report; anodizing per **MIL-A-8625F Type III** (in-house or qualified sub) | +| BP-02 Precision turned parts | CNC/manual turning of 303 stainless per **ASTM A582**; concentricity ≤ 0.02 TIR; passivation per **ASTM A967** (in-house or qualified sub) | +| BP-03 Pier & foundation | Cast-in-place reinforced concrete per **ACI 301 / ACI 318**; ability to deliver a **PE-stamped** foundation design and **ACI concrete cylinder break** reports; hot-dip galvanizing per **ASTM A123** (sub OK) | +| BP-04 Roll-off roof | Structural steel welding per **AWS D1.1** with current **WPS/PQR and welder qualifications**; hot-dip galvanizing per **ASTM A123**; rail/drive installation | +| BP-05 COTS procurement | Authorized distribution / procurement of the specified commercial items; ability to provide manufacturer datasheets and certificates of conformance | + +Bidders shall submit evidence of the above (certifications, welder qual records, PE +registration in the project jurisdiction, sample inspection reports) with their +quote. A whole-job bidder shall evidence every package it self-performs and name its +subcontractors for the rest. + +## 2. Request-for-Information (RFI) process + +- All questions shall be submitted in writing to the Owner's technical + representative by the **RFI cut-off date (SCHEDULE PLACEHOLDER — TBD)**, no later + than **[N] business days** before the bid due date. +- Reference each question to a **NW-xx part number**, a drawing sheet, or a numbered + clause of this dossier. +- Answers are issued to **all** bidders as numbered addenda; only written addenda + amend the tender. Verbal guidance is not binding. +- **Every item flagged "ASSUMED design-intent — bidder/PE to confirm" is a valid RFI + subject.** Where a bidder's fabrication method needs a value the frozen design does + not fix (weld size, roof member size, rail/drive product, GD&T value, surface + finish, rebar/anchor sizing), raise it as an RFI rather than assuming silently. + +## 3. Quote validity + +Quotes shall remain **firm and open for acceptance for 90 calendar days** from the +bid due date unless a longer period is stated on the bid form. Prices shall be fixed +(not indexed) for that period. Any assumptions, exclusions, or clarifications a +bidder relies on shall be listed explicitly on the bid form; unstated assumptions +are not binding on the Owner. + +## 4. Basis of evaluation + +Bids are evaluated on **best overall value to the Owner**, not lowest price alone. +The evaluation considers: + +1. **Compliance** with the technical baseline (partspec dimensions, materials, + finishes, tolerances, and the applicable standards register) and completeness of + the required certs/reports. +2. **Total evaluated price** for the package(s) bid — labour + material + finish, + inclusive of the Inspection & Test Plan deliverables of `04_inspection_test_plan.md`. +3. **Lead time** and schedule fit (see §7). +4. **Qualification and past performance** on comparable precision-mechanical / + structural-steel / concrete work. +5. **Handling of ASSUMED design-intent items** — a bidder who prices these + transparently and flags confirmations needed is preferred over one who buries them. + +Award may be made **by individual package, by combination, or whole-job**, whichever +gives the Owner best overall value. The Owner may award different packages to +different bidders. The Owner is not bound to accept the lowest or any bid. + +## 5. Submission format + +Each bid shall contain, as separate clearly-labelled files: + +1. **Completed pricing schedule** (`05_bid_form.md` filled in) — per line item, per + package subtotal, whole-job total, and any alternates priced. +2. **Eligibility evidence** per §1. +3. **Compliance statement** — a line-by-line confirmation of conformance to each + applicable standard in `02_standards_register.md`, with any deviations listed. +4. **Assumptions & exclusions** — including every "ASSUMED design-intent" value the + bidder has resolved in order to price, stated for PE confirmation. +5. **Proposed lead time and schedule** against the placeholders in §7. +6. For BP-04: draft **WPS/PQR** references. For BP-03: the **PE** who will stamp the + foundation. For BP-01/02: sample **inspection report** formats. + +Preferred transmittal formats mirror the file-format map (README §4): **PDF** for +narrative and signed documents, **XLSX** for the priced schedule. + +## 6. Award and documentation + +The successful bidder shall, before fabrication, receive the released Technical Data +Package for the awarded package(s): the **STEP AP242** 3-D masters (BP-01/02/03 +machined items), **DXF/DWG** flat patterns (BP-03/04 cut parts), **PDF** control +drawings (`NW-xx` sheets), and the **XLSX** BOM / COTS schedule. All released data +remains marked "ISSUED FOR BID / FOR PE REVIEW" until the responsible PE stamps the +package for construction; **no fabrication for permanent installation shall proceed +on unstamped data** (see BP-03 pier and BP-04 roof, both PE gates). + +## 7. Schedule (PLACEHOLDERS — TBD) + +| Milestone | Date | +|---|---| +| Tender issued | **[TBD]** | +| RFI cut-off | **[TBD]** | +| Final addenda issued | **[TBD]** | +| Bids due | **[TBD]** | +| Evaluation / clarifications | **[TBD]** | +| Award / letter of intent | **[TBD]** | +| Released-for-construction data (post PE stamp) | **[TBD]** | +| Delivery / installation window | **[TBD]** | + +All dates are placeholders to be fixed in the issued tender. Bidders shall quote lead +times as **calendar weeks from award** on the bid form so schedules can be normalized +across partial and whole-job bids. + +--- + +*This ITB is a commercial framing document. It does not alter any dimension, material, +finish, or tolerance in the frozen technical baseline; where it and a control drawing +appear to conflict on a technical value, the **partspec/drawing governs** and the +conflict shall be raised as an RFI.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/01_statement_of_work_general.md b/design/mechanical/tender/packages/BP-00_master_dossier/01_statement_of_work_general.md new file mode 100644 index 0000000..412a183 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/01_statement_of_work_general.md @@ -0,0 +1,143 @@ +# 01 — Statement of Work (General) + +**Project:** NIGHTWATCH Observatory — telescope mount + roll-off roof enclosure +**Document:** BP-00 / 01 — General Statement of Work, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. + +--- + +## 1. Scope summary + +NIGHTWATCH is a small robotic astronomical observatory: a **counterweight-free German +equatorial mount (GEM)** carrying an Intes-Micro MN78 f/8 optical tube, mounted on an +**isolated reinforced-concrete pier**, inside a **roll-off roof enclosure**. This +tender procures the fabricated mechanical hardware and the commercial (COTS) hardware +for that build, organized into five packages: + +- **The mount** — a machined 6061-T6 aluminium head (RA + DEC housings and saddle, + BP-01), 303 stainless turned spindles and shaft (BP-02), driven by COTS harmonic + drives, angular-contact bearings, on-axis absolute encoders, and stepper + drivetrains (BP-05). +- **The foundation** — an isolated cast-in-place concrete pier with a galvanized steel + top plate, an anodized aluminium pier-to-mount adapter, and a cast-in anchor-bolt + set (BP-03). +- **The roll-off roof** ("disappearing turret") — a welded structural-steel roof frame + with rafters/purlins, box-track rails, wheel/drive/hold-down brackets, hot-dip + galvanized, with a COTS gate-operator drive, V-groove wheels, and metal roofing + (BP-04 fabrication + BP-05 purchase). + +The mechanical design is **frozen and proven** (`design/mechanical/MECHANICAL_DESIGN.md`, +6 PASS / 5 remediated-FAIL across eleven proofs). This SOW procures the build to that +baseline; it does not re-open design decisions. + +### 1.1 Proof-out remediations embedded in this scope + +The tender carries the five computed remediations from the proof-out; bidders shall +treat each as a **mandatory** feature of the parts they touch: + +| Remediation | Where it appears | Registry basis | +|---|---|---| +| **Angular-contact 7008 (RA) / 7006 (DEC) bearings**, back-to-back — replace deep-groove 6008/6006 | Housing seats NW-MH-001 (7008 seat) / NW-MH-002 (7006 seat); spindle journals NW-TP-002 / NW-TP-003; bearings NW-CO-003 / NW-CO-004 | Stiffness proof: 6008/6006 were ~98% of the 43.5″ deflection FAIL | +| **On-axis absolute encoder** (RA + DEC) — replaces homing-grade baseline | NW-CO-005 | Encoder proof: baseline 5.02″ RMS FAIL → on-axis ring 0.54″ | +| **4× wind hold-down anchors** | Brackets NW-RR-006 + anchor set NW-PF-004 | Wind proof: survival uplift 9.2 kN, 4× 2 klbf anchors → SF 3.9 | +| **Temperature-compensated focuser** | NW-CO-008 | Thermal proof: 22 K diurnal swing walks focus ~10× depth-of-focus | +| **48 V power pack** (v2 autonomy) | NW-CO-013 | Power proof: 12 V pack 3.3 h vs 10 h night; 48 V → 13.4 h | + +## 2. Site + +| Parameter | Value | Basis | +|---|---|---| +| Location | Central Nevada (high-desert site) | Config baseline (SOURCED) | +| Elevation | **1800 m [5906 ft]** | `params.SITE.elevation_m` | +| Latitude | **38.9° N** | `params.SITE.latitude_deg` | +| Air density | ISA at 1800 m (derived) — reduced density lowers wind drag but is used for the survival case | `params.SITE.air_density` (DERIVED) | +| Design wind | Operational gust and **105 mph survival** governs roof uplift | Wind proof (ASCE 7 framing) | +| Snow | Closed-roof snow case governs roof structure (10.8 kN, ~6.1× dead weight) | Enclosure proof | +| Seismic / foundation | Pier passes with governing SF 6.8, f_n 152 Hz; embedment > frost line, isolated from any building slab | Pier proof | + +**Site loads (wind/snow/seismic) are ASSUMED design-intent at the ASCE 7 level and +shall be confirmed by the responsible PE against the actual permitted site +(ground-snow load, wind exposure category, seismic design category).** Bidders on +BP-03 and BP-04 shall price to the loads the PE confirms. + +## 3. General responsibilities + +### 3.1 Owner (or Owner's engineer) provides +- The released Technical Data Package for each awarded package: STEP AP242 masters, + DXF/DWG flat patterns, PDF control drawings (`NW-xx`), and the XLSX BOM. +- The responsible **Professional Engineer** who reviews and stamps the pier (BP-03) + and the roof structure (BP-04) before construction. +- Confirmed site loads and the survey/benchmark for pier location and orientation to + latitude 38.9°. +- Coordination of cross-package interfaces where packages are awarded separately. + +### 3.2 Bidder / Contractor provides (for each package bid) +- All labour, materials to the specified stock (see cut list), consumables, finishes, + fixturing, and shipping to the delivery point. +- Fabrication to the frozen dimensions, materials, finishes, and tolerances of the + control drawings and partspec — **dual-unit** dimensions govern as drawn. +- The **Inspection & Test Plan** deliverables of `04_inspection_test_plan.md` + (CMM reports, weld NDE, concrete cylinder breaks, material certs, finish checks) as + a condition of acceptance. +- Resolution — via RFI — of any "ASSUMED design-intent" value needed to fabricate, + submitted for PE confirmation; the contractor shall not silently finalize an + assumed value. +- For BP-03/BP-04: the PE-stampable structural submittals (rebar/anchor design; weld + procedures and member-size confirmation) required before construction. + +## 4. Standards applicability matrix + +Which standards govern which package. Full text and scope in `02_standards_register.md`. + +| Standard(s) | BP-01 | BP-02 | BP-03 | BP-04 | BP-05 | Governs | +|---|:-:|:-:|:-:|:-:|:-:|---| +| MIL-STD-31000A | ● | ● | ● | ● | ● | Technical Data Package structure | +| ASME Y14.5-2018 (GD&T) | ● | ● | ● | ● | | Geometric dimensioning & tolerancing | +| ASME Y14.100 / .24 / .34 / .1 | ● | ● | ● | ● | ● | Drawing practice, types, associated lists/BOM, sheet/title block | +| ASME Y14.36 / ASME B46.1 | ● | ● | ○ | | | Surface texture symbols & measurement | +| ISO 286 (H7/js6) | ● | ● | ○ | | | Bearing-bore / journal fits | +| ISO 2768 (mK) | ● | ● | ● | ● | | General tolerances | +| ASTM B209 / AMS-QQ-A-250/11 | ● | | ● | | | 6061-T6 plate material | +| ASTM A582 | | ● | | | | 303 stainless bar | +| ASTM A36 | | | ● | ● | | Structural steel plate | +| ASTM A500 Gr B | | | | ● | | HSS members | +| ASTM A615 Gr60 | | | ● | | | Rebar | +| ASTM F1554 Gr36 | | | ● | ● | ● | Anchor bolts | +| ASTM A574 | ● | ● | ● | ● | ● | Socket-head cap screws | +| MIL-A-8625F Type III | ● | | ● | | | Hardcoat anodize (aluminium) | +| ASTM A123 | | | ● | ● | | Hot-dip galvanize (steel) | +| ASTM A967 | | ● | | | | Passivation (stainless) | +| AWS A2.4 / D1.1 (+ WPS/PQR) | | | ○ | ● | | Weld & NDE symbols; structural steel welding | +| AWS D1.2 | | | | ○ | | Aluminium welding (if any welded Al) | +| ACI 318 (Ch. 17) / ACI 301 | | | ● | ○ | | Concrete design/spec & anchoring | +| ASCE 7 | | | ● | ● | | Wind / snow / seismic loads | +| STEP ISO 10303 AP242 | ● | ● | ● | ○ | | 3-D / CNC exchange | +| DXF / DWG | | | ● | ● | | Flat-pattern cutting | +| PDF / XLSX | ● | ● | ● | ● | ● | Drawings / BOM formats | + +● = primary/applicable · ○ = conditionally applicable (as-noted / if that process is used). + +## 5. Interfaces (owner-coordinated on split awards) + +Key mating features held by shared datums across packages — a partial bidder builds +to their own published dimensions; the Owner coordinates fit: + +- **RA housing NW-MH-001** flange (104 mm [4.094 in] bolt circle, 8 holes) ↔ **RA + spindle NW-TP-002** (8× M4 on 104 PCD) ↔ **CSF-32 harmonic drive NW-CO-001** (bore + 80.0 mm [3.150 in]). +- **DEC housing NW-MH-002** (83 mm [3.268 in] bolt circle) ↔ **DEC spindle NW-TP-003** + ↔ **CSF-25 NW-CO-002** (bore 64.0 mm [2.520 in]). +- **Bearing seats** NW-MH-001 (Ø68.0 mm [2.677 in] H7, 7008) / NW-MH-002 (Ø55.0 mm + [2.165 in] H7, 7006) ↔ spindle journals ↔ COTS bearings NW-CO-003/004. +- **Pier top plate NW-PF-002** (12 in sq) ↔ **adapter plate NW-PF-003** (10 in sq) ↔ + **RA housing NW-MH-001** ↔ **anchor set NW-PF-004** (4× Ø19.05 mm [0.750 in] F1554). +- **Roof wheel brackets NW-RR-004** ↔ **V-groove wheels NW-CO-010** ↔ **rails + NW-RR-003**; **drive bracket NW-RR-005** ↔ **gate-operator NW-CO-011**; **hold-down + brackets NW-RR-006** ↔ **anchors** (mandatory wind remediation). + +--- + +*Package-specific SOWs (per-part fabrication notes, sequencing, and acceptance) +accompany each child package BP-01 … BP-05 and incorporate this general SOW by +reference.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/02_standards_register.md b/design/mechanical/tender/packages/BP-00_master_dossier/02_standards_register.md new file mode 100644 index 0000000..19a9ec9 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/02_standards_register.md @@ -0,0 +1,104 @@ +# 02 — Standards Register + +**Document:** BP-00 / 02 — Standards Register, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. + +Every standard is cited **by number**. Bidders shall comply with the latest issue in +force at bid date unless a specific edition is named. Where a standard is marked +*ASSUMED / PE-to-confirm*, the design does not yet fix the governed value and the +bidder/PE shall confirm it before construction. + +--- + +## 1. Technical Data Package + +| Standard | Governs | Applies to | +|---|---|---| +| **MIL-STD-31000A** | Technical Data Package: product-definition data set, associated lists, data-management "issued for" states (this package is issued at the *bid* state) | Whole dossier (BP-00 … BP-05) | + +## 2. Drawing practice, GD&T, and surface texture + +| Standard | Governs | Applies to | +|---|---|---| +| **ASME Y14.5-2018** | Geometric dimensioning & tolerancing (datums, feature control frames, position/flatness/concentricity) | BP-01, BP-02, BP-03, BP-04 machined/fabricated features | +| **ASME Y14.100** | Engineering drawing practices (general) | All control drawings `NW-xx` | +| **ASME Y14.24** | Drawing types (detail, assembly, control) — these sheets are *bid control drawings* | All control drawings | +| **ASME Y14.34** | Associated lists / bill of material (parts list, data list) | Master BOM, cut list, COTS schedule (XLSX) | +| **ASME Y14.1** | Sheet size and title-block format | All control drawings (ANSI B-class sheet, ASME-style title block) | +| **ASME Y14.36** | Surface texture symbols on drawings | BP-01, BP-02 (Ra callouts), BP-03 adapter | +| **ASME B46.1** | Surface texture (measurement of roughness Ra) | Verification of BP-01/BP-02 finish (see ITP) | + +**ASSUMED / PE-to-confirm — GD&T tolerance values.** The registry states general +tolerances (ISO 2768-mK) and critical callouts (H7 bores, h6/j6 journals, +concentricity 0.02 TIR, flange flatness 0.05); specific **feature-control-frame +values per Y14.5** (true position of bolt patterns, datum references) are ASSUMED +design-intent and shall be confirmed by the PE on the released drawings. + +## 3. Fits and general tolerances + +| Standard | Governs | Applies to | +|---|---|---| +| **ISO 286** | Limits & fits — **H7** bearing bores, **js6/j6/h6** journals | BP-01 seats (Ø68.0 mm [2.677 in] H7 RA; Ø55.0 mm [2.165 in] H7 DEC), BP-02 journals | +| **ISO 2768** (class **mK**) | General linear and angular tolerances for un-toleranced dimensions | BP-01, BP-02, BP-03, BP-04 | + +## 4. Materials + +| Standard | Governs | Applies to | +|---|---|---| +| **ASTM B209 / AMS-QQ-A-250/11** | 6061-T6 aluminium plate | BP-01 (NW-MH-001/002/003), BP-03 adapter (NW-PF-003) | +| **ASTM A582** | 303 free-machining stainless bar | BP-02 (NW-TP-001/002/003) | +| **ASTM A36** | Structural carbon-steel plate | BP-03 top plate (NW-PF-002); BP-04 rails/brackets (NW-RR-003/004/005/006) | +| **ASTM A500 Gr B** | Cold-formed welded HSS | BP-04 roof frame & purlins (NW-RR-001/002) | +| **ASTM A615 Gr60** | Deformed rebar | BP-03 pier cage (NW-PF-001) — *schedule ASSUMED, PE-to-confirm* | +| **ASTM F1554 Gr36** | Anchor bolts (headed/threaded) | BP-03 anchor set (NW-PF-004); BP-04 hold-down anchors — *sizing ASSUMED, PE-to-confirm* | +| **ASTM A574** | Alloy-steel socket-head cap screws | Fastener schedule (NW-CO-014), all bolted joints | + +## 5. Finishes + +| Standard | Governs | Applies to | +|---|---|---| +| **MIL-A-8625F Type III** | Hardcoat anodize (Class 1 clear, 0.002 in [0.051 mm]) | BP-01 aluminium (NW-MH-001/002/003), BP-03 adapter (NW-PF-003) | +| **ASTM A123** | Hot-dip galvanizing of steel | BP-03 top plate, anchor set; BP-04 all steel (NW-RR-001…006) | +| **ASTM A967** | Passivation of stainless (nitric) | BP-02 (NW-TP-001/002/003) | + +## 6. Welding and NDE + +| Standard | Governs | Applies to | +|---|---|---| +| **AWS A2.4** | Weld and NDE symbols on drawings | BP-04 weld callouts; BP-03 (if any steel welding) | +| **AWS D1.1** | Structural steel welding code, incl. **WPS / PQR and welder qualification** | BP-04 (NW-RR-001…006) | +| **AWS D1.2** | Structural aluminium welding | *Conditional* — only if any aluminium is welded (BP-01 items are machined, not welded, at baseline) | + +**ASSUMED / PE-to-confirm — weld sizes.** Fillet/groove sizes, joint details, and NDE +extent for BP-04 are ASSUMED design-intent (registry: "fully welded, gusseted" +corners). The bidder's **WPS/PQR** and the PE shall fix weld sizes and NDE scope +against the confirmed ASCE 7 loads. + +## 7. Foundation and loads + +| Standard | Governs | Applies to | +|---|---|---| +| **ACI 318** (incl. **Ch. 17** anchoring-to-concrete) | Reinforced-concrete design; cast-in / post-installed anchor capacity | BP-03 pier (NW-PF-001), anchor set (NW-PF-004), roof hold-down anchors | +| **ACI 301** | Specification for structural concrete (materials, placement, cylinder testing) | BP-03 concrete work | +| **ASCE 7** | Minimum design loads — **wind, snow, seismic** | BP-03 pier; BP-04 roof (survival-wind uplift & snow cases govern) | + +**PE-stamp gate.** BP-03 (pier + anchors) and the BP-04 roof structure are +**construction-gated on a PE-stamped analysis** against ASCE 7 loads confirmed for +the permitted site. Rebar schedule, anchor sizing, member sizing, and weld sizing are +ASSUMED design-intent until stamped. + +## 8. Deliverable data formats + +| Standard / format | Governs | Applies to | +|---|---|---| +| **STEP ISO 10303 AP242** | 3-D product model exchange for CNC | BP-01, BP-02, BP-03 machined items (`cad/*.scad` → STEP master) | +| **DXF / DWG** | 2-D flat-pattern cutting geometry | BP-03/BP-04 laser/waterjet-cut plates | +| **PDF** | Released drawing sheets | All `NW-xx` control drawings | +| **XLSX** | Bill of material / associated lists / COTS schedule | Master BOM, cut list, COTS schedule | + +--- + +*Standards are cited to establish acceptance criteria only; they do not change any +dimension in the frozen technical baseline. Where a standard and a control drawing +conflict on a value, the **drawing/partspec governs** and the conflict is an RFI item.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/03_drawing_register.md b/design/mechanical/tender/packages/BP-00_master_dossier/03_drawing_register.md new file mode 100644 index 0000000..a857ea5 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/03_drawing_register.md @@ -0,0 +1,88 @@ +# 03 — Drawing Register + +**Document:** BP-00 / 03 — Drawing Register, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. + +Every fabricated part in the parts registry carries one **control drawing** — a +to-scale stock envelope with the controlling dual-unit dimensions, a keyed +feature/dimension table, and an ASME-Y14-style title block (material, finish, +tolerance, GD&T status, and the "ISSUED FOR BID / FOR PE REVIEW" state). The sheet is +the dimensioned, quotable summary a shop reviews to price the job; the **3-D +machining master** for every machined feature is the parametric +`design/mechanical/cad/*.scad` and the **STEP (ISO 10303 AP242)** exported from it. + +This register lists `partspec.fabricated_parts()` — parts with a fabrication drawing +and non-COTS stock. **15 control drawings** across four fabrication packages. +Drawing practice per **ASME Y14.100 / Y14.24 / Y14.1**; associated list per +**ASME Y14.34**. + +--- + +## 1. Register + +| # | Part No. | Name | Pkg | Material | Finish | Process | Rev | Sheet | +|:-:|---|---|:-:|---|---|---|:-:|---| +| 1 | **NW-MH-001** | RA (polar) axis housing | BP-01 | 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 | Type III hardcoat anodize per MIL-A-8625F | CNC mill, 3-axis | A | `NW-MH-001.svg` → PDF | +| 2 | **NW-MH-002** | DEC axis housing + Losmandy-D saddle | BP-01 | 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 | Type III hardcoat anodize per MIL-A-8625F | CNC mill, 3-axis | A | `NW-MH-002.svg` → PDF | +| 3 | **NW-MH-003** | DEC saddle clamp bar | BP-01 | 6061-T6 aluminium plate per ASTM B209 / AMS-QQ-A-250/11 | Type III hardcoat anodize per MIL-A-8625F | CNC mill, 3-axis | A | `NW-MH-003.svg` → PDF | +| 4 | **NW-TP-001** | Counterweight shaft | BP-02 | 303 stainless per ASTM A582 | Passivate per ASTM A967 (nitric) | CNC turn | A | `NW-TP-001.svg` → PDF | +| 5 | **NW-TP-002** | RA drive spindle / drawbar adapter | BP-02 | 303 stainless per ASTM A582 | Passivate per ASTM A967 (nitric) | CNC turn + mill | A | `NW-TP-002.svg` → PDF | +| 6 | **NW-TP-003** | DEC drive spindle / saddle stub | BP-02 | 303 stainless per ASTM A582 | Passivate per ASTM A967 (nitric) | CNC turn + mill | A | `NW-TP-003.svg` → PDF | +| 7 | **NW-PF-001** | Reinforced-concrete telescope pier | BP-03 | Cast-in-place concrete f′c = 4000 psi (27.6 MPa) per ACI 318 / ACI 301 | none (concrete) | Cast-in-place | A | `NW-PF-001.svg` → PDF | +| 8 | **NW-PF-002** | Pier top plate | BP-03 | ASTM A36 structural steel | Hot-dip galvanize per ASTM A123 | Laser/waterjet cut + drill | A | `NW-PF-002.svg` → PDF | +| 9 | **NW-PF-003** | Pier-to-mount adapter plate | BP-03 | 6061-T6 aluminium per ASTM B209 | Type III hardcoat anodize per MIL-A-8625F | CNC mill | A | `NW-PF-003.svg` → PDF | +| 10 | **NW-RR-001** | Roof frame perimeter (HSS) | BP-04 | ASTM A500 Gr B HSS (steel) | Hot-dip galvanize per ASTM A123 | Cut + weld (AWS D1.1) | A | `NW-RR-001.svg` → PDF | +| 11 | **NW-RR-002** | Roof rafters / purlins | BP-04 | ASTM A500 Gr B HSS (steel) | Hot-dip galvanize per ASTM A123 | Cut + weld (AWS D1.1) | A | `NW-RR-002.svg` → PDF | +| 12 | **NW-RR-003** | Track rail beams (box track) | BP-04 | ASTM A36 structural steel | Hot-dip galvanize per ASTM A123 | Cut + weld / bolt to piers | A | `NW-RR-003.svg` → PDF | +| 13 | **NW-RR-004** | Wheel axle brackets | BP-04 | ASTM A36 structural steel | Hot-dip galvanize per ASTM A123 | Laser cut + weld | A | `NW-RR-004.svg` → PDF | +| 14 | **NW-RR-005** | Drive bracket + end stops | BP-04 | ASTM A36 structural steel | Hot-dip galvanize per ASTM A123 | Laser cut + weld | A | `NW-RR-005.svg` → PDF | +| 15 | **NW-RR-006** | Wind hold-down anchor brackets | BP-04 | ASTM A36 structural steel | Hot-dip galvanize per ASTM A123 | Laser cut + weld | A | `NW-RR-006.svg` → PDF | + +Sheets are held in the repo as SVG (`design/mechanical/tender/drawings/NW-xx.svg`) and +issued to bidders as **PDF**. All at **Rev A** at this issue. + +## 2. 3-D / cutting masters and format map + +| Package | 3-D machining master | Cutting geometry | Sheet | +|---|---|---|---| +| BP-01 (NW-MH-00x) | `cad/ra_housing.scad`, `cad/dec_housing.scad` → **STEP AP242** | — | PDF | +| BP-02 (NW-TP-00x) | `cad/counterweight_shaft.scad` (+ turned masters) → **STEP AP242** | — | PDF | +| BP-03 (NW-PF-00x) | `cad/pier_adapter.scad` → **STEP AP242** (adapter/top plate) | **DXF/DWG** plate profiles | PDF | +| BP-04 (NW-RR-00x) | `cad/assembly.scad` (assembly reference) | **DXF/DWG** flat patterns for laser/waterjet cut | PDF | + +The `.scad` source and its STEP export are the geometry of record for machined 3-D +features; the control sheet governs the **dimensioned, toleranced** callouts. Where +the STEP model and the sheet disagree, the **sheet governs** and it is an RFI item. + +## 3. Registry exceptions (no fabrication drawing) + +The following are in the parts registry but carry **no fabrication control drawing** +(they are purchased / buy-to-print): + +- **NW-PF-004** — Anchor-bolt set + template (BP-03): COTS ASTM F1554 Gr36 bolts plus + a fabricated template; listed on the **COTS schedule** and the BP-03 SOW, sizing + ASSUMED per ACI 318 Ch. 17 (PE to confirm for ~9.2 kN uplift). No `NW-PF-004.svg`. +- **NW-CO-001 … NW-CO-014** (BP-05): all commercial off-the-shelf; specified by + manufacturer datasheet on the COTS schedule, not by a NIGHTWATCH drawing. + +## 4. Notes on the register + +- **Revisions.** Part numbers are stable; changes advance the sheet revision. Rev A is + the issue-for-bid revision. A released-for-construction revision follows the PE stamp + (see BP-03/BP-04 gates). +- **ASSUMED design-intent on the sheets.** Several sheets carry values the frozen + design does not fix — Losmandy-D saddle/clamp geometry (NW-MH-002/003), spindle body + diameters (NW-TP-002/003), adapter geometry (NW-PF-003), rebar schedule (NW-PF-001), + roof member sizes and weld sizes (NW-RR-001…006). Each is tagged **ASSUMED + design-intent — bidder/PE to confirm** in the sheet's PROVENANCE/NOTE line and shall + be confirmed before construction. +- **Title-block status.** Every sheet's title block reads **"ISSUED FOR BID / FOR PE + REVIEW — NOT FOR CONSTRUCTION"** and shall not be used for construction until + re-issued post-stamp. + +--- + +*This register is generated from `partspec.fabricated_parts()`; if the parts registry +changes, regenerate the sheets (`python3 -m design.mechanical.tender.gen.drawing`) and +reconcile this table.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/04_inspection_test_plan.md b/design/mechanical/tender/packages/BP-00_master_dossier/04_inspection_test_plan.md new file mode 100644 index 0000000..d58b83a --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/04_inspection_test_plan.md @@ -0,0 +1,129 @@ +# 04 — Inspection & Test Plan (ITP) + +**Document:** BP-00 / 04 — Inspection & Test Plan, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. + +This ITP sets **acceptance criteria and hold/witness points** for each trade. The +deliverable reports named here are a **condition of acceptance and payment** and shall +be priced into every bid (see `00_instructions_to_bidders.md` §4). Verification +methods reference the standards register (`02_standards_register.md`). + +**Point types:** **H = Hold point** (work shall not proceed past this point until the +Owner/PE releases it) · **W = Witness point** (Owner may attend; contractor gives +notice, work may proceed if Owner declines) · **R = Review of records** (documentation +only) · **S = Surveillance** (contractor's own QC, records retained). + +--- + +## 1. BP-01 — Mount head machining (6061-T6, CNC) + +Parts: NW-MH-001 (RA housing), NW-MH-002 (DEC housing + saddle), NW-MH-003 (clamp bar). + +| # | Characteristic | Acceptance criterion | Method / standard | Point | +|:-:|---|---|---|:-:| +| 1.1 | Material | 6061-T6 per ASTM B209 / AMS-QQ-A-250/11 | **Mill certificate** (heat/lot traceable) | R | +| 1.2 | Bearing seat bore — RA | Ø **68.0 mm [2.677 in] H7** (+0.030/0), for 7008 pair | **CMM** dimensional report | **H** | +| 1.3 | Bearing seat bore — DEC | Ø **55.0 mm [2.165 in] H7** (+0.030/0), for 7006 pair | **CMM** dimensional report | **H** | +| 1.4 | Drive register bores | RA Ø **80.0 mm [3.150 in]** (CSF-32); DEC Ø **64.0 mm [2.520 in]** (CSF-25) | CMM | W | +| 1.5 | Flange bolt patterns | RA 8× on **104 mm [4.094 in]** PCD; DEC on **83 mm [3.268 in]** PCD; true position per Y14.5 | CMM | W | +| 1.6 | Flange face flatness | **0.05 mm [0.002 in]** flat | CMM / surface plate | W | +| 1.7 | General tolerances | ISO 2768-mK | Calipers/micrometer, first-article | S | +| 1.8 | Surface finish | **Ra 1.6 µm** (NW-MH-001/002), **Ra 3.2 µm** (NW-MH-003) | Profilometer per **ASME B46.1** | W | +| 1.9 | Finish | Type III hardcoat anodize, **0.002 in [0.051 mm]**, MIL-A-8625F Class 1 | Coating-thickness gauge; anodizer **CoC** | R | + +**First-Article Inspection (FAI):** full CMM report on the first RA and DEC housing is +a **hold point** before running the balance of the lot / releasing to anodize. + +## 2. BP-02 — Precision turned parts (303 SS, turned) + +Parts: NW-TP-001 (CW shaft — *optional, counterweighted variant only*), NW-TP-002 (RA +spindle), NW-TP-003 (DEC spindle). + +| # | Characteristic | Acceptance criterion | Method / standard | Point | +|:-:|---|---|---|:-:| +| 2.1 | Material | 303 stainless per ASTM A582 | **Mill certificate** | R | +| 2.2 | Bearing journals | RA j6 Ø **40.0 mm [1.575 in]** (7008); DEC j6 Ø **30.0 mm [1.181 in]** (7006) | **CMM / air gauge** | **H** | +| 2.3 | Concentricity | **0.02 mm [0.0008 in] TIR** journal-to-register | Between-centres runout / CMM | **H** | +| 2.4 | Drive registers | RA **80.0 mm [3.150 in]** (CSF-32); DEC **64.0 mm [2.520 in]** (CSF-25) | CMM | W | +| 2.5 | Bolt patterns | 8× M4 on RA **104 mm [4.094 in]** / DEC **83 mm [3.268 in]** PCD | CMM | W | +| 2.6 | General tolerances | ISO 2768-mK; journals h6/j6 per ISO 286 | First-article gauge | S | +| 2.7 | Surface finish | **Ra 0.8 µm** journals | Profilometer per ASME B46.1 | W | +| 2.8 | Finish | Passivate per ASTM A967 (nitric) | Passivation **CoC**; optional copper-sulphate / water-immersion test | R | + +## 3. BP-03 — Pier & foundation (RC + steel). **PE-STAMP GATE.** + +Parts: NW-PF-001 (RC pier), NW-PF-002 (steel top plate), NW-PF-003 (Al adapter), +NW-PF-004 (anchor set + template). + +| # | Characteristic | Acceptance criterion | Method / standard | Point | +|:-:|---|---|---|:-:| +| 3.1 | **Foundation design** | PE-stamped rebar/anchor design vs confirmed **ASCE 7** loads (survival-wind uplift ~9.2 kN, seismic) | Stamped calc + drawings, **ACI 318 Ch. 17** | **H** | +| 3.2 | Concrete mix | f′c = **4000 psi [27.6 MPa]** per ACI 301 | Approved mix design | **H** | +| 3.3 | Rebar cage | 6× #4 vertical + #3 ties @ 12 in oc (A615 Gr60) — *ASSUMED, PE-to-confirm* | Placement inspection **before pour** | **H** | +| 3.4 | Anchor template / setting | 4× Ø **19.05 mm [0.750 in]** F1554, embedment **304.8 mm [12.000 in]**, projection **50 mm [1.969 in]**; template hole tol **0.02 mm** | Survey of bolt pattern **before pour** | **H** | +| 3.5 | Concrete strength | Cylinder breaks meet f′c at 28 days (and 7-day check) | **ACI cylinder break** test (4 cylinders/pour min) | **H** | +| 3.6 | Pier geometry | Ø **304.8 mm [12.000 in]**; height above grade **914.4 mm [36.000 in]**; embedment > frost line; plumb **1:200**; top level **0.5°**; isolated from any slab | Survey / level | W | +| 3.7 | Top plate NW-PF-002 | **304.8 mm [12.000 in]** sq × **9.5 mm [0.375 in]**; hole pattern ±0.25 mm | Dimensional; **A123** galvanize CoC | W | +| 3.8 | Adapter NW-PF-003 | **254.0 mm [10.000 in]** sq × **19.05 mm [0.750 in]**; latitude-38.9° orientation | CMM; anodize CoC | W | +| 3.9 | Grout / seating | Non-shrink grout under top plate, full bearing | Visual | W | + +**Hold:** no concrete pour until 3.1, 3.3, and 3.4 are released. The pier is +**construction-gated on the PE stamp** (`02_standards_register.md` §7). + +## 4. BP-04 — Roll-off roof (structural steel, AWS D1.1) + +Parts: NW-RR-001 (frame), NW-RR-002 (rafters/purlins ×5), NW-RR-003 (rails ×2), +NW-RR-004 (wheel brackets ×8), NW-RR-005 (drive bracket ×4), NW-RR-006 (hold-down ×4). + +| # | Characteristic | Acceptance criterion | Method / standard | Point | +|:-:|---|---|---|:-:| +| 4.1 | Weld procedures & personnel | Qualified **WPS / PQR** and current **welder qualifications** | Records review, **AWS D1.1** | **H** | +| 4.2 | **Member sizing** | Roof member sizes vs confirmed **ASCE 7** snow (10.8 kN closed-roof case) + wind — *ASSUMED, PE-to-confirm* | PE-stamped submittal | **H** | +| 4.3 | Material | HSS **ASTM A500 Gr B** (NW-RR-001/002); plate **ASTM A36** (NW-RR-003…006) | Mill certificates | R | +| 4.4 | Weld quality | **Visual (VT) 100%** per AWS D1.1; **NDE** (MT/UT) on critical/CJP joints per PE-set extent — *weld sizes ASSUMED, PE-to-confirm* | VT + MT/UT, symbols per **AWS A2.4** | **H** | +| 4.5 | Frame geometry | Square **3 mm/m**, diagonal **5 mm**; gauge (rail spacing) matches roof span | Tape/laser measure | W | +| 4.6 | Rail straightness | **3 mm [0.118 in]** over run; gauge **2 mm**; rail run = 2× roof length so roof clears aperture | Survey / string line | W | +| 4.7 | Wheel brackets | 8×, bore for Ø4 in V-groove wheel (NW-CO-010), M16 axle; sized ≥ 4× roof + snow | Dimensional; load rating check | W | +| 4.8 | **Wind hold-down brackets NW-RR-006** | 4×, Ø **19.05 mm [0.750 in]** F1554 anchor, capacity **≥ 2 klbf each** → survival SF 3.9 (**MANDATORY** wind remediation) | Dimensional; anchor pull-test per **ACI 318 Ch.17** | **H** | +| 4.9 | Drive & interlocks | Gate-operator (NW-CO-011) mounted; hard end stops; **snow + wind interlocks** functional | Function test (open/close, interlock trip) | **H** | +| 4.10 | Finish | Hot-dip galvanize per **ASTM A123** | Coating-thickness gauge; galvanizer **CoC** | R | + +## 5. BP-05 — COTS procurement + +| # | Characteristic | Acceptance criterion | Method | Point | +|:-:|---|---|---|:-:| +| 5.1 | Item conformance | Each NW-CO-xxx matches specified part/rating on the COTS schedule | **Certificate of Conformance** + datasheet | R | +| 5.2 | Remediation items | 7008/7006 angular-contact **DB** pairs (NW-CO-003/004); on-axis absolute encoder (NW-CO-005); temp-comp focuser (NW-CO-008); 48 V pack (NW-CO-013) supplied **as specified** (no deep-groove/homing-grade substitution) | Datasheet verification | **H** | +| 5.3 | Bearings | 7008 bore Ø40 / OD Ø68 mm; 7006 bore Ø30 / OD Ø55 mm; back-to-back preload set | Manufacturer data; incoming dimensional check | W | +| 5.4 | Fasteners | A574 SHCS; F1554 Gr36 anchors, galvanized | Mill certs / CoC | R | +| 5.5 | Incoming inspection | No transit damage; quantities per schedule | Visual + count | S | + +**No substitution** of the five proof-out remediation items without written Owner/PE +approval — each closes a documented FAIL and a like-for-like commercial swap is not +equivalent. + +## 6. Documentation package (all trades) + +At delivery the contractor shall provide, per package: + +1. Material certificates / mill certs (traceable to heat/lot). +2. First-Article Inspection and **CMM reports** (BP-01/BP-02 dimensional). +3. **Weld records** — WPS/PQR, welder quals, VT/NDE reports (BP-04). +4. **Concrete records** — mix design, placement logs, **cylinder-break** results, + pre-pour rebar/anchor inspection sign-offs (BP-03). +5. **Finish certificates** — anodize (MIL-A-8625F), galvanize (A123), passivation + (A967) CoCs with thickness/verification data. +6. **Certificates of Conformance** for all COTS (BP-05). +7. As-built notes on any RFI-resolved ASSUMED design-intent value. + +Acceptance is conditional on a complete documentation package; a shortfall is grounds +to withhold acceptance of the affected line items. + +--- + +*Hold points marked **H** require Owner/PE release; the contractor shall give the +notice period stated in the awarded contract before proceeding. All ASSUMED +design-intent values (weld sizes, roof member sizing, rebar/anchor sizing, GD&T +values, surface finish, rail/drive product) remain **bidder/PE to confirm** and are +verified against the PE-stamped, released-for-construction data — not this bid issue.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/05_bid_form.md b/design/mechanical/tender/packages/BP-00_master_dossier/05_bid_form.md new file mode 100644 index 0000000..5d50845 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/05_bid_form.md @@ -0,0 +1,138 @@ +# 05 — Bid Form / Pricing Schedule + +**Document:** BP-00 / 05 — Bid Form, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. Prices in **USD**, exclusive of tax. + +**Bidder:** ______________________ **Date:** ____________ +**Quote valid for:** _______ days (≥ 90 per ITB §3) **Currency:** USD + +Fill the price cells. A bidder may price **any single package, any combination, or +the whole job** (README §2). Leave a package blank if not bidding it. Each line item +splits **Labour / Material / Finish**; enter a **Lead time** in calendar weeks from +award. Every fabricated line ships with its ITP deliverables (`04_inspection_test_plan.md`) +priced **in**. Quantities and stock are from the parts registry — do not re-scope. + +Legend: *Ext.* = extended (unit × qty). Blank cells are for the bidder. + +--- + +## BP-01 — Mount head machining (CNC, 6061-T6; Type III anodize) + +| Part No. | Description | Qty | Stock (from cut list) | Labour $ | Material $ | Finish $ | Ext. $ | Lead (wks) | +|---|---|:-:|---|---:|---:|---:|---:|:-:| +| NW-MH-001 | RA (polar) axis housing | 1 | plate 203.2×203.2×76.2 mm [8.000×8.000×3.000 in] | | | | | | +| NW-MH-002 | DEC axis housing + Losmandy-D saddle | 1 | plate 152.4×152.4×63.5 mm [6.000×6.000×2.500 in] | | | | | | +| NW-MH-003 | DEC saddle clamp bar | 1 | plate 101.6×76.2×25.4 mm [4.000×3.000×1.000 in] | | | | | | +| | **BP-01 subtotal** | | | | | | **$________** | | + +Deliverables priced in: CMM/FAI reports (H7 bores), mill certs, anodize CoC. + +## BP-02 — Precision turned parts (303 SS; passivate) + +| Part No. | Description | Qty | Stock | Labour $ | Material $ | Finish $ | Ext. $ | Lead (wks) | +|---|---|:-:|---|---:|---:|---:|---:|:-:| +| NW-TP-001 | Counterweight shaft **(OPTIONAL — counterweighted variant only)** | 1 | round Ø31.8 × 457.2 mm [Ø1.250 × 18.000 in] | | | | | | +| NW-TP-002 | RA drive spindle / drawbar adapter | 1 | round Ø88.9 × 101.6 mm [Ø3.500 × 4.000 in] | | | | | | +| NW-TP-003 | DEC drive spindle / saddle stub | 1 | round Ø76.2 × 88.9 mm [Ø3.000 × 3.500 in] | | | | | | +| | **BP-02 subtotal** | | | | | | **$________** | | + +Deliverables priced in: CMM report (journals, 0.02 TIR concentricity), mill certs, +passivation CoC. *NW-TP-001 is deleted in the baseline counterweight-free build; price +it as a separate add if the counterweighted variant is elected (see Alternates).* + +## BP-03 — Pier & foundation (RC + steel). **PE-STAMP GATE** + +| Part No. | Description | Qty | Stock | Labour $ | Material $ | Finish $ | Ext. $ | Lead (wks) | +|---|---|:-:|---|---:|---:|---:|---:|:-:| +| NW-PF-001 | Reinforced-concrete telescope pier | 1 | Ø304.8 × 1828.8 mm [Ø12.000 × 72.000 in] cast-in-place | | | — | | | +| NW-PF-002 | Pier top plate | 1 | plate 304.8×304.8×9.5 mm [12.000×12.000×0.375 in] A36 | | | | | | +| NW-PF-003 | Pier-to-mount adapter plate | 1 | plate 254.0×254.0×19.05 mm [10.000×10.000×0.750 in] 6061-T6 | | | | | | +| NW-PF-004 | Anchor-bolt set + template | 1 | 4× Ø19.05 × 304.8 mm [Ø0.750 × 12.000 in] F1554 + template | | | | | | +| | PE-stamped foundation design + cylinder-break testing (ITP 3.1/3.5) | 1 | lump sum | | | | | | +| | **BP-03 subtotal** | | | | | | **$________** | | + +Deliverables priced in: **PE stamp**, rebar/anchor design, **ACI cylinder breaks**, +galvanize CoC (top plate/anchors), anodize CoC (adapter). *Anchor/rebar sizing ASSUMED +— confirm against confirmed ASCE 7 loads.* + +## BP-04 — Roll-off roof (structural steel; galvanize; AWS D1.1) + +| Part No. | Description | Qty | Stock | Labour $ | Material $ | Finish $ | Ext. $ | Lead (wks) | +|---|---|:-:|---|---:|---:|---:|---:|:-:| +| NW-RR-001 | Roof frame perimeter (HSS) | 1 | HSS 50.8 sq × 3.2 wall mm [2.000 × 0.125 in], 12000 mm run | | | | | | +| NW-RR-002 | Roof rafters / purlins | 5 | HSS 38.1 sq × 3.2 wall mm [1.500 × 0.125 in] | | | | | | +| NW-RR-003 | Track rail beams (box track) | 2 | box track ~63.5 sq × 4.0 wall mm [2.500 × 0.157 in], run = 2× roof length | | | | | | +| NW-RR-004 | Wheel axle brackets | 8 | plate 101.6×76.2×9.5 mm [4.000×3.000×0.375 in] A36 | | | | | | +| NW-RR-005 | Drive bracket + end stops | 4 | plate 150.0×100.0×9.5 mm [5.906×3.937×0.375 in] A36 | | | | | | +| NW-RR-006 | Wind hold-down anchor brackets **(MANDATORY)** | 4 | plate 127.0×101.6×12.7 mm [5.000×4.000×0.500 in] A36 | | | | | | +| | WPS/PQR + welder quals + VT/NDE (ITP 4.1/4.4) | 1 | lump sum | | | | | | +| | Rail + drive install / commissioning (interlocks) | 1 | lump sum | | | | | | +| | **BP-04 subtotal** | | | | | | **$________** | | + +Deliverables priced in: **WPS/PQR**, welder quals, VT + NDE reports, galvanize CoC, +hold-down anchor pull-test. *Roof member sizes and weld sizes ASSUMED — confirm vs +ASCE 7 snow/wind (PE).* + +## BP-05 — COTS procurement schedule (purchase) + +| Part No. | Description | Qty | Unit $ | Ext. $ | Lead (wks) | +|---|---|:-:|---:|---:|:-:| +| NW-CO-001 | Harmonic drive, RA (CSF-32-100-2A-GR) | 1 | | | | +| NW-CO-002 | Harmonic drive, DEC (CSF-25-80-2A-GR) | 1 | | | | +| NW-CO-003 | Angular-contact bearing 7008 (RA pair, DB) — *remediation* | 2 | | | | +| NW-CO-004 | Angular-contact bearing 7006 (DEC pair, DB) — *remediation* | 2 | | | | +| NW-CO-005 | On-axis absolute encoder (RA + DEC) — *remediation* | 2 | | | | +| NW-CO-006 | Stepper motor NEMA17 + 27:1 planetary | 2 | | | | +| NW-CO-007 | Motor driver (TMC5160) / OnStepX board | 2 | | | | +| NW-CO-008 | Temperature-compensated focuser — *remediation* | 1 | | | | +| NW-CO-009 | Counterweights (5 kg×2, 2.5 kg×1) — *optional* | 3 | | | | +| NW-CO-010 | V-groove track wheels | 8 | | | | +| NW-CO-011 | Roof drive (gate operator) | 1 | | | | +| NW-CO-012 | Metal roofing panel + flashing (~9.0 m²) | 1 | | | | +| NW-CO-013 | 48 V LiFePO4 battery pack + solar — *remediation (v2)* | 1 | | | | +| NW-CO-014 | Fastener schedule (SHCS, anchors) | 1 | | | | +| | **BP-05 subtotal** | | | **$________** | | + +*No substitution of the remediation items (NW-CO-003/004/005/008/013) without written +Owner/PE approval — each closes a documented proof FAIL.* + +--- + +## Whole-job summary + +| Package | Subtotal $ | Lead time (wks) | +|---|---:|:-:| +| BP-01 Mount head machining | | | +| BP-02 Precision turned parts | | | +| BP-03 Pier & foundation | | | +| BP-04 Roll-off roof | | | +| BP-05 COTS procurement | | | +| **Sum of packages** | **$________** | | +| Whole-job integration / install / project management (whole-job bidders) | | | +| **WHOLE-JOB TOTAL** | **$________** | | + +## Alternates (price separately; do NOT include in base totals) + +| Ref | Alternate | Basis | Price Δ $ | Lead (wks) | +|---|---|---|---:|:-:| +| ALT-1 | **Counterweighted variant** — add NW-TP-001 shaft + NW-CO-009 weights | Baseline is counterweight-free (torque proof SF 2.5/2.6); §9 delta −$110 to delete | | | +| ALT-2 | **v2 sub-arcsec upgrade** — full on-axis RESA ring in lieu of hybrid encoder | Encoder proof: hybrid → full ring reaches 0.54″; §9 +$1,500–5,000 | | | +| ALT-3 | **Active-thermal enclosure** — ventilation + insulation + day pre-cool on the roll-off roof | Thermal FAIL + DGX heat finding; §9 +$300 | | | +| ALT-4 | **48 V off-grid power pack** (NW-CO-013) in lieu of grid + UPS | Power proof: 12 V 3.3 h → 48 V 13.4 h; §9 +$400 (v2) | | | +| ALT-5 | **Torque-motor direct drive** (precision-weighted upgrade) | Trade study: flips to torque motor when precision dominates | | | +| ALT-6 | **DISAPPEARING-TURRET variant** — retractable/telescoping turret in lieu of roll-off roof | **NOT engineered; requires its own PE-stamped structural proof first** (README §5). Concept price only. | | | + +*ALT-6 is a concept-level price only and is **not** a construction bid; it is priced +here solely to size the future scope and shall carry the bidder's stated assumptions.* + +--- + +**Bidder assumptions / exclusions / RFI-resolved ASSUMED values** (attach as needed): + +_______________________________________________________________________________ + +**Signed:** ______________________ **Title:** ______________ **Date:** __________ + +*Prices are firm for the validity period (ITB §3). Where this form and a control +drawing differ on a dimension, the **drawing/partspec governs**; raise it as an RFI.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/06_terms_and_conditions.md b/design/mechanical/tender/packages/BP-00_master_dossier/06_terms_and_conditions.md new file mode 100644 index 0000000..7f8fedc --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/06_terms_and_conditions.md @@ -0,0 +1,100 @@ +# 06 — Terms & Conditions (Commercial) + +**Document:** BP-00 / 06 — Terms & Conditions, Rev A +**Status:** **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** +**Units:** mm primary, [inch] in brackets. + +These are **brief commercial placeholders** to frame a bid. They are **not legal +advice** and shall be replaced/superseded by the Owner's executed purchase order or +contract, which governs on conflict. Bracketed **[TBD]** values are set at award. + +--- + +## 1. Scope of agreement +The awarded contractor shall furnish the labour, materials, finishes, inspection, and +documentation for the awarded package(s) BP-01 … BP-05 as defined by the released +Technical Data Package (partspec-derived control drawings `NW-xx`, STEP/DXF masters, +BOM, and the ITP `04_inspection_test_plan.md`), per **MIL-STD-31000A**. Award may be +by package, combination, or whole-job (README §2). + +## 2. Price and payment +- Prices are those on the completed **bid form** (`05_bid_form.md`), firm for the + quote-validity period (ITB §3). +- Payment terms: **[TBD — e.g. Net 30 from acceptance]**. Progress/milestone payments + (e.g. material release, FAI acceptance, PE stamp, delivery) may be scheduled at award. +- Payment of any line item is **conditioned on its ITP acceptance** (hold-point + releases, CMM/weld/concrete/finish records, COTS CoCs). + +## 3. Delivery +- **Delivery point / Incoterms: [TBD]** (e.g. FCA contractor's works, or DAP central + Nevada site at 1800 m [5906 ft] elevation). +- **Lead time** runs in calendar weeks from award, as quoted per line item on the bid + form. Time is of the essence for the PE-gated items (BP-03 pier, BP-04 roof). +- Risk of loss and packaging/preservation for transit are the contractor's until the + agreed delivery point; galvanized/anodized/passivated surfaces shall be protected. + +## 4. Title +Title to conforming goods passes to the Owner on **[the later of delivery and +payment — TBD]**. Title to materials and work-in-progress for which the Owner has made +progress payment passes on payment, with the contractor retaining care, custody, and +control until delivery. + +## 5. Warranty +- The contractor warrants that all work is **free from defects in material and + workmanship** and **conforms to the released drawings, specified materials, + finishes, tolerances, and the cited standards** for **[TBD — e.g. 12 months from + acceptance / 18 months from delivery]**. +- Finishes are warranted to their standard (MIL-A-8625F Type III anodize; ASTM A123 + galvanize; ASTM A967 passivation). +- Structural work (BP-03 pier, BP-04 roof) is warranted as built to the **PE-stamped, + released-for-construction** design; the Owner-provided PE design intent itself is not + a contractor warranty item except as to conforming execution. +- Remedy: repair, replace, or re-work non-conforming items at the contractor's cost. + COTS items (BP-05) pass through the **manufacturer's warranty**. + +## 6. Change orders +- No change to scope, dimensions, materials, finishes, or the technical baseline is + effective except by **written change order** signed by the Owner. +- Resolution of any **"ASSUMED design-intent — bidder/PE to confirm"** value (weld + sizes, roof member sizing, rail/drive product, GD&T values, surface finish, + rebar/anchor sizing) that alters cost, schedule, or scope shall be handled as a + change order following the **RFI** process (ITB §2). Silent deviation from the + released baseline is not authorized. +- Verbal instructions are not change orders. + +## 7. Intellectual property / ownership of drawings +- All drawings, the parts registry, STEP/DXF models, the BOM, and this dossier are the + **Owner's property**, furnished **solely** to bid and, if awarded, fabricate the + NIGHTWATCH work. +- They **shall not** be copied, disclosed to third parties, or used for any other + project without the Owner's written consent, and shall be returned or destroyed on + request. Subcontractors are bound to the same restriction. +- The contractor retains its own pre-existing manufacturing know-how; deliverable + fabricated goods and their inspection records are the Owner's on payment. +- **Status control:** documents remain marked **"ISSUED FOR BID / FOR PE REVIEW — NOT + FOR CONSTRUCTION"** until re-issued post-PE-stamp. No permanent-installation + fabrication shall proceed on unstamped data (BP-03/BP-04 gates). + +## 8. Compliance, permits, and PE stamp +- The contractor shall comply with applicable OSHA / jurisdictional safety and + building requirements for the work it performs. +- **Permits and the Professional Engineer's stamp** for the pier and roof structure are + **[TBD — Owner-provided per SOW §3.1 unless the contract assigns them to the + contractor]**. Construction of PE-gated items shall not begin on unstamped data. + +## 9. Liability, indemnity, insurance +- **[TBD]** — limits of liability, mutual indemnity for third-party claims, and + required insurance (general liability, workers' comp, professional/E&O for any + design-delegated structural work) are set at award. + +## 10. Termination and disputes +- **[TBD]** — termination for convenience / for cause, and the governing law and + dispute-resolution forum (venue, mediation/arbitration) are set at award. Absent a + stated choice, the law of the project jurisdiction (Nevada) applies. + +--- + +*These commercial terms do not alter any technical value in the frozen baseline. On +any conflict between these terms and the executed purchase order/contract, the **PO/ +contract governs**; on any conflict with a control drawing on a technical value, the +**drawing/partspec governs** and the conflict is an RFI item.* diff --git a/design/mechanical/tender/packages/BP-00_master_dossier/README.md b/design/mechanical/tender/packages/BP-00_master_dossier/README.md new file mode 100644 index 0000000..7ccc685 --- /dev/null +++ b/design/mechanical/tender/packages/BP-00_master_dossier/README.md @@ -0,0 +1,142 @@ +# NIGHTWATCH Observatory — Master Tender Dossier (BP-00) + +**ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +Document set: `BP-00` (umbrella). Rev A. Units: **mm primary, [inch] in brackets**. +Technical Data Package structured per **MIL-STD-31000A**. + +--- + +This dossier is the umbrella over five bid packages (BP-01 … BP-05) that together +build the NIGHTWATCH telescope mount and its roll-off roof enclosure. The mechanical +engineering is proven out and frozen (see `design/mechanical/MECHANICAL_DESIGN.md`); +this dossier and its child packages are the **procurement documents** that let a shop +price and fabricate the work. Every controlling dimension quoted anywhere in the +dossier is pulled from the parts registry `design/mechanical/tender/gen/partspec.py` +and the generated Bill of Materials — no dimension is invented here. + +## 1. Package index + +| Pkg | Title | Trade | Line items | Fab drawings | Key vendor deliverables | +|---|---|---|:-:|:-:|---| +| **BP-01** | Mount head machining | CNC machining, 6061-T6 aluminium | 3 | 3 | 3× machined housings/bar, Type III hardcoat anodize, CMM report on H7 bores, material certs | +| **BP-02** | Precision turned parts | CNC / manual turning, 303 stainless | 3 | 3 | 3× turned spindles/shaft, passivation cert, concentricity report, material certs | +| **BP-03** | Pier & foundation | Reinforced concrete + structural steel | 4 | 3 | Cast-in-place RC pier, galvanized top plate, anodized adapter plate, cast-in anchor set + template. **PE stamp + concrete cylinder breaks required** | +| **BP-04** | Roll-off roof ("disappearing turret") | Structural steel fab + rail/drive install | 6 | 6 | Welded HSS roof frame, rafters/purlins, box-track rails, wheel/drive/hold-down brackets, hot-dip galvanized. **AWS D1.1 WPS/PQR + welder quals required** | +| **BP-05** | COTS procurement schedule | Purchase (buy-to-print / off-the-shelf) | 14 | 0 | Harmonic drives, angular-contact bearings, on-axis encoders, motors/drivers, focuser, wheels, roof drive, roofing, 48 V power pack, fasteners, anchor set | + +Line-item and fabricated-drawing counts above are the current registry output +(`python3 -m design.mechanical.tender.gen.partspec`). Total across the job: +**30 registry line items**, **15 fabricated control drawings** (BP-05 and the +anchor set NW-PF-004 are buy items and carry no fabrication drawing). + +## 2. BID IN PARTS OR WHOLE + +A vendor may bid **any single package, any combination of packages, or the whole +job.** There is no requirement to bid the complete scope. + +- **Single-trade shops** — a machine shop may bid BP-01 and/or BP-02 alone; a steel + fabricator may bid BP-04 alone; a concrete/foundation contractor may bid BP-03 + alone; a distributor may quote BP-05 alone. +- **Line-item bidding** — within a package a bidder may price individual NW-xx line + items. The pricing schedule (`05_bid_form.md`) has one row per line item plus a + per-package subtotal and a whole-job total, so partial bids are first-class. +- **Whole-job bidding** — a general/turnkey bidder prices every package plus the + integration/install scope and submits the whole-job total. +- **Interfaces stay the owner's risk to coordinate.** Mating features are held on the + drawings by shared bolt patterns and registers (e.g. NW-MH-001 flange ↔ NW-TP-002 + register; NW-PF-002 ↔ NW-PF-003 hole pattern; NW-RR-004 bracket ↔ NW-CO-010 + wheel). A partial bidder is responsible only for their own package's conformance to + the published dimensions and datums; the owner coordinates cross-package fit. + +Award may be made by package, so price each package to stand alone. + +## 3. Part-numbering scheme (NW-xx) + +Every fabricated or purchased item carries a stable part number of the form +**`NW--`**: + +| Field | Meaning | +|---|---| +| `NW` | Project prefix — NIGHTWATCH | +| `TT` | Trade/type code: **MH** = mount-head machined, **TP** = turned part, **PF** = pier/foundation, **RR** = roll-off roof, **CO** = commercial off-the-shelf | +| `NNN` | Sequential item within the trade (001, 002, …) | + +The trade code maps one-to-one onto the bid package: MH→BP-01, TP→BP-02, PF→BP-03, +RR→BP-04, CO→BP-05. A part number never changes across revisions; the sheet revision +(Rev A at issue) tracks changes. Control drawings are named `NW--.svg` +(PDF at issue) and match the part number exactly. + +## 4. File-format map (which trade gets what) + +The 3-D machining master for every fabricated part is the parametric +`design/mechanical/cad/*.scad` and the **STEP (ISO 10303 AP242)** exported from it. +The dimensioned bid control drawing is the per-part sheet. Deliverable formats a +bidder receives / returns by trade: + +| Package | Trade | Native/CNC geometry | Cutting / flat pattern | Drawing | BOM / schedule | +|---|---|---|---|---|---| +| BP-01 Mount head machining | 3-axis CNC mill | **STEP AP242** (3-D master) | — | **PDF** (`NW-MH-00x`) | **XLSX** | +| BP-02 Precision turned parts | CNC / manual turn | **STEP AP242** | — | **PDF** (`NW-TP-00x`) | **XLSX** | +| BP-03 Pier & foundation | RC + steel + anodized adapter | **STEP AP242** (adapter/top plate) | **DXF/DWG** (plate profiles) | **PDF** (`NW-PF-00x`) | **XLSX** | +| BP-04 Roll-off roof | Steel fab (cut + weld) | STEP AP242 (assembly ref) | **DXF/DWG** (flat-pattern laser/waterjet cut) | **PDF** (`NW-RR-00x`) | **XLSX** | +| BP-05 COTS procurement | Purchase | — | — | Vendor datasheets (PDF) | **XLSX** (COTS schedule) | + +Rule of thumb: **STEP AP242 for anything CNC-machined in 3-D; DXF/DWG for anything +cut flat and welded; PDF for every dimensioned drawing; XLSX for every BOM and the +COTS schedule.** Drawings and BOM are also carried in this repo as generator source +(SVG sheets, CSV BOM) so the package regenerates deterministically. + +## 5. DISAPPEARING TURRET — VARIANT APPENDIX + +The observatory's popular name is the "disappearing turret." **The primary, +tendered, and proven scope is the roll-off roof of BP-04** — a rectangular steel +roof frame that rolls its own length off the aperture on box-track rails, selected +in the trade study (Weighted-Pugh 3.92, the winner at default and cost-heavy +weights) and proven in the enclosure and wind proofs (drive move-force 235 N at +SF 2.1; snow case governs; survival-wind hold-down anchors mandatory). Bidders +should price BP-04 as the delivered enclosure. + +**A retractable / telescoping vertical turret is documented here as a future +ALTERNATE only.** A turret that lowers or telescopes the whole enclosure below a +parapet has architectural appeal but is **not engineered in this data package** and +must not be priced as the base scope. Any bidder proposing it must treat it as a +separate, later procurement subject to these gates: + +- **It needs its own structural proof first.** None of the eleven Phase-C proofs + covers a telescoping turret: its lifting structure, guide columns, seals, snow and + survival-wind load paths, and drive are all unproven. The roll-off roof's wind + proof (survival uplift 9.2 kN, hold-downs → SF 3.9) and snow proof (10.8 kN closed + roof) do not transfer. +- **It is an ASSUMED design-intent concept — bidder/PE to confirm.** Member sizing, + drive selection, seal detail, and foundation reactions are all open. +- **Bid it as a priced alternate**, in the "Alternates" block of `05_bid_form.md`, + clearly separated from the base roll-off scope, and flagged as requiring an + independent PE-stamped structural analysis before it can be built. + +Until that analysis exists, the roll-off roof (BP-04) is the only enclosure +authorized for construction pricing. + +## 6. Dossier contents + +| File | Purpose | +|---|---| +| `README.md` | This index — packages, bid-in-parts rules, numbering, formats, turret appendix | +| `00_instructions_to_bidders.md` | ITB — eligibility, RFI, validity, evaluation, submission, schedule | +| `01_statement_of_work_general.md` | General SOW — scope, site, responsibilities, standards applicability | +| `02_standards_register.md` | Full standards table and what each governs | +| `03_drawing_register.md` | Every control drawing (part_no, name, pkg, material, rev, sheet) | +| `04_inspection_test_plan.md` | ITP — acceptance criteria per trade, hold/witness points | +| `05_bid_form.md` | Pricing schedule the vendor fills (per package, per line item, whole-job, alternates) | +| `06_terms_and_conditions.md` | Commercial T&Cs placeholders | + +Child bid packages BP-01 … BP-05 (their SOWs, per-part fabrication notes, and +package-specific ITPs) live under their own `packages/BP-0x_*/` directories and +inherit every general document in this dossier. + +--- + +*Every "ASSUMED design-intent" item in this dossier and its children is a value the +proven design does not yet fix (weld sizes, roof member sizing, rail/drive product +selection, GD&T tolerance values, surface finish, rebar/anchor sizing). Each is +explicitly flagged for the **bidder/PE to confirm** and is never presented as final.* diff --git a/design/mechanical/tender/packages/BP-01_mount_machining/README.md b/design/mechanical/tender/packages/BP-01_mount_machining/README.md new file mode 100644 index 0000000..441f43f --- /dev/null +++ b/design/mechanical/tender/packages/BP-01_mount_machining/README.md @@ -0,0 +1,134 @@ +# BP-01 — Mount Head Machining (CNC 6061-T6) — Bidder README + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | **BP-01 — Mount head machining** | +| Trade | CNC machining (6061-T6 aluminium), 3-axis mill | +| Parts | NW-MH-001, NW-MH-002, NW-MH-003 (3 fabricated line items) | +| Branch / project | `design/mechanical-tender` — NIGHTWATCH Observatory Mount & Roll-off Tender | +| Issue date | 2026-08-06 | +| Revision | A | +| Governing TDP standard | MIL-STD-31000A (Technical Data Package) | + +This README is the entry point for the machine shop bidding BP-01. It defines the **scope**, the +**data package the shop receives**, the **bidder deliverables**, and the items still carried as +**ASSUMED design-intent**. Read it with `SOW.md` (the per-part machining specification) and +`acceptance.md` (the CMM dimensional acceptance plan) in this same folder. + +--- + +## 1. Scope + +BP-01 covers the **mount head only** — the three fabricated 6061-T6 aluminium parts that form the +right-ascension (RA) and declination (DEC) axis housings and the dovetail saddle clamp. The shop: + +1. Procures **6061-T6 plate** to spec, with material certification. +2. CNC-machines the three parts to the STEP master and the NW-MH control drawings. +3. Applies **Type III hardcoat anodize** per MIL-A-8625F Class 1 (clear). +4. Inspects per `acceptance.md` and delivers finished, certified, first-article-inspected parts. + +**Out of scope for BP-01** (separate packages — the head *interfaces* to these but the shop does not +supply them): + +| Interface | Supplied under | Notes | +|---|---|---| +| Counterweight shaft, RA drive spindle, DEC drive spindle | BP-02 (303 SS precision turned) | Spindles carry the bearing inner races that seat in the BP-01 housings. | +| Concrete pier, pier top plate, pier-to-mount adapter | BP-03 (pier & foundation) | The RA housing mounts to the adapter plate (NW-PF-003). | +| Roll-off roof steelwork | BP-04 | Not a mount-head interface. | +| Harmonic drives, angular-contact bearings, on-axis encoders, motors, fasteners | BP-05 (COTS) | See §4 interface list. | + +--- + +## 2. Data package the shop receives (per MIL-STD-31000A) + +| Item | Format / standard | Role | +|---|---|---| +| 3-D solid master | **STEP ISO 10303 AP242** | **CNC authority** — governs as-modelled geometry. | +| Control drawings NW-MH-001 / -002 / -003 | **PDF** (drawing practices per ASME Y14.100; drawing types Y14.24; sheet & title block Y14.1) | Envelope + title block + key dimensions & features; bid control drawing. | +| `SOW.md` | Markdown | Per-part machining spec: datums, H7 bores, registers, saddle, finish, GD&T, certs. | +| `acceptance.md` | Markdown | CMM dimensional acceptance of the H7 bores + flatness; FAI requirements. | +| `cut_list.csv` (rows for NW-MH-001/002/003) | CSV/XLSX | Stock size, cut allowance, stock mass. | + +**Order of precedence.** The **STEP AP242 solid governs geometry**; the drawing + SOW govern +tolerances, notes, finish, and material; where geometry and the tolerance package disagree, the **SOW +controls** and the bidder shall raise an RFI before cutting metal. All GD&T is interpreted per +**ASME Y14.5-2018**; general tolerances per **ISO 2768-mK**; fits per **ISO 286**. + +--- + +## 3. Line-item summary (source: `partspec.parts_for('BP-01')` + `cut_list.csv`) + +| Part no. | Name | Stock (plate) | Qty | Stock mass | Finish | Drawing | cut_list row | +|---|---|---|---:|---:|---|---|---| +| **NW-MH-001** | RA (polar) axis housing | 203.2 mm [8.000 in] × 203.2 mm [8.000 in] × 76.2 mm [3.000 in] | 1 | 8.50 kg | Type III hardcoat anodize | NW-MH-001 | row 2 | +| **NW-MH-002** | DEC axis housing + Losmandy-D saddle | 152.4 mm [6.000 in] × 152.4 mm [6.000 in] × 63.5 mm [2.500 in] | 1 | 3.98 kg | Type III hardcoat anodize | NW-MH-002 | row 3 | +| **NW-MH-003** | DEC saddle clamp bar | 101.6 mm [4.000 in] × 76.2 mm [3.000 in] × 25.4 mm [1.000 in] | 1 | 0.53 kg | Type III hardcoat anodize | NW-MH-003 | row 4 | + +Stock mass is the **quantity ordered** (stock volume × 6061-T6 density), not finished net mass. Cut +allowance is 3.0 mm on stock. Material for all three: **6061-T6 aluminium plate per ASTM B209 / +AMS-QQ-A-250/11**. + +--- + +## 4. Cross-package interfaces the shop must preserve + +The bearing seats and drive registers are mating features to other packages. The shop machines the +BP-01 side; the datums must be held so the mating COTS/turned parts assemble: + +| BP-01 feature | Mates to | Part | +|---|---|---| +| NW-MH-001 bearing seat Ø68 H7 | Angular-contact **7008** DB pair (OD 68 mm) | NW-CO-003 (BP-05) | +| NW-MH-001 drive register Ø80 mm | **CSF-32-100** harmonic drive (bore 80 mm) via RA spindle | NW-CO-001 / NW-TP-002 | +| NW-MH-002 bearing seat Ø55 H7 | Angular-contact **7006** DB pair (OD 55 mm) | NW-CO-004 (BP-05) | +| NW-MH-002 drive register Ø64 mm | **CSF-25-80** harmonic drive (bore 64 mm) via DEC spindle | NW-CO-002 / NW-TP-003 | +| Axis (through-bore) | **On-axis absolute encoder ring** | NW-CO-005 (BP-05) | +| NW-MH-002 Losmandy-D saddle + NW-MH-003 clamp | OTA dovetail | (owner-supplied OTA) | + +> **Remediation context.** The bearing seats are sized for **angular-contact 7008 (RA) / 7006 (DEC)** +> pairs in a **back-to-back (DB), preloaded** arrangement — the proven remediation of the deep-groove +> 6008/6006 baseline, which the stiffness proof showed to be ~98% of the pointing-deflection FAIL. +> This is a **moment-stiffness** decision: seat coaxiality and bore-to-face perpendicularity are the +> stiffness-critical callouts (see `SOW.md` §4 and MECHANICAL_DESIGN.md §9). + +--- + +## 5. Bidder deliverables + +1. Finished, anodized parts NW-MH-001 / -002 / -003 (qty 1 each). +2. **Material certification / CoC** for the 6061-T6 plate (ASTM B209 / AMS-QQ-A-250/11), with heat/lot + traceability and T6 temper confirmation. +3. **Anodize certification** — MIL-A-8625F Type III Class 1, coating thickness record. +4. **First-Article Inspection (FAI) report** per `acceptance.md`, including the CMM results for every + H7 bore and flatness callout. +5. RFI / nonconformance log for any ASSUMED value the shop needed resolved. + +--- + +## 6. ASSUMED design-intent — bidder / PE to confirm (NOT final) + +The following are carried as **ASSUMED design-intent** in the parts registry and must **not** be +treated as released dimensions. The bidder/PE shall confirm before production: + +- **Flange bolt-circle diameters and patterns** — NW-MH-001 Ø104.0 mm [4.094 in] × 8 holes; NW-MH-002 + Ø83.0 mm [3.268 in] (DERIVED ~1.3× drive bore; not fixed in the design). +- **Losmandy-D saddle geometry** on NW-MH-002 (76.2 mm [3.000 in] width, 15° dovetail) — ASSUMED to a + standard Losmandy-D profile; undercut/relief not dimensioned in the repo. +- **NW-MH-003 saddle clamp bar** — the entire part is ASSUMED design-intent; the Losmandy-D clamp is + not dimensioned in the repo. +- **GD&T tolerance values** — coaxiality, perpendicularity, and position values are ASSUMED pending PE. +- **Bearing-seat surface finish and preload method** (shim vs. clamp) — ASSUMED, bidder/PE to detail. +- **Encoder-ring register location** on the axis — ASSUMED; preserve the through-bore. + +--- + +## 7. References + +- Parts registry: `design/mechanical/tender/gen/partspec.py` → `parts_for('BP-01')` (single source of truth). +- BOM / cut list: `design/mechanical/tender/bom/{master_bom,cut_list}.csv`. +- Control drawings: `design/mechanical/tender/drawings/NW-MH-00{1,2,3}.svg`. +- Design basis: `design/mechanical/MECHANICAL_DESIGN.md` §9 (selected config + remediations); + `design/mechanical/tradestudy/SECTION.md` (selection rationale, item 8 bearing fix). +- Standards register: MIL-STD-31000A, ASME Y14.5-2018 / Y14.100 / Y14.24 / Y14.1 / Y14.36, ASME B46.1, + ISO 286, ISO 2768, MIL-A-8625F, ASTM B209 / AMS-QQ-A-250/11. diff --git a/design/mechanical/tender/packages/BP-01_mount_machining/SOW.md b/design/mechanical/tender/packages/BP-01_mount_machining/SOW.md new file mode 100644 index 0000000..47c69e9 --- /dev/null +++ b/design/mechanical/tender/packages/BP-01_mount_machining/SOW.md @@ -0,0 +1,194 @@ +# BP-01 — Statement of Work: Mount Head Machining (CNC 6061-T6) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-01 — Mount head machining | +| Parts | NW-MH-001 (RA housing), NW-MH-002 (DEC housing + saddle), NW-MH-003 (saddle clamp bar) | +| Process | CNC mill, 3-axis | +| Material | 6061-T6 aluminium plate per **ASTM B209 / AMS-QQ-A-250/11** | +| Finish | **MIL-A-8625F** Type III hardcoat anodize, 0.002 in, Class 1 (clear) | +| Issue date / rev | 2026-08-06 / A | + +**Purpose.** This SOW specifies the machining, tolerancing, finishing, and certification requirements +for the three fabricated mount-head parts. It is read with the STEP ISO 10303 AP242 solid master (CNC +authority), the NW-MH control drawings (PDF), and `acceptance.md`. All dimensions are dual-unit, +**mm primary, inch in brackets**, drawn from the parts registry (`partspec.parts_for('BP-01')`) and +`cut_list.csv` — no dimension herein is invented. + +**Standards applied.** MIL-STD-31000A (TDP); **ASME Y14.5-2018** (GD&T); ASME Y14.100 (drawing +practices), Y14.24 (drawing types), Y14.1 (sheet/title block); **ASME Y14.36 & ASME B46.1** (surface +texture); **ISO 286** (H7/js6 fits); **ISO 2768-mK** (general tolerances); **MIL-A-8625F** (hardcoat +anodize); **ASTM B209 / AMS-QQ-A-250/11** (6061-T6 plate). + +--- + +## 1. General requirements (all three parts) + +1. **Material & certification.** 6061-T6 plate per **ASTM B209 / AMS-QQ-A-250/11**, T6 temper. + Furnish mill certification and Certificate of Conformance with **heat/lot traceability**; mark each + part with part number and revision per ASME Y14.100. +2. **General tolerance.** **ISO 2768-mK** (medium, fine machining class) on all dimensions not + otherwise toleranced. Break all sharp edges 0.3–0.5 mm [0.012–0.020 in] unless a sealing/mating + face is noted; no burrs. +3. **GD&T.** Interpret all geometric callouts per **ASME Y14.5-2018**. Datum reference frames are + defined per part below. Surface-texture symbols per **ASME Y14.36**, values per **ASME B46.1**. +4. **Fits.** Bearing seats and registers per **ISO 286**: bores at **H7** (see per-part tables); + pilot/register bores at H8 unless the mating part dictates otherwise (**ASSUMED — bidder/PE to + confirm**). +5. **Stress relief.** Rough-machine, then stress-relieve before finish-machining the bearing seats to + hold the H7 tolerance through anodize (**ASSUMED best practice — bidder to propose**). +6. **Finish sequence.** Apply **Type III hardcoat anodize per MIL-A-8625F Class 1 (clear), 0.002 in + [~0.051 mm]** *after* final machining. **Mask** the bearing seats, drive registers, and threaded + holes. The hardcoat builds ~0.001 in [~0.025 mm] **per side**, which consumes the H7 band — so the + **H7 bores shall be dimensioned and verified in the as-delivered (post-mask/post-anodize) state** + (**ASSUMED masking approach — bidder/PE to confirm**; see `acceptance.md`). +7. **Surface finish.** Part-level surface finish per the control drawing: **Ra 1.6 µm [63 µin]** on + NW-MH-001 / NW-MH-002, **Ra 3.2 µm [125 µin]** on NW-MH-003. Bearing-seat bores are typically held + finer at **Ra ≤ 0.8 µm [32 µin]** (**ASSUMED design-intent — bidder/PE to confirm**). + +--- + +## 2. NW-MH-001 — RA (polar) axis housing + +**Drawing:** NW-MH-001 (rev A). **Stock:** plate **203.2 mm [8.000 in] × 203.2 mm [8.000 in] × +76.2 mm [3.000 in]**, cut allowance 3.0 mm, stock mass 8.50 kg (`cut_list.csv` row 2). **Wall:** +8.0 mm [0.315 in]. Pocket-lightening is permitted at the bidder's option **provided the bore and +flange datums are retained**. + +**Datum reference frame (ASME Y14.5-2018):** +- **Datum A** — primary flange / mounting face (planar; controls the housing to the pier-adapter and + DEC-head interface). +- **Datum B** — bearing-seat bore axis, Ø68 H7 (the axis of rotation). +- **Datum C** — drive-register bore or a designated flange hole, for clocking the bolt pattern. + +**Critical features:** + +| Feature | Nominal (mm [in]) | Tolerance / fit | GD&T (per Y14.5) | Ra | +|---|---|---|---|---| +| Bearing seat bore (7008) | Ø **68.000 – 68.030** [Ø2.677] | **H7** per ISO 286 (+0.030 / 0) | Ⓐ datum B; cylindricity *(value ASSUMED)* | ≤0.8 µm (ASSUMED) | +| Bore-to-flange perpendicularity | — | — | ⟂ bore axis to Datum A *(value ASSUMED — PE)* | — | +| Two-seat coaxiality (bearing span 76.2 mm [3.000 in]) | — | — | ◎ seats coaxial *(value ASSUMED — PE)* | — | +| Drive register bore (CSF-32) | Ø **80.0** [3.150] | H8 pilot (ASSUMED) | positional to B | 1.6 µm | +| Flange face flatness | — | — | ▱ **0.05 mm** to Datum A | 1.6 µm | +| Flange bolt circle | Ø **104.0** [4.094], **8 holes** | position *(ASSUMED)* | ⊕ to B/C *(ASSUMED)* | — | +| Corner mounting holes | **4 × M6** | ISO 2768-mK | — | — | + +**Provenance / flags.** Outer/wall/depth = P.RA_HOUSING (SOURCED); bearing seat = **7008 remediation**; +**flange PCD Ø104 mm and 8-hole pattern are DERIVED (~1.3× bore) → ASSUMED design-intent, bidder/PE +to confirm.** Perpendicularity + coaxiality are the stiffness-critical callouts (§4). + +**Interfaces:** bearing seat ← NW-CO-003 (7008 DB pair, OD 68); drive register ← NW-CO-001 (CSF-32, +bore 80) via NW-TP-002; through-bore ← NW-CO-005 encoder ring. + +--- + +## 3. NW-MH-002 — DEC axis housing + Losmandy-D saddle + +**Drawing:** NW-MH-002 (rev A). **Stock:** plate **152.4 mm [6.000 in] × 152.4 mm [6.000 in] × +63.5 mm [2.500 in]**, cut allowance 3.0 mm, stock mass 3.98 kg (`cut_list.csv` row 3). **Wall:** +8.0 mm [0.315 in]. Integral Losmandy-D dovetail saddle. + +**Datum reference frame (ASME Y14.5-2018):** +- **Datum A** — flange / mounting face (interfaces to the RA head). +- **Datum B** — bearing-seat bore axis, Ø55 H7 (DEC axis of rotation). +- **Datum C** — Losmandy-D saddle dovetail centre-plane. + +**Critical features:** + +| Feature | Nominal (mm [in]) | Tolerance / fit | GD&T (per Y14.5) | Ra | +|---|---|---|---|---| +| Bearing seat bore (7006) | Ø **55.000 – 55.030** [Ø2.165] | **H7** per ISO 286 (+0.030 / 0) | Ⓐ datum B; cylindricity *(ASSUMED)* | ≤0.8 µm (ASSUMED) | +| Bore-to-flange perpendicularity | — | — | ⟂ bore axis to Datum A *(ASSUMED — PE)* | — | +| Two-seat coaxiality (bearing span 63.5 mm [2.500 in]) | — | — | ◎ seats coaxial *(ASSUMED — PE)* | — | +| Drive register bore (CSF-25) | Ø **64.0** [2.520] | H8 pilot (ASSUMED) | positional to B | 1.6 µm | +| Flange face flatness | — | — | ▱ **0.05 mm** to Datum A | 1.6 µm | +| Flange bolt circle | Ø **83.0** [3.268] | position *(ASSUMED)* | ⊕ to B/C *(ASSUMED)* | — | +| Losmandy-D saddle width | **76.2** [3.000] | ISO 2768-mK *(profile ASSUMED)* | profile to Datum C *(ASSUMED)* | 1.6 µm | +| Dovetail angle | **15°** | ± *(ASSUMED)* | — | 1.6 µm | + +**Provenance / flags.** Outer/wall/depth = P.DEC_HOUSING (SOURCED); bearing seat = **7006 remediation**; +**Losmandy-D saddle geometry (76.2 mm width, 15° dovetail, undercut/relief) is ASSUMED to a standard +Losmandy-D profile — bidder/PE to confirm against a reference dovetail.** Flange PCD Ø83 mm is +ASSUMED design-intent. + +**Interfaces:** bearing seat ← NW-CO-004 (7006 DB pair, OD 55); drive register ← NW-CO-002 (CSF-25, +bore 64) via NW-TP-003; saddle + NW-MH-003 clamp ← OTA dovetail. + +--- + +## 4. NW-MH-003 — DEC saddle clamp bar + +**Drawing:** NW-MH-003 (rev A). **Stock:** plate **101.6 mm [4.000 in] × 76.2 mm [3.000 in] × +25.4 mm [1.000 in]**, cut allowance 3.0 mm, stock mass 0.53 kg (`cut_list.csv` row 4). + +**Datum reference frame:** Datum A = clamp mating face; Datum B = dovetail bevel face. + +| Feature | Nominal (mm [in]) | Tolerance | Ra | +|---|---|---|---| +| Length | **101.6** [4.000] | ISO 2768-mK | 3.2 µm | +| Width | **76.2** [3.000] | ISO 2768-mK | 3.2 µm | +| Thickness | **25.4** [1.000] | ISO 2768-mK | 3.2 µm | +| Clamp screws | **2 × M8** | ISO 2768-mK | — | +| Dovetail bevel | **15°** (matching NW-MH-002) | ± *(ASSUMED)* | 3.2 µm | + +> **ASSUMED design-intent — bidder/PE to confirm.** The **entire NW-MH-003 clamp bar** is carried as +> ASSUMED design-intent: the Losmandy-D clamp is not dimensioned in the repository. It pairs with the +> NW-MH-002 saddle to clamp the OTA dovetail; the shop shall confirm the mating profile and clamp +> travel before production. + +--- + +## 5. Bearing-seat callout block (stiffness remediation — grounding) + +The two housing bearing seats (**Ø68 H7 on NW-MH-001, Ø55 H7 on NW-MH-002**) are sized for +**matched angular-contact bearing pairs — 7008 (RA, OD 68 mm) and 7006 (DEC, OD 55 mm) — in a +back-to-back (DB), preloaded arrangement** (NW-CO-003 / NW-CO-004, BP-05). This is the proven +**remediation of the deep-groove 6008/6006 baseline**, which the stiffness/deflection proof identified +as ~98% of the 43.5″ pointing-deflection FAIL against the 5″ target (MECHANICAL_DESIGN.md §9; trade +study item 8). + +Because moment stiffness — not load rating — governs, the machining controls that matter are: + +- **Coaxiality** of the two end seats over the bearing span (76.2 mm RA / 63.5 mm DEC) — GD&T value + **ASSUMED, PE to set**. +- **Perpendicularity** of each seat axis to the flange mounting face (Datum A) — **ASSUMED, PE to set**. +- **H7 bore roundness/cylindricity and Ra ≤ 0.8 µm** for correct outer-ring seating. +- **Preload method** (ground spacer/shim set vs. clamped ring) — **ASSUMED design-intent; bidder/PE to + detail.** The housing shall provide an axial shoulder or clamp land per the released design. + +--- + +## 6. Finishing + +- **Type III hardcoat anodize per MIL-A-8625F Class 1 (clear), 0.002 in [~0.051 mm]** on all external + surfaces after final machining. +- **Mask:** bearing seats (Ø68 H7, Ø55 H7), drive registers (Ø80, Ø64), and all threaded holes. +- Post-anodize, re-verify the masked H7 bores are within tolerance (see `acceptance.md`). +- Furnish an anodize certificate with coating-thickness record. + +--- + +## 7. Submittals & deliverables + +1. Finished, anodized parts (qty 1 each): NW-MH-001, NW-MH-002, NW-MH-003. +2. Material cert / CoC (ASTM B209 / AMS-QQ-A-250/11) with heat/lot traceability. +3. Anodize certificate (MIL-A-8625F Type III). +4. First-Article Inspection report per `acceptance.md` (CMM for every H7 bore + flatness). +5. RFI log resolving every ASSUMED value before production. + +--- + +## 8. ASSUMED design-intent register (this SOW) + +| Item | Part(s) | Status | +|---|---|---| +| Flange PCD & bolt pattern (Ø104/8-hole, Ø83) | NW-MH-001, -002 | ASSUMED — bidder/PE to confirm | +| GD&T coaxiality / perpendicularity / position values | NW-MH-001, -002 | ASSUMED — PE to set | +| Bearing-seat surface finish (Ra ≤ 0.8 µm) & preload method | NW-MH-001, -002 | ASSUMED — bidder/PE to detail | +| Drive-register fit class (H8 pilot) | NW-MH-001, -002 | ASSUMED — bidder/PE to confirm | +| Losmandy-D saddle profile / undercut / 15° dovetail | NW-MH-002 | ASSUMED — confirm vs reference dovetail | +| Entire saddle clamp bar geometry | NW-MH-003 | ASSUMED design-intent | +| Encoder-ring register location | NW-MH-001, -002 | ASSUMED — preserve through-bore | +| Post-anodize masking approach on H7 bores | all | ASSUMED — bidder/PE to confirm | diff --git a/design/mechanical/tender/packages/BP-01_mount_machining/acceptance.md b/design/mechanical/tender/packages/BP-01_mount_machining/acceptance.md new file mode 100644 index 0000000..63abfc9 --- /dev/null +++ b/design/mechanical/tender/packages/BP-01_mount_machining/acceptance.md @@ -0,0 +1,127 @@ +# BP-01 — Dimensional Acceptance Plan: Mount Head Machining + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-01 — Mount head machining | +| Parts | NW-MH-001, NW-MH-002, NW-MH-003 | +| Method | CMM dimensional acceptance of the **H7 bearing-seat bores** + **flange flatness**; profilometer for Ra | +| Issue date / rev | 2026-08-06 / A | + +**Scope.** This plan defines the inspection method and accept/reject criteria for BP-01. Because each +part has quantity 1, the **First-Article Inspection (FAI) *is* the production acceptance**. The H7 +bearing seats and the flange flatness are the **critical characteristics** and receive **100% CMM +verification**; general dimensions are verified to **ISO 2768-mK** on the FAI. All limits are dual-unit +and derived from `partspec` / the NW-MH control drawings — nothing invented. + +**Standards.** ASME Y14.5-2018 (datum alignment / GD&T interpretation); ISO 286 (H7 limits); ISO 2768 +(general tolerance); ASME B46.1 (surface texture measurement); ISO 1 (20 °C reference temperature). + +--- + +## 1. Inspection method + +1. **CMM** for all bores, flatness, and located hole patterns. Datum alignment shall follow the part's + datum reference frame in `SOW.md` (Datum A flange face, Datum B bore axis, Datum C register/saddle). +2. **Metrology environment:** temperature-controlled at **20 °C** per ISO 1; parts thermally soaked + before measurement (**ASSUMED soak time — bidder to state**). +3. **Measurement uncertainty:** CMM + fixturing uncertainty shall be **≤ 10% of the tolerance band** + for the H7 bores (gauge R&R / MSA evidence on request) — **ASSUMED acceptance gate, bidder/PE to + confirm**. +4. **Bore evaluation:** each H7 seat probed with **≥ 8 points per circle at 3 axial depths**; report + least-squares diameter, roundness, and cylindricity. +5. **Surface finish:** profilometer per ASME B46.1 on bore walls and flange faces. +6. **State of measurement:** the H7 bores are measured **as delivered (post-mask / post-anodize)** — + see `SOW.md` §1.6/§6. + +--- + +## 2. Acceptance criteria — NW-MH-001 (RA housing) + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Bearing seat bore Ø68 **H7** | Ø68.000 [2.677] | **Ø68.000 – 68.030 mm** (+0.030/0) | CMM, ≥8 pts × 3 depths | **Critical — 100%** | +| Bearing seat roundness / cylindricity | — | per PE GD&T *(value ASSUMED)* | CMM | Critical | +| Bore-to-flange perpendicularity | — | per PE GD&T *(ASSUMED)* | CMM (B to A) | Critical | +| Two-seat coaxiality over 76.2 mm [3.000 in] span | — | per PE GD&T *(ASSUMED)* | CMM | Critical | +| Flange face flatness | — | **≤ 0.05 mm [0.0020 in]** | CMM plane fit | Critical — 100% | +| Drive register bore Ø80 (CSF-32) | Ø80.0 [3.150] | H8 pilot *(ASSUMED)* | CMM | Major | +| Flange bolt circle Ø104, 8 holes | Ø104.0 [4.094] | position *(ASSUMED)* | CMM | Major | +| Corner holes 4 × M6 | — | ISO 2768-mK | thread gauge | Minor | +| Bore surface finish | — | **Ra ≤ 0.8 µm [32 µin]** *(ASSUMED)* | profilometer | Major | +| General surface finish | — | **Ra 1.6 µm [63 µin]** | profilometer | Minor | +| Envelope 203.2 × 203.2 × 76.2 | [8.000×8.000×3.000] | ISO 2768-mK | calipers/CMM | Minor | + +## 3. Acceptance criteria — NW-MH-002 (DEC housing + saddle) + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Bearing seat bore Ø55 **H7** | Ø55.000 [2.165] | **Ø55.000 – 55.030 mm** (+0.030/0) | CMM, ≥8 pts × 3 depths | **Critical — 100%** | +| Bearing seat roundness / cylindricity | — | per PE GD&T *(ASSUMED)* | CMM | Critical | +| Bore-to-flange perpendicularity | — | per PE GD&T *(ASSUMED)* | CMM (B to A) | Critical | +| Two-seat coaxiality over 63.5 mm [2.500 in] span | — | per PE GD&T *(ASSUMED)* | CMM | Critical | +| Flange face flatness | — | **≤ 0.05 mm [0.0020 in]** | CMM plane fit | Critical — 100% | +| Drive register bore Ø64 (CSF-25) | Ø64.0 [2.520] | H8 pilot *(ASSUMED)* | CMM | Major | +| Flange bolt circle Ø83 | Ø83.0 [3.268] | position *(ASSUMED)* | CMM | Major | +| Losmandy-D saddle width | 76.2 [3.000] | ISO 2768-mK *(profile ASSUMED)* | CMM / profile gauge | Major | +| Dovetail angle | 15° | ± *(ASSUMED)* | angle gauge / CMM | Major | +| Bore surface finish | — | **Ra ≤ 0.8 µm** *(ASSUMED)* | profilometer | Major | +| General surface finish | — | **Ra 1.6 µm [63 µin]** | profilometer | Minor | +| Envelope 152.4 × 152.4 × 63.5 | [6.000×6.000×2.500] | ISO 2768-mK | calipers/CMM | Minor | + +## 4. Acceptance criteria — NW-MH-003 (saddle clamp bar) + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Length | 101.6 [4.000] | ISO 2768-mK | calipers/CMM | Minor | +| Width | 76.2 [3.000] | ISO 2768-mK | calipers/CMM | Minor | +| Thickness | 25.4 [1.000] | ISO 2768-mK | calipers/CMM | Minor | +| Clamp screws | 2 × M8 | ISO 2768-mK | thread gauge | Minor | +| Dovetail bevel | 15° (match NW-MH-002) | ± *(ASSUMED)* | angle gauge | Major | +| Surface finish | — | **Ra 3.2 µm [125 µin]** | profilometer | Minor | + +> The whole part is **ASSUMED design-intent** (Losmandy-D clamp not dimensioned in the repo); acceptance +> limits above are provisional pending PE release. + +--- + +## 5. Sampling + +- **Critical characteristics** (H7 bores, flange flatness, and the stiffness-critical coaxiality / + perpendicularity callouts): **100%** — each part is qty 1, so FAI = production acceptance. +- **Major / Minor characteristics:** verified on the FAI to ISO 2768-mK. +- **Post-anodize re-check:** the H7 bores are re-verified after masking/anodize (per `SOW.md` §6). + +## 6. Documentation & sign-off + +The bidder shall deliver, per part: + +1. **FAI report** — every characteristic in §2–§4 with measured value vs. limit and pass/fail. +2. **CMM raw output** — probe data + datum alignment record (Y14.5 frame). +3. **Material certificate** (ASTM B209 / AMS-QQ-A-250/11) with heat/lot traceability. +4. **Anodize certificate** (MIL-A-8625F Type III) with coating-thickness record. +5. **Nonconformance / RFI dispositions** for any ASSUMED value or out-of-tolerance condition. + +| Role | Name | Signature | Date | +|---|---|---|---| +| Bidder QA | | | | +| Owner / PE review | | | | + +--- + +## 7. ASSUMED-value register (do NOT treat as final) + +The following acceptance values are **ASSUMED design-intent — bidder/PE to confirm** before they can +be applied as pass/fail gates: + +| # | ASSUMED value | Part(s) | +|---|---|---| +| 1 | All GD&T coaxiality / perpendicularity / position tolerances | NW-MH-001, -002 | +| 2 | Bearing-seat surface finish Ra ≤ 0.8 µm | NW-MH-001, -002 | +| 3 | Drive-register fit class (H8 pilot) | NW-MH-001, -002 | +| 4 | Flange PCD & pattern (Ø104/8-hole, Ø83) | NW-MH-001, -002 | +| 5 | Losmandy-D saddle profile & 15° dovetail | NW-MH-002 | +| 6 | Entire saddle clamp bar geometry | NW-MH-003 | +| 7 | CMM uncertainty gate (≤10% of tol band), soak time | all | +| 8 | Post-anodize masking / re-check approach | all | diff --git a/design/mechanical/tender/packages/BP-02_turned_parts/README.md b/design/mechanical/tender/packages/BP-02_turned_parts/README.md new file mode 100644 index 0000000..6d528fb --- /dev/null +++ b/design/mechanical/tender/packages/BP-02_turned_parts/README.md @@ -0,0 +1,163 @@ +# BP-02 — Precision Turned Parts (303 SS) — Bidder README + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | **BP-02 — Precision turned parts** | +| Trade | CNC / manual turning (303 stainless steel), single-point turning + light milling | +| Parts | NW-TP-001, NW-TP-002, NW-TP-003 (3 fabricated line items) | +| Branch / project | `design/mechanical-tender` — NIGHTWATCH Observatory Mount & Roll-off Tender | +| Issue date | 2026-08-06 | +| Revision | A | +| Governing TDP standard | MIL-STD-31000A (Technical Data Package) | + +This README is the entry point for the turning shop bidding BP-02. It defines the **scope**, the +**data package the shop receives**, the **bidder deliverables**, and the items still carried as +**ASSUMED design-intent**. Read it with `SOW.md` (the per-part turning specification) and +`acceptance.md` (the dimensional / concentricity acceptance plan) in this same folder. + +--- + +## 1. Scope + +BP-02 covers the **three turned 303 stainless-steel parts** that couple the harmonic drives to the +right-ascension (RA) and declination (DEC) axes and carry the angular-contact bearing inner races — +plus the optional counterweight shaft. The shop: + +1. Procures **303 stainless round bar** to spec (ASTM A582), with material certification. +2. **Turns** (and lightly mills the bolt patterns / wrench flats on) the three parts to the STEP + master and the NW-TP control drawings, holding the **bearing-race journals** and the + **harmonic-drive registers** to their ISO 286 fit classes. +3. Applies **passivation per ASTM A967** (nitric or citric). +4. Inspects per `acceptance.md` and delivers finished, passivated, first-article-inspected parts. + +**Out of scope for BP-02** (separate packages — the turned parts *interface* to these but the shop +does not supply them): + +| Interface | Supplied under | Notes | +|---|---|---| +| RA / DEC axis housings, dovetail saddle | BP-01 (6061-T6 machining) | The bearing **outer** races seat in the BP-01 housing bores (Ø68 H7 / Ø55 H7); the BP-02 journals carry the **inner** races. | +| Concrete pier, top plate, mount adapter | BP-03 (pier & foundation) | Not a turned-part interface. | +| Roll-off roof steelwork | BP-04 | Not a turned-part interface. | +| Harmonic drives, angular-contact bearings, on-axis encoders, counterweights, fasteners | BP-05 (COTS) | See §4 interface list. | + +--- + +## 2. Data package the shop receives (per MIL-STD-31000A) + +| Item | Format / standard | Role | +|---|---|---| +| 3-D solid master | **STEP ISO 10303 AP242** | **CNC authority** — governs as-modelled geometry. | +| Control drawings NW-TP-001 / -002 / -003 | **PDF** (drawing practices per ASME Y14.100; drawing types Y14.24; sheet & title block Y14.1) | Envelope + title block + key dimensions & features; bid control drawing. | +| `SOW.md` | Markdown | Per-part turning spec: journals to ISO 286 fits, CSF registers, concentricity/TIR, passivation, certs. | +| `acceptance.md` | Markdown | Dimensional acceptance of the journal fits + concentricity/run-out + passivation verification; FAI. | +| `cut_list.csv` (rows for NW-TP-001/002/003) | CSV/XLSX | Stock size, cut allowance, stock mass. | + +**Order of precedence.** The **STEP AP242 solid governs geometry**; the drawing + SOW govern +tolerances, notes, finish, and material; where geometry and the tolerance package disagree, the **SOW +controls** and the bidder shall raise an RFI before cutting metal. All GD&T is interpreted per +**ASME Y14.5-2018**; general tolerances per **ISO 2768-mK**; fits per **ISO 286**; surface texture per +**ASME Y14.36 / ASME B46.1**. + +--- + +## 3. Line-item summary (source: `partspec.parts_for('BP-02')` + `cut_list.csv`) + +| Part no. | Name | Stock (round bar) | Qty | Stock mass | Finish | Drawing | cut_list row | +|---|---|---|---:|---:|---|---|---| +| **NW-TP-001** | Counterweight shaft **(OPTIONAL — see §6)** | Ø31.75 mm [1.250 in] × 457.2 mm [18.000 in] | 1 | 2.90 kg | Passivate ASTM A967 | NW-TP-001 | row 5 | +| **NW-TP-002** | RA drive spindle / drawbar adapter | Ø88.9 mm [3.500 in] × 101.6 mm [4.000 in] | 1 | 5.05 kg | Passivate ASTM A967 | NW-TP-002 | row 6 | +| **NW-TP-003** | DEC drive spindle / saddle stub | Ø76.2 mm [3.000 in] × 88.9 mm [3.500 in] | 1 | 3.24 kg | Passivate ASTM A967 | NW-TP-003 | row 7 | + +Stock mass is the **quantity ordered** (stock volume × 303-SS density 8000 kg/m³), not finished net +mass. Cut allowance is 3.0 mm on stock. Material for all three: **303 stainless per ASTM A582**. + +> The NW-TP-001 stock diameter is Ø31.75 mm (= 1.250 in exactly); the generated control sheet and +> `cut_list.csv` display the mm value rounded to **Ø31.8 mm** — the same diameter. + +--- + +## 4. Cross-package interfaces the shop must preserve + +The bearing-race **journals** and the harmonic-drive **registers** are mating features to other +packages. The shop turns the BP-02 side; the fits and concentricity must be held so the mating +COTS/housing parts assemble and preload correctly: + +| BP-02 feature | Nominal | Fit / carries | Mates to | +|---|---|---|---| +| NW-TP-002 bearing journal | Ø **40.0 mm [1.575 in]** | **j6** (ISO 286) — inner race of 7008 DB pair | NW-CO-003 (bore 40) → seats in NW-MH-001 bore Ø68 H7 | +| NW-TP-002 CSF-32 register | Ø **80.0 mm [3.150 in]** | locating fit to CSF-32 output | NW-CO-001 (bore 80) → drive register in NW-MH-001 | +| NW-TP-002 bolt pattern | **8 × M4 on Ø104 PCD** | bolts spindle to CSF-32 output flange | matches NW-MH-001 flange PCD (BP-01) | +| NW-TP-003 bearing journal | Ø **30.0 mm [1.181 in]** | **j6** (ISO 286) — inner race of 7006 DB pair | NW-CO-004 (bore 30) → seats in NW-MH-002 bore Ø55 H7 | +| NW-TP-003 CSF-25 register | Ø **64.0 mm [2.520 in]** | locating fit to CSF-25 output | NW-CO-002 (bore 64) → drive register in NW-MH-002 | +| NW-TP-003 bolt pattern | **8 × M4 on Ø83 PCD** | bolts spindle to CSF-25 output flange | matches NW-MH-002 flange PCD (BP-01) | +| NW-TP-001 stud / safety-stop | **M12** stud (× 40) + M12 safety-stop | carries the counterweight stack | NW-CO-009 counterweights (OPTIONAL) | + +> **Remediation context.** The journals carry the inner races of **angular-contact 7008 (RA) / 7006 +> (DEC)** pairs in a **back-to-back (DB), preloaded** arrangement — the proven remediation of the +> deep-groove 6008/6006 baseline. The stiffness/deflection proof showed the two deep-groove pairs +> contributing **14.21″ (RA, 6008 @ 76.2 mm span) + 28.43″ (DEC, 6006 @ 63.5 mm span) = 42.64″ of the +> 43.52″ static pointing deflection — ~98% of the 5″-target FAIL**. Because **moment stiffness** (not +> load rating) governs, the **journal fit, journal-shoulder squareness, and journal-to-register +> concentricity** are the stiffness-critical turned features (see `SOW.md` §5 and MECHANICAL_DESIGN.md +> §9; trade study item 8). + +--- + +## 5. Bidder deliverables + +1. Finished, passivated parts NW-TP-002 / -003 (qty 1 each), and NW-TP-001 **only if the + counterweighted variant is bid** (see §6). +2. **Material certification / CoC** for the 303 SS bar (ASTM A582), with heat/lot traceability and + chemistry (incl. sulphur — relevant to passivation, §6 / `SOW.md` §6). +3. **Passivation certification** — ASTM A967, method and acceptance test used. +4. **First-Article Inspection (FAI) report** per `acceptance.md`, including the journal-diameter + measurements, journal-to-register concentricity / TIR, and thread verification. +5. RFI / nonconformance log for any ASSUMED value the shop needed resolved. + +--- + +## 6. ASSUMED design-intent — bidder / PE to confirm (NOT final) + +The following are carried as **ASSUMED design-intent** in the parts registry and must **not** be +treated as released dimensions. The bidder/PE shall confirm before production: + +- **Bearing-race journal fit class.** The race journals are shown at **j6** (Ø40 on NW-TP-002, Ø30 on + NW-TP-003) while the drawing's general note reads **"journal diameters h6."** Because the shaft + (inner ring) **rotates relative to the gravity load**, bearing-maker practice would call for a light + transition/interference fit (j6/k5/k6) rather than a clearance h6. The **exact grade (j6 vs h6 vs + k6) and the resulting preload are a PE call** — bidder/PE to confirm. +- **Spindle body diameters** — NW-TP-002 Ø88.9 mm [3.500 in] and NW-TP-003 Ø76.2 mm [3.000 in] are + **ASSUMED** (registry provenance: "Body dia ASSUMED"); they set only the stock size and the flange + OD, not a mating fit. +- **Bolt patterns (8 × M4 on Ø104 / Ø83 PCD)** — DERIVED to match the BP-01 housing flange PCDs, which + are themselves ASSUMED design-intent; confirm against the released CSF output-flange pattern. +- **CSF register fit & form** — whether the Ø80 / Ø64 register is an OD pilot or a bore, and its fit + class to the harmonic-drive output (H7/h6 locating assumed) — ASSUMED, bidder/PE to confirm against + the CSF-32 / CSF-25 datasheet. +- **"Drawbar adapter" thread on NW-TP-002** — the drawtube/drawbar thread implied by the part name is + not dimensioned in the repo — ASSUMED design-intent. +- **Journal-shoulder axial location & squareness / preload land** — the shoulder that sets DB preload + and its perpendicularity to the journal axis — ASSUMED, PE to set. +- **Passivation method & acceptance test** — ASTM A967 nitric vs citric, and the verification test + (water-immersion / high-humidity / salt-spray / copper-sulphate) — ASSUMED; note 303 is a + **free-machining, high-sulphur** grade that passivates less readily than 304 (`SOW.md` §6). +- **Bearing-journal surface finish** — part-level Ra 0.8 µm is on the sheet; bearing-race seats may + need **Ra ≤ 0.4 µm** — ASSUMED, bidder/PE to confirm. +- **NW-TP-001 inclusion** — the selected **counterweight-FREE GEM** (torque proof, SF 2.5/2.6) omits + the counterweight shaft entirely; NW-TP-001 is **OPTIONAL** and bid only for the counterweighted + variant. + +--- + +## 7. References + +- Parts registry: `design/mechanical/tender/gen/partspec.py` → `parts_for('BP-02')` (single source of truth). +- BOM / cut list: `design/mechanical/tender/bom/{master_bom,cut_list}.csv`. +- Control drawings: `design/mechanical/tender/drawings/NW-TP-00{1,2,3}.svg`. +- Design basis: `design/mechanical/MECHANICAL_DESIGN.md` §9 (selected config + remediations; bearings + 7008/7006 fix); `design/mechanical/tradestudy/SECTION.md` (selection rationale, item 8 bearing fix). +- Master dossier: `packages/BP-00_master_dossier/` (ITB, general SOW, standards & drawing registers). +- Standards register: MIL-STD-31000A, ASME Y14.5-2018 / Y14.100 / Y14.24 / Y14.1 / Y14.36, ASME B46.1, + ISO 286, ISO 2768, ASTM A582 (303 SS), ASTM A967 (passivation). diff --git a/design/mechanical/tender/packages/BP-02_turned_parts/SOW.md b/design/mechanical/tender/packages/BP-02_turned_parts/SOW.md new file mode 100644 index 0000000..7527dc1 --- /dev/null +++ b/design/mechanical/tender/packages/BP-02_turned_parts/SOW.md @@ -0,0 +1,217 @@ +# BP-02 — Statement of Work: Precision Turned Parts (303 SS) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-02 — Precision turned parts | +| Parts | NW-TP-001 (counterweight shaft, OPTIONAL), NW-TP-002 (RA drive spindle), NW-TP-003 (DEC drive spindle) | +| Process | CNC / manual turning (single-point) + light milling (bolt patterns, flats) | +| Material | 303 stainless steel per **ASTM A582** | +| Finish | **Passivate per ASTM A967** (nitric or citric) | +| Issue date / rev | 2026-08-06 / A | + +**Purpose.** This SOW specifies the turning, tolerancing, finishing, and certification requirements for +the three turned parts. It is read with the STEP ISO 10303 AP242 solid master (CNC authority), the +NW-TP control drawings (PDF), and `acceptance.md`. All dimensions are dual-unit, **mm primary, inch in +brackets**, drawn from the parts registry (`partspec.parts_for('BP-02')`) and `cut_list.csv` — no +dimension herein is invented. + +**Standards applied.** MIL-STD-31000A (TDP); **ASME Y14.5-2018** (GD&T); ASME Y14.100 (drawing +practices), Y14.24 (drawing types), Y14.1 (sheet/title block); **ASME Y14.36 & ASME B46.1** (surface +texture); **ISO 286** (j6 / h6 / H7 fits); **ISO 2768-mK** (general tolerances); **ASTM A582** (303 +free-machining stainless bar); **ASTM A967** (passivation of stainless). + +--- + +## 1. General requirements (all parts) + +1. **Material & certification.** 303 stainless bar per **ASTM A582** (free-machining austenitic). + Furnish mill certification and Certificate of Conformance with **heat/lot traceability** and full + **chemistry** (the sulphur content governs passivation, §6). Mark each part with part number and + revision per ASME Y14.100. +2. **General tolerance.** **ISO 2768-mK** (medium, fine machining class) on all dimensions not + otherwise toleranced. Break sharp edges 0.3–0.5 mm [0.012–0.020 in]; deburr all threads and + cross-holes; no burrs into bearing seats. +3. **Journal fits.** Bearing-race journals and drive registers to **ISO 286** — see per-part tables. + The general note is **journal diameters h6**; the two **bearing-race journals are shown at j6** (a + transition fit to retain the rotating inner ring). See §5 and the ASSUMED-value register (§8). +4. **Concentricity / run-out.** The turned features share one datum axis: **journal-to-register (and + journal-to-journal) run-out shall not exceed 0.02 mm [0.0008 in] TIR** (registry `_TOL_TURN` + SOURCED value). Turn the mating diameters **in one setup / between centres** wherever practical to + hold it. The datum-axis assignment (which feature is Datum A) is **ASSUMED — bidder/PE to confirm**. +5. **GD&T.** Interpret all geometric callouts per **ASME Y14.5-2018**. Surface-texture symbols per + **ASME Y14.36**, values per **ASME B46.1**. +6. **Stress / distortion.** 303 work-hardens and can move on the last cut; **finish the bearing + journals last, in a light final pass**, to hold the j6/h6 band and the 0.02 TIR (**ASSUMED best + practice — bidder to propose**). +7. **Surface finish.** Part-level **Ra 0.8 µm [32 µin]** per the control drawing. Bearing-race journals + are typically held finer at **Ra ≤ 0.4 µm [16 µin]** (**ASSUMED design-intent — bidder/PE to + confirm**; see `acceptance.md`). +8. **Finish sequence.** Turn/mill complete, deburr, then **passivate per ASTM A967** (§6). No plating. + +--- + +## 2. NW-TP-002 — RA drive spindle / drawbar adapter + +**Drawing:** NW-TP-002 (rev A). **Stock:** 303-SS round bar **Ø88.9 mm [3.500 in] × 101.6 mm +[4.000 in]**, cut allowance 3.0 mm, stock mass 5.05 kg (`cut_list.csv` row 6). Process: **CNC turn + +mill**. + +**Function.** Couples the **CSF-32-100** harmonic-drive output to the RA axis and **carries the inner +race of the 7008 angular-contact pair**. + +**Datum reference frame (ASME Y14.5-2018, ASSUMED — PE to confirm):** +- **Datum A** — bearing-journal axis (the axis of rotation, Ø40 j6). +- **Datum B** — CSF-32 register face (axial seat against the drive output). +- **Datum C** — a designated bolt hole, for clocking the 8 × M4 pattern. + +**Critical features:** + +| Feature | Nominal (mm [in]) | Fit / tolerance | ISO 286 limits (representative) | Ra | +|---|---|---|---|---| +| Bearing journal (7008 inner race) | Ø **40.0** [1.575] | **j6** (ASSUMED grade) per ISO 286 | Ø39.995 – 40.011 (j6) *(h6 alt: 39.984 – 40.000)* | ≤0.4 µm (ASSUMED) | +| Journal-to-register concentricity | — | **0.02 mm TIR** (SOURCED) | run-out to Datum A/B | — | +| Journal-shoulder squareness / preload land | — | ⟂ to Datum A *(value ASSUMED — PE)* | — | — | +| CSF-32 wave-gen / output register | Ø **80.0** [3.150] | locating fit (H7/h6 ASSUMED) | per CSF-32 datasheet | 0.8 µm | +| Body dia (flange OD) | Ø **88.9** [3.500] | ISO 2768-mK *(ASSUMED)* | — | 0.8 µm | +| Length | **101.6** [4.000] | ISO 2768-mK | — | 0.8 µm | +| Bolt pattern | **8 × M4 on Ø104 PCD** | position *(ASSUMED)* | matches NW-MH-001 flange | — | +| Drawbar / drawtube thread | *(not dimensioned)* | **ASSUMED design-intent** | — | — | + +**Provenance / flags.** Journal Ø40 = **7008 bore (remediation)**; CSF-32 register Ø80 = +**P.RA_DRIVE.bore_m (SOURCED)**; **body dia Ø88.9 ASSUMED**; bolt pattern DERIVED to the NW-MH-001 +flange PCD (itself ASSUMED). The **journal fit grade (j6 vs h6)** and the **drawbar thread** are +ASSUMED — bidder/PE to confirm. + +**Interfaces:** journal → NW-CO-003 (7008 DB pair, bore 40) seating in NW-MH-001 bore Ø68 H7; register +→ NW-CO-001 (CSF-32, bore 80); bolt pattern → NW-MH-001 flange. + +--- + +## 3. NW-TP-003 — DEC drive spindle / saddle stub + +**Drawing:** NW-TP-003 (rev A). **Stock:** 303-SS round bar **Ø76.2 mm [3.000 in] × 88.9 mm +[3.500 in]**, cut allowance 3.0 mm, stock mass 3.24 kg (`cut_list.csv` row 7). Process: **CNC turn + +mill**. + +**Function.** Couples the **CSF-25-80** harmonic-drive output to the DEC axis / saddle side and +**carries the inner race of the 7006 angular-contact pair**. + +**Datum reference frame (ASME Y14.5-2018, ASSUMED — PE to confirm):** +- **Datum A** — bearing-journal axis (DEC axis of rotation, Ø30 j6). +- **Datum B** — CSF-25 register face. +- **Datum C** — a designated bolt hole for clocking the 8 × M4 pattern. + +**Critical features:** + +| Feature | Nominal (mm [in]) | Fit / tolerance | ISO 286 limits (representative) | Ra | +|---|---|---|---|---| +| Bearing journal (7006 inner race) | Ø **30.0** [1.181] | **j6** (ASSUMED grade) per ISO 286 | Ø29.996 – 30.009 (j6) *(h6 alt: 29.987 – 30.000)* | ≤0.4 µm (ASSUMED) | +| Journal-to-register concentricity | — | **0.02 mm TIR** (SOURCED) | run-out to Datum A/B | — | +| Journal-shoulder squareness / preload land | — | ⟂ to Datum A *(value ASSUMED — PE)* | — | — | +| CSF-25 wave-gen / output register | Ø **64.0** [2.520] | locating fit (H7/h6 ASSUMED) | per CSF-25 datasheet | 0.8 µm | +| Body dia (flange OD) | Ø **76.2** [3.000] | ISO 2768-mK *(ASSUMED)* | — | 0.8 µm | +| Length | **88.9** [3.500] | ISO 2768-mK | — | 0.8 µm | +| Bolt pattern | **8 × M4 on Ø83 PCD** | position *(ASSUMED)* | matches NW-MH-002 flange | — | + +**Provenance / flags.** Journal Ø30 = **7006 bore (remediation)**; CSF-25 register Ø64 = +**P.DEC_DRIVE.bore_m (SOURCED)**; **body dia Ø76.2 ASSUMED**; bolt pattern DERIVED to the NW-MH-002 +flange PCD (itself ASSUMED). The **journal fit grade (j6 vs h6)** is ASSUMED — bidder/PE to confirm. + +**Interfaces:** journal → NW-CO-004 (7006 DB pair, bore 30) seating in NW-MH-002 bore Ø55 H7; register +→ NW-CO-002 (CSF-25, bore 64); bolt pattern → NW-MH-002 flange. + +--- + +## 4. NW-TP-001 — Counterweight shaft (OPTIONAL) + +**Drawing:** NW-TP-001 (rev A). **Stock:** 303-SS round bar **Ø31.75 mm [1.250 in] × 457.2 mm +[18.000 in]** (sheet displays Ø31.8 mm), cut allowance 3.0 mm, stock mass 2.90 kg (`cut_list.csv` +row 5). Process: **CNC turn**. + +> **OPTIONAL PART.** The selected topology is the **counterweight-FREE GEM** (torque proof SF 2.5/2.6; +> deletes 15.4 kg + 29% RA inertia — MECHANICAL_DESIGN.md §9). NW-TP-001 (and its COTS counterweights +> NW-CO-009) are supplied **only if the counterweighted variant is bid**. Price it as a separately +> deletable line item. + +**Datum reference frame:** Datum A = shaft OD axis. + +| Feature | Nominal (mm [in]) | Tolerance | Ra | +|---|---|---|---| +| Shaft diameter | Ø **31.75** [1.250] | ISO 2768-mK | 0.8 µm | +| Length | **457.2** [18.000] | ISO 2768-mK | 0.8 µm | +| Stud end | **M12 × 40** | 6g thread (ISO 2768) | — | +| Safety-stop thread | **M12** | 6g thread | — | +| Root fillet | **R3.0** [0.118] | — | — | + +**Provenance / flags.** Diameter/length = **P.COUNTERWEIGHTS (SOURCED)**; stud (M12 × 40), safety-stop +(M12), and root fillet (R3.0) are on the sheet. Thread class (6g) and the safety-stop feature detail +are **ASSUMED — bidder/PE to confirm**. + +**Interfaces:** M12 stud → DEC-axis counterweight boss; carries NW-CO-009 counterweights (cast iron, +5 kg × 2 + 2.5 kg × 1). Safety-stop retains the weight stack against drop. + +--- + +## 5. Bearing-journal callout block (stiffness remediation — grounding) + +The two spindle journals (**Ø40 j6 on NW-TP-002, Ø30 j6 on NW-TP-003**) carry the inner races of the +**matched angular-contact pairs — 7008 (RA, bore 40 mm) and 7006 (DEC, bore 30 mm) in a back-to-back +(DB), preloaded arrangement** (NW-CO-003 / NW-CO-004, BP-05). This is the proven **remediation of the +deep-groove 6008/6006 baseline**: the stiffness/deflection proof attributed **14.21″ (RA 6008 pair, +76.2 mm span) + 28.43″ (DEC 6006 pair, 63.5 mm span) = 42.64″ of the 43.52″ static pointing deflection +(~98%) — the dominant term in the 5″-target FAIL** (MECHANICAL_DESIGN.md §9; trade study item 8). + +Because **moment stiffness** — not load rating — governs, the turned controls that matter are: + +- **Journal fit** to the inner race — a snug transition/interference (**j6 shown; grade ASSUMED, PE to + set**) so the rotating inner ring does not creep or lose preload. +- **Journal-shoulder squareness** to the journal axis, and the **axial location** of the shoulder that + sets the DB preload — **ASSUMED, PE to set**. +- **Journal-to-register / journal-to-journal concentricity ≤ 0.02 mm TIR** (SOURCED) and **Ra ≤ 0.4 µm** + (ASSUMED) on the race seats — turn in one setup / between centres to hold it. +- **Preload method** (ground spacer/shim vs. clamped nut) — the housing (BP-01) and spindle shoulders + together set it; **ASSUMED design-intent, bidder/PE to detail.** + +--- + +## 6. Finishing — passivation + +- **Passivate per ASTM A967** after all machining and deburring. Nitric or citric process acceptable; + state the process on the certificate. +- **303 is a free-machining, high-sulphur grade**: the manganese-sulphide inclusions passivate less + readily than 304/316 and can leave active sites. Use a process qualified for free-machining grades + and **verify** by an ASTM A967 acceptance test (water-immersion / high-humidity / salt-spray / + copper-sulphate) — the specific test is **ASSUMED, bidder/PE to confirm**. +- **Mask nothing that must stay dimensional** — passivation is not a coating and does not build + thickness, so the j6/h6 journals are unaffected; still re-verify per `acceptance.md`. +- Furnish a **passivation certificate** stating process, solution, and the acceptance test used. + +--- + +## 7. Submittals & deliverables + +1. Finished, passivated parts: NW-TP-002, NW-TP-003 (qty 1 each); **NW-TP-001 only if bid**. +2. Material cert / CoC (ASTM A582) with heat/lot traceability and chemistry. +3. Passivation certificate (ASTM A967) with process + acceptance test. +4. First-Article Inspection report per `acceptance.md` (journal diameters, concentricity/TIR, threads). +5. RFI log resolving every ASSUMED value before production. + +--- + +## 8. ASSUMED design-intent register (this SOW) + +| Item | Part(s) | Status | +|---|---|---| +| Bearing-race journal fit grade (j6 shown vs h6 general note; k5/k6 alt for rotating inner ring) | NW-TP-002, -003 | ASSUMED — PE to set | +| Bearing-journal surface finish (Ra ≤ 0.4 µm) | NW-TP-002, -003 | ASSUMED — bidder/PE to confirm | +| Journal-shoulder squareness / axial preload land | NW-TP-002, -003 | ASSUMED — PE to set | +| Datum-axis assignment for the 0.02 TIR run-out | NW-TP-002, -003 | ASSUMED — bidder/PE to confirm | +| Spindle body diameters (Ø88.9 / Ø76.2) | NW-TP-002, -003 | ASSUMED design-intent | +| Bolt patterns (8 × M4 on Ø104 / Ø83 PCD) | NW-TP-002, -003 | DERIVED/ASSUMED — confirm vs CSF flange | +| CSF register form & fit class (OD pilot vs bore; H7/h6) | NW-TP-002, -003 | ASSUMED — confirm vs CSF datasheet | +| Drawbar / drawtube thread | NW-TP-002 | ASSUMED design-intent (not dimensioned) | +| Thread class (6g) & safety-stop detail | NW-TP-001 | ASSUMED — bidder/PE to confirm | +| Passivation method (nitric/citric) & acceptance test | all | ASSUMED — bidder/PE to confirm | +| NW-TP-001 inclusion (counterweight-free build omits it) | NW-TP-001 | OPTIONAL — bid separately deletable | diff --git a/design/mechanical/tender/packages/BP-02_turned_parts/acceptance.md b/design/mechanical/tender/packages/BP-02_turned_parts/acceptance.md new file mode 100644 index 0000000..bc3f94a --- /dev/null +++ b/design/mechanical/tender/packages/BP-02_turned_parts/acceptance.md @@ -0,0 +1,143 @@ +# BP-02 — Dimensional Acceptance Plan: Precision Turned Parts + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-02 — Precision turned parts | +| Parts | NW-TP-001 (OPTIONAL), NW-TP-002, NW-TP-003 | +| Method | Dimensional acceptance of the **bearing-race journals** + **journal-to-register concentricity/run-out**; thread gauging; passivation verification | +| Issue date / rev | 2026-08-06 / A | + +**Scope.** This plan defines the inspection method and accept/reject criteria for BP-02. Because each +part has quantity 1, the **First-Article Inspection (FAI) *is* the production acceptance**. The +**bearing-race journals** (Ø40 j6 / Ø30 j6), the **journal-to-register concentricity (0.02 mm TIR)**, +and the **harmonic-drive registers** (Ø80 / Ø64) are the **critical characteristics** and receive +**100% verification**; general dimensions are verified to **ISO 2768-mK** on the FAI. All limits are +dual-unit and derived from `partspec` / the NW-TP control drawings — nothing invented. + +**Standards.** ASME Y14.5-2018 (datum alignment / GD&T interpretation); ISO 286 (j6 / h6 / H7 limits); +ISO 2768 (general tolerance & threads); ASME B46.1 (surface texture measurement); ASTM A967 +(passivation verification); ISO 1 (20 °C reference temperature). + +--- + +## 1. Inspection method + +1. **Diameters.** Bearing journals and registers measured with **CMM or bench-centre + calibrated bore + micrometer / air gauge**, ≥ 3 axial stations × ≥ 4 angular positions; report least-squares diameter, + roundness, and taper. +2. **Concentricity / run-out.** Part supported **between centres (or in matched V-blocks)**; journal + and register **total indicated run-out (TIR)** read with a dial/probe against the datum axis + (`SOW.md` §4). Datum-axis assignment per the FAI datum frame (ASSUMED — see §7). +3. **Threads (NW-TP-001).** M12 stud and safety-stop verified with **go/no-go ring/plug gauges** + (class 6g ASSUMED); root fillet R3.0 by radius gauge / optical comparator. +4. **Surface finish.** Profilometer per ASME B46.1 on journals, registers, and general OD. +5. **Metrology environment:** temperature-controlled at **20 °C** per ISO 1; parts thermally soaked + before measurement (**ASSUMED soak time — bidder to state**). +6. **Measurement uncertainty:** CMM/gauge + fixturing uncertainty **≤ 10% of the tolerance band** for + the j6 journals (gauge R&R / MSA evidence on request) — **ASSUMED acceptance gate, bidder/PE to + confirm**. +7. **State of measurement:** passivation is not a coating and does not build thickness; journals are + measured **as delivered (post-passivation)**. + +--- + +## 2. Acceptance criteria — NW-TP-002 (RA drive spindle) + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Bearing journal Ø40 **j6** (7008 race) | Ø40.0 [1.575] | **Ø39.995 – 40.011 mm** (j6, ASSUMED grade) | CMM / air gauge | **Critical — 100%** | +| Journal-to-register concentricity | — | **≤ 0.02 mm [0.0008 in] TIR** | between centres, dial/probe | **Critical — 100%** | +| Journal roundness / taper | — | within the j6 band | CMM | Critical | +| Journal-shoulder squareness / preload land | — | per PE GD&T *(value ASSUMED)* | CMM | Critical | +| CSF-32 register Ø80 | Ø80.0 [3.150] | locating fit *(H7/h6 ASSUMED)* | CMM / micrometer | Major | +| Bolt pattern 8 × M4 on Ø104 PCD | Ø104.0 [4.094] | position *(ASSUMED)* | CMM | Major | +| Body dia Ø88.9 | Ø88.9 [3.500] | ISO 2768-mK *(ASSUMED)* | micrometer | Minor | +| Length | 101.6 [4.000] | ISO 2768-mK | calipers/CMM | Minor | +| Journal surface finish | — | **Ra ≤ 0.4 µm [16 µin]** *(ASSUMED)* | profilometer | Major | +| General surface finish | — | **Ra 0.8 µm [32 µin]** | profilometer | Minor | + +## 3. Acceptance criteria — NW-TP-003 (DEC drive spindle) + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Bearing journal Ø30 **j6** (7006 race) | Ø30.0 [1.181] | **Ø29.996 – 30.009 mm** (j6, ASSUMED grade) | CMM / air gauge | **Critical — 100%** | +| Journal-to-register concentricity | — | **≤ 0.02 mm [0.0008 in] TIR** | between centres, dial/probe | **Critical — 100%** | +| Journal roundness / taper | — | within the j6 band | CMM | Critical | +| Journal-shoulder squareness / preload land | — | per PE GD&T *(value ASSUMED)* | CMM | Critical | +| CSF-25 register Ø64 | Ø64.0 [2.520] | locating fit *(H7/h6 ASSUMED)* | CMM / micrometer | Major | +| Bolt pattern 8 × M4 on Ø83 PCD | Ø83.0 [3.268] | position *(ASSUMED)* | CMM | Major | +| Body dia Ø76.2 | Ø76.2 [3.000] | ISO 2768-mK *(ASSUMED)* | micrometer | Minor | +| Length | 88.9 [3.500] | ISO 2768-mK | calipers/CMM | Minor | +| Journal surface finish | — | **Ra ≤ 0.4 µm [16 µin]** *(ASSUMED)* | profilometer | Major | +| General surface finish | — | **Ra 0.8 µm [32 µin]** | profilometer | Minor | + +## 4. Acceptance criteria — NW-TP-001 (counterweight shaft, OPTIONAL) + +> Inspect **only if the counterweighted variant is bid** (§ `SOW.md` §4). The selected build is +> counterweight-free. + +| Characteristic | Nominal (mm [in]) | Accept limits | Method | Class | +|---|---|---|---|---| +| Shaft diameter | Ø31.75 [1.250] | ISO 2768-mK | micrometer | Minor | +| Length | 457.2 [18.000] | ISO 2768-mK | tape/CMM | Minor | +| Stud end thread | M12 × 40 | 6g go/no-go *(ASSUMED)* | ring gauge | Major | +| Safety-stop thread | M12 | 6g go/no-go *(ASSUMED)* | ring gauge | Major | +| Root fillet | R3.0 [0.118] | radius gauge | comparator | Minor | +| Surface finish | — | **Ra 0.8 µm [32 µin]** | profilometer | Minor | + +--- + +## 5. Passivation verification (all parts) + +| Characteristic | Requirement | Method | Class | +|---|---|---|---| +| Passivation | Per **ASTM A967** (nitric or citric) | certificate + acceptance test | **Critical — 100%** | +| Free-iron / passivity check | Pass the stated ASTM A967 test | water-immersion / high-humidity / salt-spray / copper-sulphate *(specific test ASSUMED — bidder/PE to confirm)* | Critical | + +> **303 note.** Because 303 is a **free-machining, high-sulphur** grade, verify passivity with a test +> qualified for free-machining stainless; report the process (solution, concentration, time, +> temperature) and the acceptance-test result on the certificate (`SOW.md` §6). + +--- + +## 6. Sampling & documentation + +- **Critical characteristics** (j6 journals, journal-to-register concentricity, registers, passivation): + **100%** — each part is qty 1, so FAI = production acceptance. +- **Major / Minor characteristics:** verified on the FAI to ISO 2768-mK. + +The bidder shall deliver, per part: + +1. **FAI report** — every characteristic in §2–§5 with measured value vs. limit and pass/fail. +2. **CMM / gauge raw output** — probe data + datum alignment record (Y14.5 frame) + run-out traces. +3. **Material certificate** (ASTM A582) with heat/lot traceability and chemistry. +4. **Passivation certificate** (ASTM A967) with process + acceptance-test result. +5. **Nonconformance / RFI dispositions** for any ASSUMED value or out-of-tolerance condition. + +| Role | Name | Signature | Date | +|---|---|---|---| +| Bidder QA | | | | +| Owner / PE review | | | | + +--- + +## 7. ASSUMED-value register (do NOT treat as final) + +The following acceptance values are **ASSUMED design-intent — bidder/PE to confirm** before they can be +applied as pass/fail gates: + +| # | ASSUMED value | Part(s) | +|---|---|---| +| 1 | Journal fit grade (j6 shown vs h6 general note; k5/k6 alt for rotating inner ring) | NW-TP-002, -003 | +| 2 | Bearing-journal surface finish Ra ≤ 0.4 µm | NW-TP-002, -003 | +| 3 | Journal-shoulder squareness / axial preload land | NW-TP-002, -003 | +| 4 | Datum-axis assignment for the 0.02 mm TIR run-out | NW-TP-002, -003 | +| 5 | CSF register fit class (H7/h6) and form (OD pilot vs bore) | NW-TP-002, -003 | +| 6 | Bolt-pattern position (8 × M4 on Ø104 / Ø83 PCD) | NW-TP-002, -003 | +| 7 | Spindle body diameters (Ø88.9 / Ø76.2) | NW-TP-002, -003 | +| 8 | Drawbar / drawtube thread | NW-TP-002 | +| 9 | Thread class (6g) & safety-stop detail | NW-TP-001 | +| 10 | Passivation method (nitric/citric) & specific acceptance test | all | +| 11 | CMM/gauge uncertainty gate (≤ 10% of tol band), soak time | all | diff --git a/design/mechanical/tender/packages/BP-03_pier_foundation/README.md b/design/mechanical/tender/packages/BP-03_pier_foundation/README.md new file mode 100644 index 0000000..8778f4f --- /dev/null +++ b/design/mechanical/tender/packages/BP-03_pier_foundation/README.md @@ -0,0 +1,199 @@ +# BP-03 — Pier & Foundation (Reinforced Concrete + Structural Steel) — Bidder README + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | **BP-03 — Pier & foundation** | +| Trade | Reinforced concrete (cast-in-place) + structural steel + light CNC | +| Parts | NW-PF-001, NW-PF-002, NW-PF-003, NW-PF-004 (4 line items; 3 fabrication control drawings) | +| Branch / project | `design/mechanical-tender` — NIGHTWATCH Observatory Mount & Roll-off Tender | +| Issue date | 2026-08-06 | +| Revision | A | +| Governing TDP standard | MIL-STD-31000A (Technical Data Package) | + +This README is the entry point for the concrete/foundation contractor bidding BP-03. It defines the +**scope**, the **data package the contractor receives**, the **bidder deliverables**, the **PE-STAMP +GATE**, and the items carried as **ASSUMED design-intent**. Read it with `SOW.md` (per-part +specification) and `acceptance.md` (cylinder breaks, anchor pull, plumb/level) in this same folder. + +--- + +## 0. PE-STAMP GATE — READ FIRST (this package is NOT releasable for construction) + +> **⚠ THE PIER AND FOUNDATION ARE ISSUED FOR PE REVIEW ONLY.** +> **A licensed Professional Engineer, registered in the jurisdiction of the build site, MUST review, +> size, and STAMP the reinforced-concrete pier, its reinforcement, its embedment, and the anchor +> bolts BEFORE any permit is drawn or any concrete is placed.** Nothing in this package — dimensions, +> rebar schedule, anchor sizing, embedment — is a released structural design. Every structural +> quantity herein is either **SOURCED from the proven parametric model** (envelope geometry, f'c, +> embedment) or **ASSUMED design-intent** (rebar schedule, anchor grade/count, cover, seismic +> detailing) and is presented **for the PE to confirm, complete, and stamp** under the governing +> building code and the referenced load standard. + +**Why a stamp is mandatory.** The build site is a remote, seismically-active, high-desert Nevada +location (**site datum 38.9 °N / −117.4 °W, elevation ~1800 m [~5900 ft]**). Three structural loads +govern and are **owner/PE responsibilities under ASCE 7**: + +| Load | Basis (design study) | What it drives | +|---|---|---| +| **Wind** | ASCE 7 basic wind ~105 mph 3-sec gust, Risk Category I (survival, roof closed) | roof/enclosure hold-down, pier overturning | +| **Snow** | ~25 psf ground snow, high-desert @ ~1800 m | enclosure (BP-04), not the pier directly | +| **Seismic** | S_DS ≈ 0.5 g (Walker Lane vicinity) | pier base shear, anchorage, embedment | + +The wind basic speed (105 mph) and seismic S_DS (0.5 g) are **ASSUMED** in the design study because +the repository was silent on them — they are **exactly the values the PE must set from the site's +ASCE 7 hazard data and stamp.** See `MECHANICAL_DESIGN.md` §7 (wind — FAIL/remediated) and §8 +(pier — PASS) for the computed basis. + +**No concrete is placed, and no anchor is cast, until the stamped drawings and the PE-approved +Inspection & Test Plan (`acceptance.md`) are in hand.** + +--- + +## 1. Scope + +BP-03 covers the **telescope pier and its foundation interface** — the cast-in-place reinforced-concrete +pier, the steel top plate grouted to it, the aluminium adapter plate that orients the mount to site +latitude, and the cast-in anchor-bolt set with its setting template. The contractor: + +1. Places a **cast-in-place reinforced-concrete pier** (NW-PF-001) to f'c = 4000 psi (27.6 MPa) per + **ACI 318 / ACI 301**, with a rebar cage per the PE-stamped schedule (baseline 6 × #4 vertical + + #3 ties per **ASTM A615 Gr60**), **structurally isolated from any building slab or footing.** +2. Casts the **anchor-bolt set** (NW-PF-004, **ASTM F1554 Gr36**, per **ACI 318 Ch.17**) into the wet + pier via a fabricated setting template so the projecting bolts match the top-plate pattern. +3. Supplies the **galvanized steel top plate** (NW-PF-002, **ASTM A36**, hot-dip galv per **ASTM A123**) + and the **anodized aluminium adapter plate** (NW-PF-003, **6061-T6** per ASTM B209, Type III + hardcoat per **MIL-A-8625F**), or subcontracts their cut/machine scope. +4. Grouts, levels, and plumbs the plate stack, then inspects per `acceptance.md` (concrete cylinder + breaks, anchor proof/pull, plumb/level) and delivers a certified, PE-signed foundation. + +**Out of scope for BP-03** (separate packages — the pier *interfaces* to these but the contractor does +not supply them): + +| Interface | Supplied under | Notes | +|---|---|---| +| RA (polar) axis housing NW-MH-001 | BP-01 (mount head) | Bolts to the adapter plate NW-PF-003 (4 × M6). | +| Roll-off roof steelwork + **wind hold-down anchor brackets NW-RR-006** | BP-04 | The **4× survival-wind roof hold-downs** anchor the enclosure/building foundation, **not** the telescope pier (the pier is isolated). See §4. | +| Harmonic drives, bearings, encoders, fasteners | BP-05 (COTS) | Not a pier interface. | +| Enclosure/building foundation | Owner / site civil (not in this tender scope) | The pier passes **through** but is **isolated from** the building slab. | + +--- + +## 2. Data package the contractor receives (per MIL-STD-31000A) + +| Item | Format / standard | Role | +|---|---|---| +| 3-D solid master (pier + plates) | **STEP ISO 10303 AP242** | As-modelled geometry master. | +| Control drawings NW-PF-001 / -002 / -003 | **PDF** (drawing practices ASME Y14.100; drawing types Y14.24; sheet & title block Y14.1) | Envelope + title block + key dimensions; bid control drawing. | +| **PE-stamped structural drawings** | PDF (issued by owner's PE) | **Governs for construction** — supersedes the ASSUMED rebar/anchor/embedment values herein. | +| `SOW.md` | Markdown | Per-part spec: concrete, rebar, anchors, isolation, embedment, finishes. | +| `acceptance.md` | Markdown | Concrete cylinder breaks, anchor pull/proof, plumb/level, placement inspection. | +| `cut_list.csv` (rows NW-PF-001/002/003) | CSV/XLSX | Stock size, cut allowance, stock mass. | +| `cots_schedule.csv` (row NW-PF-004) | CSV/XLSX | Anchor-bolt buy item + template. | + +**Order of precedence.** For the **fabricated plates** (NW-PF-002/-003) the **STEP AP242 solid +governs geometry**; the drawing + SOW govern tolerances, finish, and material. **For the +reinforced-concrete pier, the reinforcement, the embedment, and the anchor bolts, the PE-STAMPED +STRUCTURAL DRAWINGS GOVERN and supersede every number in this package.** Where any document disagrees, +the **PE-stamped set controls** and the bidder shall raise an RFI before placing concrete. General +tolerances per **ISO 2768**; plate GD&T per **ASME Y14.5-2018**; fits per **ISO 286**. + +--- + +## 3. Line-item summary (source: `partspec.parts_for('BP-03')` + BOM/cut list) + +| Part no. | Name | Material | Stock / size | Qty | Stock mass | Finish | Fab drawing | +|---|---|---|---|---:|---:|---|---| +| **NW-PF-001** | Reinforced-concrete telescope pier | Cast-in-place concrete **f'c = 4000 psi (27.6 MPa)** per ACI 318 / ACI 301 | Round pier **Ø304.8 mm [12.000 in] × 1828.8 mm [72.000 in]** overall | 1 | 320.26 kg (≈0.13 m³ [≈4.7 ft³ / ≈0.18 yd³] concrete) | none (formed) | NW-PF-001 | +| **NW-PF-002** | Pier top plate | ASTM A36 structural steel | Plate **304.8 mm [12.000 in] × 304.8 mm [12.000 in] × 9.5 mm [0.375 in]** | 1 | 6.94 kg | Hot-dip galvanize per ASTM A123 | NW-PF-002 | +| **NW-PF-003** | Pier-to-mount adapter plate | 6061-T6 aluminium per ASTM B209 | Plate **254.0 mm [10.000 in] × 254.0 mm [10.000 in] × 19.1 mm [0.750 in]** | 1 | 3.32 kg | Type III hardcoat anodize per MIL-A-8625F | NW-PF-003 | +| **NW-PF-004** | Anchor-bolt set + template | ASTM F1554 Gr36 galvanized | **4 × Ø19.05 mm [0.750 in] × 304.8 mm [12.000 in]** anchor bolts + setting template | 1 | — (buy + fabricate) | Hot-dip galvanize per ASTM A123 | none (buy item; template bidder-detailed) | + +Pier stock mass is the **quantity ordered** (stock envelope volume × concrete density 2400 kg/m³), +not a formed-net figure; the ≈0.13 m³ concrete volume is a **pour-planning estimate derived from the +Ø304.8 × 1828.8 mm envelope** — confirm against the stamped forming detail. Plate cut allowance is +3.0 mm on stock. NW-PF-004 carries **no fabrication control drawing** — the bolts are purchased to +ASTM F1554 and the setting template is fabricated by the bidder to the stamped top-plate/adapter hole +pattern (see `SOW.md` §5). + +--- + +## 4. Cross-package interfaces the contractor must preserve + +The plate stack chains the pier up to the mount head. Bottom-to-top: + +| Level | Feature | Mates to | Part | +|---|---|---|---| +| Pier top | 4 × cast-in anchor bolts, projection 50.0 mm [1.969 in] | Top-plate anchor holes | NW-PF-004 → NW-PF-002 | +| Top plate | 4 × Ø20.6 mm [Ø0.81 in] anchor holes (clearance for Ø0.75 F1554) | Cast-in anchors NW-PF-004 | NW-PF-002 | +| Top plate → adapter | Centre hole pattern 4 × Ø11.2 mm [Ø0.44 in] | Adapter pier-plate pattern | NW-PF-002 ↔ NW-PF-003 | +| Adapter → RA housing | 4 × M6 pattern + centre bore Ø90.0 mm [3.543 in] | RA (polar) axis housing | NW-PF-003 → **NW-MH-001 (BP-01)** | +| Adapter wedge | Latitude tilt to **38.9°** (wedge/shim, bidder to detail) | Sets polar axis to site latitude | NW-PF-003 | + +> **Wind hold-down clarification (important).** The **9.2 kN survival-wind roof uplift** is resisted by +> the **4× wind hold-down anchor brackets NW-RR-006 (BP-04)**, which anchor the **enclosure/building +> foundation** — because the design study assumes the **telescope pier is structurally isolated from +> the enclosure** (standard observatory practice, to keep vibration off the optics). The BP-03 +> anchor-bolt set NW-PF-004 casts into the **pier** and resists the **mount overturning + seismic** +> demand (pier proof §8). The registry nonetheless **sizes NW-PF-004 against the ~9.2 kN uplift number +> as a bounding case** — see §6 and `SOW.md` §5. **Where each anchor group lands, and the final anchor +> capacity, are PE coordination items — PE to confirm.** + +--- + +## 5. Bidder deliverables + +1. Finished pier (NW-PF-001), top plate (NW-PF-002), adapter plate (NW-PF-003), and cast anchor set + + template (NW-PF-004), installed, grouted, leveled, and plumbed. +2. **Concrete submittals** — approved mix design (f'c = 4000 psi per ACI 301), batch tickets, and + **cylinder break reports** (7-day and 28-day) per `acceptance.md`. +3. **Reinforcement certs** — mill certs for the #4 / #3 bar (ASTM A615 Gr60) and a rebar-placement / + cover inspection record against the stamped cage. +4. **Anchor-bolt certs** — mill cert / CoC for the ASTM F1554 Gr36 bolts, galvanize cert (ASTM A123), + and the **anchor proof/pull test record** per `acceptance.md`. +5. **Steel & aluminium plate certs** — A36 material cert + galvanize (ASTM A123) coating-thickness + record for NW-PF-002; 6061-T6 cert (ASTM B209) + Type III anodize cert (MIL-A-8625F) for NW-PF-003. +6. **Plumb / level survey** — pier plumb ≤ 1:200 and top-of-plate level ≤ 0.5° per `acceptance.md`. +7. **PE sign-off** — the PE-stamped structural drawings and the PE's field-inspection/hold-point + sign-offs (rebar, embedment, pour, anchor set). +8. RFI / nonconformance log resolving every ASSUMED value before it is built. + +--- + +## 6. ASSUMED design-intent — bidder / PE to confirm (NOT final) + +Carried as **ASSUMED design-intent** in the parts registry; **not** released dimensions. The PE shall +confirm/complete and stamp before construction: + +- **Rebar schedule** — baseline **6 × #4 vertical + #3 ties @ 12 in [304.8 mm] oc** (ASTM A615 Gr60) + is **ASSUMED design-intent**; bar size/count, tie spacing, seismic hoop detailing, splices, and + **concrete cover** (ASSUMED ~76 mm [3 in] cast against earth per ACI 318) are **PE to set.** +- **Anchor-bolt sizing** — **4 × Ø0.75 in [19.05 mm] F1554 Gr36**, embedment 304.8 mm [12.000 in], + projection 50.0 mm [1.969 in], is **ASSUMED per ACI 318 Ch.17**; final diameter, grade, embedment, + edge distance, and count are **PE to size for the governing wind/seismic uplift (~9.2 kN roof-uplift + bounding case; net anchor demand 7.4 kN → 4× 2 klbf gives SF 3.9).** +- **Embedment vs frost** — 914.4 mm [36.000 in] embedment is **SOURCED**; it exceeds the **ASSUMED** + central-NV frost line of 0.3–0.6 m [12–24 in] (SF ≈ 1.52). PE to confirm the site frost depth and + any bearing/bell-footing detail. +- **Latitude wedge on NW-PF-003** — the 38.9° polar-axis tilt (wedge or shim set) is **ASSUMED + design-intent; adapter geometry is not fixed in the repo** — bidder to detail, PE to confirm. +- **Wind speed / seismic parameters** — basic wind 105 mph and S_DS 0.5 g are **ASSUMED (ASCE 7); + PE to set from site hazard data.** +- **Grout, leveling nuts, and isolation gap detail** between pier and any building slab — ASSUMED; + bidder/PE to detail (the pier **must not** bear on or bond to the slab). + +--- + +## 7. References + +- Parts registry: `design/mechanical/tender/gen/partspec.py` → `parts_for('BP-03')` (single source of truth). +- BOM / cut list / COTS: `design/mechanical/tender/bom/{master_bom,cut_list,cots_schedule}.csv`. +- Control drawings: `design/mechanical/tender/drawings/NW-PF-00{1,2,3}.svg`. +- Design basis: `design/mechanical/MECHANICAL_DESIGN.md` §7 (wind loads — FAIL/remediated), + §8 (pier/foundation — PASS, governing SF 6.8); `design/mechanical/tradestudy/SECTION.md` item 5 + (pier — Concrete Sonotube baseline retained). +- Standards register: MIL-STD-31000A; ACI 318 (Ch.17 anchoring), ACI 301 (concrete spec); ASCE 7 + (wind/snow/seismic); ASTM A615 Gr60 (rebar), ASTM F1554 Gr36 (anchor bolts), ASTM A36 (steel), + ASTM A123 (hot-dip galvanize), ASTM B209 (6061-T6), MIL-A-8625F (Type III anodize); + ASME Y14.5-2018 / Y14.100 / Y14.24 / Y14.1; ISO 286; ISO 2768. diff --git a/design/mechanical/tender/packages/BP-03_pier_foundation/SOW.md b/design/mechanical/tender/packages/BP-03_pier_foundation/SOW.md new file mode 100644 index 0000000..6a3f86a --- /dev/null +++ b/design/mechanical/tender/packages/BP-03_pier_foundation/SOW.md @@ -0,0 +1,249 @@ +# BP-03 — Statement of Work: Pier & Foundation (Reinforced Concrete + Structural Steel) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-03 — Pier & foundation | +| Parts | NW-PF-001 (RC pier), NW-PF-002 (top plate), NW-PF-003 (adapter plate), NW-PF-004 (anchor set + template) | +| Process | Cast-in-place reinforced concrete; laser/waterjet + drill (steel plate); CNC mill (aluminium plate); buy + template (anchors) | +| Site datum | 38.9 °N / −117.4 °W, elevation ~1800 m [~5900 ft], remote high-desert Nevada, seismically active | +| Issue date / rev | 2026-08-06 / A | + +> ## PE-STAMP GATE +> **The reinforced-concrete pier (NW-PF-001), its reinforcement, its embedment, and the anchor-bolt set +> (NW-PF-004) are ISSUED FOR PE REVIEW.** A **licensed Professional Engineer**, registered in the +> build-site jurisdiction, **must review, size, complete, and STAMP** these items under the governing +> building code and **ASCE 7** (wind/snow/seismic) **before any permit is issued or any concrete is +> placed.** All structural quantities below are **SOURCED envelope/material values** or **ASSUMED +> design-intent** presented for the PE — none is a released structural design. + +**Purpose.** This SOW specifies the concrete, reinforcement, anchorage, isolation, embedment, +finishing, and certification requirements for the four BP-03 parts. It is read with the PE-stamped +structural drawings (which **govern for construction**), the STEP ISO 10303 AP242 solids (plate +geometry), the NW-PF control drawings (PDF), and `acceptance.md`. All dimensions are dual-unit, +**mm primary, inch in brackets**, drawn from the parts registry (`partspec.parts_for('BP-03')`) and +the BOM/cut list — **no dimension herein is invented.** + +**Standards applied.** MIL-STD-31000A (TDP); **ACI 318** (structural concrete, incl. **Ch.17** +anchoring-to-concrete); **ACI 301** (specifications for structural concrete); **ASCE 7** +(wind/snow/seismic loads); **ASTM A615 Gr60** (deformed reinforcing bar); **ASTM F1554 Gr36** (anchor +bolts); **ASTM A36** (structural steel); **ASTM A123** (hot-dip galvanize); **ASTM B209** (6061-T6 +plate); **MIL-A-8625F** (Type III hardcoat anodize); **ASME Y14.5-2018 / Y14.100 / Y14.24 / Y14.1** +(plate drawings); **ISO 286** (fits); **ISO 2768** (general tolerances). Concrete field/lab test +methods are those **invoked by ACI 301** — see `acceptance.md`. + +--- + +## 1. General requirements (all parts) + +1. **PE authority.** The PE-stamped structural drawings supersede every rebar, anchor, embedment, and + reinforcement value in this SOW. Where this SOW and the stamped set disagree, **the stamped set + controls**; the bidder shall RFI before placing concrete. +2. **Materials & certification.** Furnish mill certs / CoC with heat/lot traceability for: the concrete + mix (ACI 301 approved mix design, f'c = 4000 psi), the reinforcing bar (ASTM A615 Gr60), the anchor + bolts (ASTM F1554 Gr36), the A36 plate, and the 6061-T6 plate (ASTM B209). Mark fabricated plates + with part number and revision per ASME Y14.100. +3. **General tolerance.** Plate dimensions to **ISO 2768-mK**; concrete workmanship tolerances per + **ACI 117** as invoked by ACI 301 (**ASSUMED — PE/bidder to confirm the tolerance class**). +4. **Isolation (MANDATORY).** The pier is **structurally isolated** from any building slab, footing, or + floor — no bearing, no bond, no shared reinforcement. Maintain an isolation gap/joint (compressible + filler) around the pier where it passes any slab (detail bidder/PE to confirm). This keeps floor + footfall and enclosure vibration off the optical train. +5. **Embedment vs frost.** Pier embedment 914.4 mm [36.000 in] (SOURCED) shall be verified by the PE + to exceed the **site frost depth** (ASSUMED 0.3–0.6 m [12–24 in] for central-NV high desert; + SF ≈ 1.52) and to satisfy seismic/overturning fixity. +6. **Cold-joint / lift control, curing, and hot/cold-weather placement** per ACI 301. + +--- + +## 2. NW-PF-001 — Reinforced-concrete telescope pier + +**Drawing:** NW-PF-001 (rev A). **Envelope:** round pier **Ø304.8 mm [12.000 in] × 1828.8 mm +[72.000 in]** overall (height above grade 914.4 mm [36.000 in] + embedment 914.4 mm [36.000 in]). +**Stock/pour mass:** 320.26 kg (≈0.13 m³ [≈4.7 ft³ / ≈0.18 yd³] concrete — pour-planning estimate +from the envelope; `cut_list.csv` row NW-PF-001). **Material:** cast-in-place concrete **f'c = 4000 psi +(27.6 MPa)** per **ACI 318 / ACI 301**. + +**Controlling dimensions (SOURCED = P.PIER):** + +| Feature | Nominal (mm [in]) | Source | +|---|---|---| +| Diameter | Ø **304.8** [12.000] | SOURCED | +| Height above grade | **914.4** [36.000] | SOURCED | +| Embedment | **914.4** [36.000] | SOURCED | +| Overall length | **1828.8** [72.000] | SOURCED | +| f'c | **4000 psi (27.6 MPa)** | SOURCED | + +**Reinforcement schedule (ASSUMED design-intent — PE to confirm/complete/stamp):** + +| Item | Baseline callout | Bar dia (mm [in]) | Standard | Status | +|---|---|---|---|---| +| Vertical bars | **6 × #4** | Ø12.7 [0.500] | ASTM A615 Gr60 | ASSUMED — PE to set count/size | +| Ties / hoops | **#3 @ 12 in [304.8 mm] oc** | Ø9.5 [0.375] | ASTM A615 Gr60 | ASSUMED — PE to set spacing + seismic hoop detail | +| Clear cover | **~76 mm [3.00 in]** cast against earth | — | ACI 318 §20.5 | ASSUMED — PE to confirm | +| Splices / development / hooks | per ACI 318 Ch.25 | — | ACI 318 | PE to detail | + +> **ASSUMED design-intent — bidder/PE to confirm.** The rebar cage (6 × #4 vertical, #3 ties), cover, +> tie spacing, seismic hoop confinement, and lap splices are **ASSUMED**. In a Walker-Lane-vicinity +> seismic zone (S_DS ≈ 0.5 g, ASSUMED) the transverse reinforcement/confinement detailing is a +> **PE-stamped** decision. The design study models the pier as an unreinforced circular cantilever for +> tilt/frequency (concrete SF 111 compression / 13.8 tension) — reinforcement is required by code for +> ductility/anchorage regardless of the low computed stress. + +**Structural basis (from `MECHANICAL_DESIGN.md` §8 — pier PASS, governing SF 6.8):** + +| Quantity | Value | Note | +|---|---|---| +| Pier pointing tilt @ 35 mph gust | 0.70″ | vs 5″ budget → SF 7.2 | +| Pier first mode | 151.9 Hz | vs 10 Hz target → SF 15.2 | +| Seismic overturning SF (embed + weight) | 7.0 | dead-weight-only 0.81 → **embedment governs** | +| Sliding SF (passive / base shear) | 6.8 | governing SF | +| Concrete net compression / f'c | SF 111 | — | +| Concrete net tension / modulus of rupture | SF 13.8 | f_r = 0.62√f'c = 3.26 MPa (ASSUMED code value) | +| Embedment / frost depth | SF 1.52 | 0.914 m vs ASSUMED 0.6 m frost | + +**Workmanship tolerances (drawing):** **plumb ≤ 1:200**; **top-of-pier level ≤ 0.5°**. Anchor cage + +setting template (NW-PF-004) cast integrally — see §5. + +**Provenance / flags.** Ø/height/embed/f'c = P.PIER (**SOURCED**); rebar schedule **ASSUMED +design-intent (PE to confirm)**. **REQUIRES PE STAMP.** + +--- + +## 3. NW-PF-002 — Pier top plate + +**Drawing:** NW-PF-002 (rev A). **Stock:** A36 plate **304.8 mm [12.000 in] × 304.8 mm [12.000 in] × +9.5 mm [0.375 in]**, cut allowance 3.0 mm, stock mass 6.94 kg (`cut_list.csv` row NW-PF-002). +**Process:** laser/waterjet cut + drill. **Finish:** **hot-dip galvanize per ASTM A123** (after all +cutting/drilling). + +| Feature | Nominal (mm [in]) | Tolerance | Note | +|---|---|---|---| +| Side (square) | **304.8** [12.000] | ISO 2768-mK | matches pier diameter | +| Thickness | **9.5** [0.375] | ISO 2768-mK | A36 plate | +| Anchor holes | **4 × Ø20.6 [Ø0.81]** | ± 0.25 mm pattern | clearance for Ø0.75 F1554 (NW-PF-004) | +| Centre pattern | **4 × Ø11.2 [Ø0.44]** | ± 0.25 mm pattern | matches NW-PF-003 adapter | +| Surface finish | Ra 3.2 µm [125 µin] | — | cut faces | + +**Notes.** Grouted onto the pier over the cast-in anchor bolts, then leveled and torqued with +leveling/plate nuts (bidder to detail; leveling-nut vs full-grout method **ASSUMED — PE to confirm**). +Galvanize per ASTM A123 with coating-thickness record; ream/retap holes after galvanize as needed to +clear the anchors and the Ø0.44 pattern. + +**Provenance / flags.** Side/thickness = P.PIER (**SOURCED**); hole patterns **DERIVED** to match +NW-PF-004 (Ø0.81) and NW-PF-003 (Ø0.44) → hole positions **ASSUMED design-intent, bidder/PE to +confirm** against the stamped anchor layout. + +--- + +## 4. NW-PF-003 — Pier-to-mount adapter plate + +**Drawing:** NW-PF-003 (rev A). **Stock:** 6061-T6 plate **254.0 mm [10.000 in] × 254.0 mm [10.000 in] +× 19.1 mm [0.750 in]**, cut allowance 3.0 mm, stock mass 3.32 kg (`cut_list.csv` row NW-PF-003). +**Process:** CNC mill. **Finish:** **Type III hardcoat anodize, 0.002 in, Class 1 (clear) per +MIL-A-8625F** (after machining; mask bores/threads). + +| Feature | Nominal (mm [in]) | Tolerance | Note | +|---|---|---|---| +| Side (square) | **254.0** [10.000] | ISO 2768-mK | — | +| Thickness | **19.1** [0.750] | ISO 2768-mK | — | +| RA-housing pattern | **4 × M6** | position *(ASSUMED)* | matches **NW-MH-001 (BP-01)** | +| Pier-plate pattern | **4 × Ø11.2 [Ø0.44]** | position *(ASSUMED)* | matches **NW-PF-002** | +| Centre bore | Ø **90.0** [3.543] | H8 pilot *(ASSUMED)* | cable/drawbar clearance | +| Face flatness | ▱ 0.05 mm to mounting face | per Y14.5 | mount seating | +| Surface finish | Ra 3.2 µm [125 µin] | — | — | + +> **ASSUMED design-intent — bidder/PE to confirm.** The adapter **bridges the 12×12 in pier plate to +> the 8×8 in RA housing and provides the latitude wedge to orient the polar (RA) axis to the site +> latitude 38.9°** (wedge plate or shim set). **Adapter geometry (patterns, centre bore, wedge angle) +> is not fixed in the repository** — it is ASSUMED design-intent; the bidder details the wedge/shim and +> the PE confirms. Preserve the 4 × M6 RA-housing pattern and the 4 × Ø0.44 pier-plate pattern so the +> stack assembles. + +**Provenance / flags.** ASSUMED design-intent — adapter geometry not in repo (bridges 12×12 plate to +8×8 housing). Levels/orients the RA axis to site latitude 38.9° (wedge or shim set, bidder to detail). + +--- + +## 5. NW-PF-004 — Anchor-bolt set + setting template + +**Buy + fabricate** (no fabrication control drawing SVG). **Anchors:** **4 × Ø19.05 mm [0.750 in] × +304.8 mm [12.000 in]**, **ASTM F1554 Gr36**, **hot-dip galvanized per ASTM A123**, cast into the wet +pier per **ACI 318 Ch.17** (anchoring to concrete). **Projection** 50.0 mm [1.969 in] above the pier +top. **Template:** rigid setting template (ASSUMED 0.25 in [6.35 mm] plywood/ply-steel), hole position +tolerance **0.02** (drawing callout), matching the NW-PF-002 anchor pattern. + +| Feature | Nominal (mm [in]) | Standard | Status | +|---|---|---|---| +| Bolt diameter | Ø **19.05** [0.750] | ASTM F1554 Gr36 | ASSUMED — PE to size | +| Embedment (into pier) | **304.8** [12.000] | ACI 318 Ch.17 | ASSUMED — PE to size | +| Projection | **50.0** [1.969] | — | for plate + nut stack | +| Count | **4** | — | ASSUMED — PE to set | +| Template hole tolerance | **± 0.02 mm** pattern | — | so bolts drop through NW-PF-002 | + +> **Anchor sizing tied to the wind-uplift number — PE to confirm (per task + registry provenance).** +> The registry sizes NW-PF-004 against the governing **survival-wind roof-uplift** case from +> `MECHANICAL_DESIGN.md` §7: **roof gross uplift 9.2 kN (9163 N)** minus roof self-weight 1765 N → +> **net anchor demand 7.4 kN (7398 N)**; **4× 2 klbf [≈8.9 kN] anchors → capacity 35.6 kN → SF 3.9** +> (the wind remediation). **However:** the design study assumes the **telescope pier is isolated from +> the enclosure**, so the **9.2 kN roof uplift is actually carried by the enclosure/building hold-downs +> (NW-RR-006, BP-04), not the pier.** The pier anchors NW-PF-004 resist the **mount overturning + +> seismic** demand (base shear 1008 N, overturning 673.8 Nm; embedment governs). The 9.2 kN figure is +> retained as the **bounding tension case** for anchor selection. **Final anchor diameter, grade, +> embedment, edge distance, count, and the pier-vs-enclosure anchor split are ASSUMED design-intent — +> the PE shall size and stamp them per ACI 318 Ch.17 and ASCE 7.** + +**Setting procedure.** Position the template on the form; hang the 4 anchors plumb at the specified +projection; verify pattern against NW-PF-002 before the pour; place concrete; do not disturb until +initial set; strip template after cure. Cast integrally with the pier — **no post-installed anchors** +unless the PE approves and re-specifies to ACI 318 Ch.17 post-installed provisions. + +**Provenance / flags.** ASSUMED design-intent per ACI 318 Ch.17 (PE to size for wind/seismic uplift +~9.2 kN). Cast into pier via template; sizing tied to the wind proof roof/pier uplift — **PE to +confirm.** + +--- + +## 6. Finishing + +| Part | Finish | Standard | Notes | +|---|---|---|---| +| NW-PF-001 pier | formed concrete, cured | ACI 301 | rub/patch form-tie holes; no coating unless PE specifies | +| NW-PF-002 top plate | **hot-dip galvanize** | **ASTM A123** | after cut/drill; coating-thickness record; clear holes post-dip | +| NW-PF-003 adapter | **Type III hardcoat anodize 0.002 in, Class 1 (clear)** | **MIL-A-8625F** | after machining; mask centre bore + threads | +| NW-PF-004 anchors | **hot-dip galvanize** | **ASTM A123** | full-length; protect threads/projection during pour | + +--- + +## 7. Submittals & deliverables + +1. Installed pier + plate stack + cast anchor set (NW-PF-001..004), leveled and plumbed. +2. **Concrete:** ACI 301 approved mix design, batch tickets, **7-day + 28-day cylinder break reports** + (`acceptance.md`), slump/air records. +3. **Reinforcement:** ASTM A615 Gr60 mill certs + rebar-placement/cover inspection record vs the + stamped cage. +4. **Anchors:** ASTM F1554 Gr36 mill cert/CoC, ASTM A123 galvanize cert, **anchor proof/pull test + record**. +5. **Plates:** A36 cert + ASTM A123 coating-thickness (NW-PF-002); 6061-T6 cert (ASTM B209) + Type III + anodize cert (NW-PF-003). +6. **Plumb/level survey** (≤ 1:200 plumb; ≤ 0.5° top level). +7. **PE-stamped structural drawings + PE field hold-point sign-offs** (rebar, embedment, anchor set, + pour). +8. RFI/NCR log resolving every ASSUMED value before construction. + +--- + +## 8. ASSUMED design-intent register (this SOW) + +| # | Item | Part(s) | Status | +|---|---|---|---| +| 1 | Rebar schedule (6 × #4 vert + #3 ties), cover, seismic hoop detailing, splices | NW-PF-001 | ASSUMED — PE to set & stamp | +| 2 | Anchor sizing (Ø0.75 F1554 Gr36, embed, projection, count) vs ~9.2 kN uplift | NW-PF-004 | ASSUMED — PE to size (ACI 318 Ch.17) | +| 3 | Pier-vs-enclosure anchor split (9.2 kN roof uplift lands on BP-04 hold-downs) | NW-PF-004 / NW-RR-006 | ASSUMED — PE coordination | +| 4 | Site frost depth vs embedment; bearing/bell detail | NW-PF-001 | ASSUMED — PE to confirm | +| 5 | Wind speed (105 mph) / seismic S_DS (0.5 g) | all | ASSUMED (ASCE 7) — PE to set | +| 6 | Latitude wedge angle + adapter geometry (patterns, centre bore) | NW-PF-003 | ASSUMED — bidder to detail, PE to confirm | +| 7 | Grout/leveling-nut method + pier-slab isolation gap detail | NW-PF-001/002 | ASSUMED — bidder/PE to detail | +| 8 | Top-plate hole positions (Ø0.81 anchor, Ø0.44 adapter) | NW-PF-002 | ASSUMED — confirm vs stamped anchor layout | +| 9 | Concrete workmanship tolerance class (ACI 117) | NW-PF-001 | ASSUMED — PE/bidder to confirm | diff --git a/design/mechanical/tender/packages/BP-03_pier_foundation/acceptance.md b/design/mechanical/tender/packages/BP-03_pier_foundation/acceptance.md new file mode 100644 index 0000000..bee739e --- /dev/null +++ b/design/mechanical/tender/packages/BP-03_pier_foundation/acceptance.md @@ -0,0 +1,198 @@ +# BP-03 — Inspection & Test / Acceptance Plan: Pier & Foundation + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-03 — Pier & foundation | +| Parts | NW-PF-001 (RC pier), NW-PF-002 (top plate), NW-PF-003 (adapter plate), NW-PF-004 (anchor set) | +| Method | Concrete **cylinder breaks** + slump/air; **anchor proof/pull** test; **plumb/level** survey; rebar-placement & cover inspection; galvanize/anodize coating verification; plate dimensional check | +| Issue date / rev | 2026-08-06 / A | + +> ## PE-STAMP GATE — this plan is not executable until the PE approves it +> **The reinforced-concrete pier and anchor set are ISSUED FOR PE REVIEW.** This Inspection & Test Plan +> (ITP) and its accept/reject limits are **provisional pending the PE-stamped structural drawings.** +> The PE sets the final f'c acceptance basis, the reinforcement/cover inspection criteria, the anchor +> proof load, and owns the **field hold points** below. **No concrete is placed and no anchor is cast +> until the stamped drawings and the PE-approved ITP are in hand.** + +**Scope.** This plan defines the inspection methods and accept/reject criteria for BP-03. The concrete +strength (cylinder breaks), the anchor proof/pull, and the pier plumb/level are the **critical +characteristics**. All limits are dual-unit and derived from `partspec` / the NW-PF control drawings — +nothing invented. Concrete field/lab test methods are those **invoked by ACI 301**; where a specific +ASTM C-series method is named it is the method ACI 301 calls up (flagged **PE to confirm** if outside +the tender's standards register). + +**Standards.** ACI 318 (structural concrete; Ch.17 anchoring); ACI 301 (concrete spec, invokes the +field/lab test methods and ACI 117 tolerances); ASCE 7 (loads — PE); ASTM A615 Gr60 (rebar); +ASTM F1554 Gr36 (anchors); ASTM A123 (galvanize coating thickness); ASTM B209 (6061-T6); +MIL-A-8625F (anodize); ASME Y14.5-2018 (plate GD&T); ISO 286 / ISO 2768 (fits / general tolerance); +ISO 1 (20 °C metrology reference for the plates). + +--- + +## 1. Field hold points (PE / owner sign-off required to proceed) + +| # | Hold point | Verify before proceeding | Sign-off | +|---|---|---|---| +| H1 | **Excavation / embedment** | Depth ≥ 914.4 mm [36.000 in] and ≥ site frost depth; bearing soil per PE | PE | +| H2 | **Rebar cage set** | Bar size/count/spacing, cover, splices, seismic hoops vs stamped cage | PE | +| H3 | **Anchor template set** | 4 × F1554 plumb, projection 50.0 mm [1.969 in], pattern vs NW-PF-002 | PE | +| H4 | **Pre-pour** | Forms, isolation gap from any slab, cleanliness, mix delivered = approved mix | PE / owner | +| H5 | **Pour + cylinders cast** | Placement, consolidation, cylinders fabricated & cured | Inspector | +| H6 | **Strip / cure complete** | Form strip, cure duration, surface finish | Bidder QA | +| H7 | **28-day strength + anchor proof** | f'c met; anchor proof/pull passed; plumb/level survey | PE | + +--- + +## 2. Concrete — cylinder breaks & fresh-concrete tests (NW-PF-001) + +**Specified strength:** **f'c = 4000 psi (27.6 MPa)** at 28 days, per ACI 318 / ACI 301. + +| Test | Method (invoked by ACI 301) | Frequency | Accept criterion | +|---|---|---|---| +| **Compressive strength** | cast + cure standard cylinders; lab break (ACI 301 → ASTM C31 fabrication, ASTM C39 test) *(method PE to confirm)* | min. 1 set (pier is a single small pour); set = 2× 28-day + 1× 7-day + 1 spare | ACI 318 §26.12: avg of any 3 consecutive tests ≥ f'c **and** no single test < f'c − 500 psi [3.4 MPa] *(PE to confirm basis for a single-pour element)* | +| **7-day break (info)** | as above, 7-day | 1 cylinder | typically ≥ 65–70% f'c *(ASSUMED — trend indicator, not accept gate)* | +| **Slump** | ACI 301 → ASTM C143 | each load | per approved mix design ± tolerance *(PE to set)* | +| **Air content** | ACI 301 → ASTM C231 | each load (if air-entrained) | per approved mix *(PE to set — may be N/A)* | +| **Temperature** | ACI 301 (fresh-concrete temp) | each load | within hot/cold-weather limits (ACI 301) | + +**Notes.** Cylinders cured and broken by an approved testing lab. Because the pier is a **single small +pour (≈0.13 m³ [≈4.7 ft³])**, the sampling frequency and the single-element acceptance basis are +**PE to confirm** (a small element may use a reduced set with the PE accepting the risk). **Concrete +below f'c → PE disposition** (core testing per ASTM C42, load evaluation, or reject) — no +self-disposition by the bidder. + +--- + +## 3. Reinforcement placement & cover (NW-PF-001) + +| Characteristic | Criterion | Method | Class | +|---|---|---|---| +| Bar size / grade | #4 vertical, #3 ties, **ASTM A615 Gr60** (or PE-stamped substitute) | mill cert + tape/gauge | Critical | +| Bar count | 6 × vertical (or PE-stamped) | count at H2 | Critical | +| Tie spacing | #3 @ 12 in [304.8 mm] oc (or PE-stamped) | tape at H2 | Major | +| **Clear cover** | ~76 mm [3.00 in] cast against earth *(ASSUMED — PE to set)* | cover meter / spacers | Critical | +| Splices / hooks / development | per ACI 318 Ch.25 (PE-stamped) | visual at H2 | Critical | +| Seismic hoop/confinement detail | per PE (S_DS ≈ 0.5 g ASSUMED) | visual at H2 | Critical | + +> The full reinforcement schedule is **ASSUMED design-intent**; the accept criteria above become +> gates **only when the PE stamps the cage.** + +--- + +## 4. Anchor-bolt set — proof / pull test (NW-PF-004) + +**Anchors:** 4 × Ø19.05 mm [0.750 in] × 304.8 mm [12.000 in] ASTM F1554 Gr36, galvanized, cast per +ACI 318 Ch.17. + +| Characteristic | Criterion | Method | Class | +|---|---|---|---| +| Material / grade | ASTM F1554 Gr36 | mill cert / CoC | Critical | +| Galvanize | ASTM A123 coating-thickness record | magnetic gauge | Major | +| Projection | 50.0 mm [1.969 in] ± *(PE)* | scale | Major | +| Pattern (as-cast) | matches NW-PF-002 4 × Ø0.81 holes; template tol ± 0.02 mm | template / bolts drop through plate | Critical | +| Plumb | vertical within *(PE tol)* | level | Major | +| **Proof / pull test** | apply PE-specified proof load and hold; **no slip, no concrete cracking/breakout** | calibrated hydraulic ram + reaction frame *(protocol PE to confirm)* | **Critical** | + +**Proof-load basis (PE to set).** Tie the proof load to the governing tension demand. Bounding wind +case (`MECHANICAL_DESIGN.md` §7): roof gross uplift **9.2 kN**, net anchor demand **7.4 kN** across the +4-anchor group → **~1.85 kN/anchor service demand**; 4× 2 klbf [≈8.9 kN] capacity → **SF 3.9**. The +**field proof load per anchor is PE-specified** (commonly a factor of the service/design tension) and +**ASSUMED here pending PE**. Cast-in anchors are primarily accepted by **material cert + placement +inspection (H3)**; the proof/pull test is the **PE/owner-specified confirmation** requested for this +package. **Do not exceed a load that would damage the anchor or the young concrete — schedule after the +28-day strength is confirmed (H7).** + +> **PE coordination.** Per the isolation assumption, the **9.2 kN roof uplift is carried by the +> enclosure hold-downs (NW-RR-006, BP-04)**, not the pier; the pier anchors resist mount overturning + +> seismic. The proof-load target and the anchor split are **PE to confirm.** + +--- + +## 5. Plumb / level & dimensional (NW-PF-001, NW-PF-002) + +| Characteristic | Nominal | Accept limit | Method | Class | +|---|---|---|---|---| +| Pier plumb (verticality) | vertical | **≤ 1:200** (5 mm/m) | plumb bob / total station | Critical | +| Top-of-pier / plate level | level | **≤ 0.5°** | precision level / total station | Critical | +| Pier height above grade | 914.4 mm [36.000 in] | per ACI 117 *(PE tol)* | tape / survey | Major | +| Pier diameter | Ø304.8 mm [12.000 in] | per ACI 117 *(PE tol)* | tape | Minor | +| Top-plate flatness after grout | flat | plate bears fully on grout, no rock | feeler / straightedge | Major | + +--- + +## 6. Fabricated-plate acceptance (NW-PF-002, NW-PF-003) + +**Environment:** 20 °C per ISO 1 for the machined-plate checks. Datum/GD&T per ASME Y14.5-2018. + +### 6.1 NW-PF-002 — top plate (A36, galvanized) + +| Characteristic | Nominal (mm [in]) | Accept limit | Method | Class | +|---|---|---|---|---| +| Side (square) | 304.8 [12.000] | ISO 2768-mK | tape/CMM | Minor | +| Thickness | 9.5 [0.375] | ISO 2768-mK | caliper | Minor | +| Anchor holes | 4 × Ø20.6 [Ø0.81] | ± 0.25 mm pattern | CMM/gauge | Major | +| Adapter pattern | 4 × Ø11.2 [Ø0.44] | ± 0.25 mm pattern | CMM/gauge | Major | +| Galvanize coating | ASTM A123 | thickness per A123 table *(grade PE/spec)* | magnetic gauge | Major | +| Surface finish | Ra 3.2 µm [125 µin] | — | comparator | Minor | + +### 6.2 NW-PF-003 — adapter plate (6061-T6, anodized) + +| Characteristic | Nominal (mm [in]) | Accept limit | Method | Class | +|---|---|---|---|---| +| Side (square) | 254.0 [10.000] | ISO 2768-mK | caliper/CMM | Minor | +| Thickness | 19.1 [0.750] | ISO 2768-mK | caliper | Minor | +| RA-housing pattern | 4 × M6 | position *(ASSUMED — PE)* | CMM / thread gauge | Major | +| Pier-plate pattern | 4 × Ø11.2 [Ø0.44] | position *(ASSUMED)* | CMM/gauge | Major | +| Centre bore | Ø90.0 [3.543] | H8 pilot *(ASSUMED)* | CMM/plug | Major | +| Mounting-face flatness | ≤ 0.05 mm [0.0020 in] | CMM plane fit | CMM | Major | +| Latitude wedge angle | to 38.9° *(ASSUMED — bidder detail)* | angle gauge / CMM | Major | +| Anodize | MIL-A-8625F Type III, 0.002 in | thickness record | eddy-current gauge | Major | +| Surface finish | Ra 3.2 µm [125 µin] | — | profilometer | Minor | + +> The adapter geometry (patterns, centre bore, wedge angle) is **ASSUMED design-intent** — accept +> limits above are provisional pending PE release. + +--- + +## 7. Sampling & documentation + +- **Critical characteristics** (concrete strength, anchor proof/pull, plumb/level, reinforcement, + wedge/pattern): **100%** — the pier and each plate are qty 1, so first-article inspection = production + acceptance. +- **Major / Minor:** verified on the acceptance record to ISO 2768-mK (plates) / ACI 117 (concrete). + +The bidder shall deliver: + +1. **Concrete test reports** — mix design (ACI 301), batch tickets, 7-/28-day cylinder breaks, slump/air. +2. **Reinforcement** — ASTM A615 Gr60 mill certs + rebar-placement/cover record (H2). +3. **Anchors** — ASTM F1554 Gr36 cert/CoC, ASTM A123 galvanize cert, **anchor proof/pull record**. +4. **Plates** — A36 cert + A123 coating record (NW-PF-002); 6061-T6 cert + MIL-A-8625F anodize record + (NW-PF-003); dimensional/FAI. +5. **Plumb/level survey** (≤ 1:200; ≤ 0.5°). +6. **PE-stamped drawings + PE hold-point sign-offs** (H1–H7). +7. **NCR / RFI dispositions** for any ASSUMED value or out-of-tolerance condition. + +| Role | Name | Signature | Date | +|---|---|---|---| +| Bidder QA | | | | +| Testing lab (concrete) | | | | +| **Owner / PE (stamp + hold points)** | | | | + +--- + +## 8. ASSUMED-value register (do NOT treat as final) + +| # | ASSUMED value | Part(s) | +|---|---|---| +| 1 | Rebar schedule, cover (~76 mm), tie spacing, seismic hoops, splices | NW-PF-001 | +| 2 | Single-pour cylinder sampling frequency & single-element f'c acceptance basis | NW-PF-001 | +| 3 | Slump/air targets (per approved mix) | NW-PF-001 | +| 4 | Anchor sizing + **field proof/pull load** (tied to ~9.2 kN uplift bounding case) | NW-PF-004 | +| 5 | Pier-vs-enclosure anchor split (9.2 kN roof uplift → BP-04 hold-downs) | NW-PF-004 / NW-RR-006 | +| 6 | Site frost depth vs 914.4 mm embedment | NW-PF-001 | +| 7 | Wind 105 mph / seismic S_DS 0.5 g | all | +| 8 | Latitude wedge angle (38.9°) + adapter patterns/centre bore | NW-PF-003 | +| 9 | Grout/leveling-nut method + pier-slab isolation gap detail | NW-PF-001/002 | +| 10 | Concrete workmanship tolerance class (ACI 117) | NW-PF-001 | diff --git a/design/mechanical/tender/packages/BP-04_rolloff_roof/README.md b/design/mechanical/tender/packages/BP-04_rolloff_roof/README.md new file mode 100644 index 0000000..0522001 --- /dev/null +++ b/design/mechanical/tender/packages/BP-04_rolloff_roof/README.md @@ -0,0 +1,252 @@ +# BP-04 — Roll-off Roof "Disappearing Turret" (Structural Steel + Rail/Drive) — Bidder README + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | **BP-04 — Roll-off roof ("disappearing turret")** | +| Trade | Structural steel fabrication (welded, AWS D1.1) + rail/drive install | +| Parts | NW-RR-001, -002, -003, -004, -005, -006 (6 fabricated line items; 6 control drawings) | +| Branch / project | `design/mechanical-tender` — NIGHTWATCH Observatory Mount & Roll-off Tender | +| Issue date | 2026-08-06 | +| Revision | A | +| Governing TDP standard | MIL-STD-31000A (Technical Data Package) | + +This README is the entry point for the steel fabricator / gate-and-rail installer bidding BP-04. It +defines the **scope**, the **data package the shop receives**, the **bidder deliverables**, the +**PE-STAMP GATE**, and the items carried as **ASSUMED design-intent**. Read it with `SOW.md` (per-part +specification), `weld_map.md` (the joint schedule with AWS symbols), and `acceptance.md` (weld +visual/NDE, rail alignment, drive-cycle test, hold-down pull) in this same folder. + +--- + +## 0. PE-STAMP GATE — READ FIRST (this package is NOT releasable for construction) + +> **⚠ THE ROLL-OFF ROOF AND ITS SUPPORTING STEEL ARE A STRUCTURE. THEY ARE ISSUED FOR PE REVIEW ONLY.** +> **A licensed Professional Engineer, registered in the jurisdiction of the build site, MUST review, +> size, and STAMP every structural member (frame, purlins, rail beams, brackets), every weld size, and +> every hold-down/rail anchor BEFORE any steel is cut or any anchor is set.** Nothing in this package — +> member sizes, weld sizes, anchor sizing — is a released structural design. The controlling **section +> sizes are ASSUMED design-intent**; they are presented **for the PE to confirm, complete, and stamp** +> against the governing building code and ASCE 7 load cases. + +**Why a stamp is mandatory.** The building shell and its moving roof carry code-level loads. Two load +cases govern this package and are **owner/PE responsibilities under ASCE 7**: + +| Load case | Basis (design proof) | What it drives | Proof status | +|---|---|---|---| +| **Snow (structural)** | ~25 psf [1197 Pa] ground snow, high-desert @ ~1800 m; **closed flat roof carries 10.8 kN [2.42 klbf]** (≈6.1× the roof's own 1.8 kN [397 lbf] dead weight; ≈1.1 t of snow) | roof frame + purlin sizing, panel; **snow interlock** | §10 PASS + FINDING — **snow is the roof's design driver** | +| **Survival wind (105 mph [46.9 m/s])** | q = 1131 Pa; gross roof uplift **9.2 kN [2.06 klbf]** vs 1.8 kN self-weight → **SF 0.19 FAIL**; net anchor demand 7.4 kN [1.66 klbf] | **4× wind hold-down anchors** (NW-RR-006) | §7 FAIL → remediated: **4× ≥2 klbf → SF 3.9** | + +The wind basic speed (105 mph) and ground snow load (25 psf) are **ASSUMED** in the design study +because the repository was silent on roof structure — they are **exactly the values the PE must set +from the site's ASCE 7 hazard data and stamp.** See `MECHANICAL_DESIGN.md` §7 (wind — FAIL/remediated) +and §10 (roll-off drive + snow — PASS + finding). + +**No steel is cut, and no anchor is set, until the stamped structural drawings, the PE-approved +weld/NDE plan, and the PE-approved Inspection & Test Plan (`acceptance.md`) are in hand.** + +--- + +## 1. Scope + +BP-04 covers the **roll-off "disappearing turret" roof and its track/drive** — a rectangular welded +steel roof frame that rolls its **own full length clear of the aperture** on a **box-track rail run +twice the roof length**, driven by a COTS gate operator, held closed against survival wind by +**mandatory hold-down anchors**, and interlocked against wind and snow. The shop: + +1. Fabricates the **welded steel roof frame + purlins** (NW-RR-001, -002) per **AWS D1.1** (weld + symbols per **AWS A2.4**), sized to carry the closed-roof snow case. +2. Fabricates/installs the **box-track rail beams** (NW-RR-003), each **2× the roof length** so the + roof fully clears the aperture, with **end stops**. +3. Fabricates the **wheel axle brackets** (NW-RR-004) that carry the COTS V-groove wheels, the + **drive bracket + end stops** (NW-RR-005) that mount the COTS gate operator, and the **MANDATORY + 4× wind hold-down anchor brackets** (NW-RR-006). +4. **Hot-dip galvanizes** all steel per **ASTM A123** (after welding), weatherproofs the closed roof, + and commissions the drive with its **wind + snow interlocks**. +5. Inspects per `acceptance.md` (weld VT/NDE, rail alignment, drive-cycle test, hold-down pull) and + delivers a certified, PE-signed assembly. + +**Out of scope for BP-04** (separate packages — the roof *interfaces* to these but the shop does not +supply them, except where noted "install"): + +| Interface | Supplied under | Notes | +|---|---|---| +| **V-groove track wheels** (NW-CO-010, ×8) | BP-05 (COTS) | Shop **installs** them on the NW-RR-004 brackets. | +| **Roof drive — gate operator / rack** (NW-CO-011) | BP-05 (COTS) | Shop **installs + commissions** it on NW-RR-005; see §6 for the suggested product category. | +| **Metal roofing panel + flashing** (NW-CO-012) | BP-05 (COTS) | Weatherproof skin; shop installs + seals over the purlins. | +| Telescope pier NW-PF-001, anchor set NW-PF-004 | BP-03 | The pier is **structurally isolated** from the enclosure; roof uplift lands on the **building/enclosure foundation**, not the pier. See §4. | +| Mount head, optics, electronics | BP-01 / BP-02 / BP-05 | Not a roof interface. | +| Enclosure walls / building shell / enclosure foundation | Owner / site civil (coordinated, PE-stamped) | The roof rides on this shell; the hold-down anchors land in it. | + +--- + +## 2. Data package the shop receives (per MIL-STD-31000A) + +| Item | Format / standard | Role | +|---|---|---| +| 3-D solid master (frame + brackets) | **STEP ISO 10303 AP242** | As-modelled geometry master (weldment + brackets). | +| Flat-pattern cut files (plate brackets NW-RR-004/-005/-006) | **DXF / DWG** | Laser/waterjet flat patterns. | +| Control drawings NW-RR-001…-006 | **PDF** (drawing practices ASME Y14.100; drawing types Y14.24; sheet & title block Y14.1) | Envelope + title block + key dims; bid control drawing. | +| **PE-stamped structural drawings** | PDF (issued by owner's PE) | **Governs for construction** — supersedes the ASSUMED member/weld/anchor values herein. | +| `SOW.md` | Markdown | Per-part spec: members, welds (AWS D1.1/A2.4), rail run, drive, hold-downs, galvanize, weatherproofing. | +| `weld_map.md` | Markdown | Joint schedule: joint type, AWS A2.4 weld symbol, ASSUMED size, process, NDE. | +| `acceptance.md` | Markdown | Weld VT/NDE, rail alignment, drive-cycle test, hold-down pull, weatherproofing. | +| `cut_list.csv` (rows NW-RR-001…-006) | CSV/XLSX | Stock size, cut allowance, stock mass. | + +**Order of precedence.** For the **fabricated brackets** (NW-RR-004/-005/-006) the **STEP AP242 solid +governs geometry**; the DXF/DWG governs flat patterns; the drawing + SOW govern tolerances, finish, and +material. **For the structural steel (frame, purlins, rail beams), the member sizing, the weld sizing, +and the anchorage, the PE-STAMPED STRUCTURAL DRAWINGS GOVERN and supersede every number in this +package.** Where any document disagrees, the **PE-stamped set controls** and the bidder shall raise an +RFI before cutting steel. Welding, weld procedures (WPS/PQR) and welder qualification (WPQ) per +**AWS D1.1**; weld & NDE symbols per **AWS A2.4**; general tolerances per **ISO 2768**; bracket GD&T per +**ASME Y14.5-2018**; fits per **ISO 286**. + +--- + +## 3. Line-item summary (source: `partspec.parts_for('BP-04')` + `cut_list.csv`) + +| Part no. | Name | Material | Stock / size | Qty | Stock mass | Finish | Drawing | +|---|---|---|---|---:|---:|---|---| +| **NW-RR-001** | Roof frame perimeter (HSS) | ASTM A500 Gr B HSS | HSS **50.8 mm [2.000 in] sq × 3.2 mm [0.125 in] wall × 12000.0 mm [472.441 in]** total (4 sides of the 3.0 m [9.843 ft] square perimeter) | 1 | 56.98 kg | Galvanize ASTM A123 | NW-RR-001 | +| **NW-RR-002** | Roof rafters / purlins | ASTM A500 Gr B HSS | HSS **38.1 mm [1.500 in] sq × 3.2 mm [0.125 in] wall × 15000.0 mm [590.551 in]** total (5 purlins × 3000 mm [118.110 in] span @ 600 mm [23.622 in] o.c.) | 5 | 261.14 kg | Galvanize ASTM A123 | NW-RR-002 | +| **NW-RR-003** | Track rail beams (box track) | ASTM A36 structural steel | Box track **63.5 mm [2.500 in] sq × 4.0 mm [0.157 in] wall × 6000.0 mm [236.220 in] each** (2 rails; run = **2× roof length**) | 2 | 179.36 kg | Galvanize ASTM A123 | NW-RR-003 | +| **NW-RR-004** | Wheel axle brackets | ASTM A36 structural steel | Plate **101.6 mm [4.000 in] × 76.2 mm [3.000 in] × 9.5 mm [0.375 in]** | 8 | 4.63 kg | Galvanize ASTM A123 | NW-RR-004 | +| **NW-RR-005** | Drive bracket + end stops | ASTM A36 structural steel | Plate/angle assembly **150.0 mm [5.906 in] × 100.0 mm [3.937 in] × 9.5 mm [0.375 in]** | 4 | 4.49 kg | Galvanize ASTM A123 | NW-RR-005 | +| **NW-RR-006** | **Wind hold-down anchor brackets (REMEDIATION — MANDATORY)** | ASTM A36 structural steel | Plate **127.0 mm [5.000 in] × 101.6 mm [4.000 in] × 12.7 mm [0.500 in]** | 4 | 5.15 kg | Galvanize ASTM A123 | NW-RR-006 | + +Stock mass is the **quantity ordered** (stock envelope volume × steel density), not finished net mass; +cut allowance is 3.0 mm on stock. Rail run per rail is **6000.0 mm [236.220 in] = 2 × the 3000.0 mm +[118.110 in] roof length**, so the roof travels its full length off the aperture. Total roof assembly +mass basis for the drive/wheel sizing is **180 kg [397 lb]** (`P.ENCLOSURE.roof_mass_kg`, ASSUMED). + +> **Member-callout reconciliation (ASSUMED).** On NW-RR-002 the nominal member designation in the +> registry key-dims (**"HSS 2×1×1/8"**, a rectangular section) differs from the modelled/ordered stock +> envelope (**38.1 mm [1.500 in] square × 3.2 mm wall**). Both are **ASSUMED design-intent**; the PE +> shall reconcile the purlin section against the ASCE 7 snow demand and issue the released size. + +--- + +## 4. Cross-package interfaces the shop must preserve + +| Interface | Feature | Mates to | Part | +|---|---|---|---| +| Roof → rail | 8 × V-groove wheels on NW-RR-004 brackets | Box-track rail running surface | NW-CO-010 (BP-05) → NW-RR-003 | +| Roof → drive | Drive bracket + rack/pinion pickup | Gate operator + gear rack | NW-CO-011 (BP-05) → NW-RR-005 | +| Roof → weather | Purlin top flanges | Metal roofing panel + flashing | NW-CO-012 (BP-05) → NW-RR-002 | +| Rail → structure | Rail-beam anchorage | Enclosure/building foundation (PE-designed) | NW-RR-003 → owner civil | +| Roof/rail → structure | **4× hold-down anchor brackets** | Ø0.75 in [19.05 mm] F1554 anchors into the enclosure foundation | NW-RR-006 → owner civil | +| End of travel | 4× end stops + rubber bump pads | Rail ends + drive limit | NW-RR-005 / NW-RR-003 | + +> **Wind hold-down clarification (important).** The **9.2 kN [2.06 klbf] survival-wind roof uplift** is +> resisted by the **4× wind hold-down anchor brackets NW-RR-006 (this package)**, which anchor the +> **enclosure/building foundation** — because the telescope pier (BP-03) is **structurally isolated** +> from the enclosure (standard observatory practice, to keep vibration off the optics). The BP-03 +> anchor set NW-PF-004 casts into the **pier** and resists the mount overturning + seismic demand, a +> **separate** load path. **Where each hold-down lands, the anchor edge distance/embedment into the +> building foundation, and the final anchor capacity are PE coordination items — anchorage into +> concrete per ACI 318 Ch.17; PE to confirm.** + +--- + +## 5. Bidder deliverables + +1. Finished, galvanized, weatherproofed assembly: frame (NW-RR-001), purlins (NW-RR-002 ×5), rail + beams (NW-RR-003 ×2), wheel brackets (NW-RR-004 ×8), drive bracket + stops (NW-RR-005 ×4), and the + **4× wind hold-down brackets (NW-RR-006)** — installed, aligned, and commissioned. +2. **Material / mill certs** — ASTM A500 Gr B (HSS) and ASTM A36 (plate/rail), heat/lot traceable. +3. **Welding submittals** — WPS + supporting PQR and welder qualification records (WPQ) per **AWS D1.1**; + weld map (`weld_map.md`) marked up with as-built weld sizes; **CWI** weld inspection report. +4. **NDE reports** — visual (VT) 100% + magnetic-particle (MT)/dye-penetrant (PT) on the critical + hold-down, wheel-bracket, and rail-splice welds per `acceptance.md` (extent **ASSUMED — PE/CWI to set**). +5. **Galvanize cert** — ASTM A123 coating mass/thickness record; vent/drain-hole detail for the HSS; + field-repair procedure for damaged galvanize (bidder-proposed; the recognized repair standard is + **ASTM A780**, supplementary to the register). +6. **Rail-alignment survey** — gauge, straightness, level/co-planarity, splice flushness (`acceptance.md`). +7. **Drive-cycle + interlock commissioning report** — full open/close travel within timeout, tractive + load/current, soft-start/stop, end-stop engagement, manual release, and the **wind + snow interlocks**. +8. **Hold-down pull-test record** — each anchor proofed to the PE-set load (≥2 klbf [8.9 kN] baseline). +9. **Weatherproofing test** — roof-closed water/rain test, flashing/gasket, drainage. +10. **PE sign-off** — stamped structural drawings + field hold-point sign-offs (fit-up, weld, anchor set). +11. RFI / nonconformance log resolving every ASSUMED value before it is built. + +--- + +## 6. Suggested COTS product categories (procured under BP-05 — named, not specified) + +The task's two long-lead COTS items map to well-established commercial product categories. These are +**suggested categories to guide the BP-05 buy**, not a released product spec — final selection and +sizing are the bidder's/PE's, confirmed against the loads below. + +- **Track rail + wheels → sliding-gate "V-groove box track" hardware (steel).** The **box-track rail + NW-RR-003** (2.5 in [63.5 mm] class) and the **8× V-groove wheels NW-CO-010** correspond to the + **steel V-groove gate-track + V-groove wheel-with-preattached-steel-box** category used on slide + gates and DIY roll-off observatories. Steel 4 in [101.6 mm] V-groove wheels are commonly rated to + ~3,000 lb [~13 kN] each; with **8 wheels for the 180 kg [397 lb] roof + snow**, capacity is not the + constraint — **alignment and straightness are** (see `acceptance.md`). Size wheels for **≥4×** the + wheel share of roof + snow (per NW-RR-004 note). +- **Roof drive → rack-and-pinion "slide-gate operator" (light-commercial class).** The **roof drive + NW-CO-011** corresponds to a **rack-and-pinion sliding-gate operator**. The proof requires only + **235 N [52.8 lbf]** tractive force (rolling 88 N [19.8 lbf] + 35 mph [15.6 m/s] wind 147 N + [33.1 lbf]) → **SF 2.1 vs a ~500 N [112 lbf] garage-door-class drive**; light-commercial slide-gate + operators rated for gate weights of ~270–500 kg [~600–1,100 lb] give ample margin **and** provide the + needed **soft-start/soft-stop, manual release, and photo-beam/limit interlocks**. Because **wind is + 62% of the tractive load, the drive is WIND-sized** — spec a **positive, fail-safe close against the + 35 mph gust**. + +> **Sizing anchors (from the proofs, for the BP-05 buyer).** Move force 235 N [52.8 lbf]; drive torque +> 11.77 Nm [8.68 lbf·ft] @ 50 mm [1.969 in] wheel; roof speed 0.067 m/s (3 m in 45 s) < 0.30 m/s +> ceiling; open time 45 s < 60 s motor timeout. **Snow interlock is MANDATORY:** a fully snow-laden +> roof needs **627 N [141 lbf]** to roll — it **exceeds** the ~500 N drive, so the roof must **never** +> be commanded open under snow load (`MECHANICAL_DESIGN.md` §10). + +Sources for the named categories (BP-05 to confirm): [Steel Supply LP — V-groove track & wheels](https://www.steelsupplylp.com/ornamental-iron/store/gate-door-hardware/v-groove-tracks-wheels-boxes), +[Hoover Fence — V-groove wheel w/ preattached steel box](https://www.hooverfence.com/v-groove-wheel-with-preattached-steel-box), +[Cloudy Nights — roll-off roof roller/rail systems](https://www.cloudynights.com/topic/812178-recommend-a-low-friction-rollerrail-system-for-8x10/), +[FAAC 844 ER rack-and-pinion slide-gate operator](https://www.affordableopeners.com/faac-usa-844-er-rack-and-pinion-slide-gate-operator.html), +[LiftMaster rack-and-pinion slide-gate operator](https://www.asapgaragedoorandgaterepair.com/portfolio/liftmaster-residential-rack-pinion-drive-slide-gate-operator/). + +--- + +## 7. ASSUMED design-intent — bidder / PE to confirm (NOT final) + +Carried as **ASSUMED design-intent** in the parts registry; **not** released dimensions. The PE shall +confirm/complete and stamp before construction: + +- **All structural member sizes** — frame HSS 2×2×1/8 (NW-RR-001), purlin HSS (NW-RR-002, incl. the + 2×1 vs 1.5-sq reconciliation of §3), box-track section 2.5 in class (NW-RR-003), and bracket + thicknesses — are **ASSUMED design-intent; PE to confirm vs the ASCE 7 snow (10.8 kN [2.42 klbf] + closed-roof) + survival-wind cases.** +- **All weld sizes and joint types** — every weld leg/throat and CJP/PJP call in `weld_map.md` is + **ASSUMED; PE/CWI to size per AWS D1.1 minimum-fillet tables and the computed demand.** +- **Hold-down anchor sizing** — 4× Ø0.75 in [19.05 mm] F1554, capacity ≥2 klbf [8.9 kN] each, is the + **REMEDIATION** basis (survival uplift 9.2 kN → SF 3.9). Final diameter, grade, embedment, and edge + distance into the enclosure foundation are **PE to set per ACI 318 Ch.17.** +- **Rail support / anchorage spacing** and the rail-to-foundation connection (weld vs bolt) — ASSUMED; + PE to detail for the roof + snow + wind reactions. +- **Roof geometry (3.0 × 3.0 m [9.843 × 9.843 ft], 180 kg [397 lb])** — ASSUMED footprint/mass + (`P.ENCLOSURE`, flagged); confirm against the released enclosure. +- **Snow interlock and wind interlock logic** — MANDATORY per §10/§7; interlock thresholds + (25 mph [11.2 m/s] park, 35 mph [15.6 m/s] close, snow hold-closed) SOURCED, but the sensor/logic + implementation is ASSUMED — bidder/PE to detail. +- **Surface finish / GD&T values** on the brackets — ASSUMED; PE to set. +- **Weatherproofing detail** (panel lap, flashing, gasket, drainage) — ASSUMED; bidder to detail with + the NW-CO-012 panel. + +--- + +## 8. References + +- Parts registry: `design/mechanical/tender/gen/partspec.py` → `parts_for('BP-04')` (single source of truth). +- BOM / cut list / COTS: `design/mechanical/tender/bom/{master_bom,cut_list,cots_schedule}.csv`. +- Control drawings: `design/mechanical/tender/drawings/NW-RR-00{1..6}.svg`. +- Design basis: `design/mechanical/MECHANICAL_DESIGN.md` §7 (wind — FAIL/remediated, hold-downs), + §10 (roll-off drive + snow — PASS + snow-interlock finding), §9 (selected config + the 5 remediations); + `design/mechanical/tradestudy/SECTION.md` (enclosure = roll-off + active thermal; anchors = 4× 2 klbf). +- Standards register: MIL-STD-31000A; **AWS D1.1** (structural steel welding, WPS/PQR/WPQ), **AWS A2.4** + (weld & NDE symbols); ASTM A500 Gr B (HSS), ASTM A36 (steel), ASTM A123 (hot-dip galvanize), + ASTM F1554 Gr36 (anchor bolts), ASTM A574 (SHCS); ACI 318 Ch.17 (anchoring), ASCE 7 (wind/snow/seismic); + ASME Y14.5-2018 / Y14.100 / Y14.24 / Y14.1; ISO 286; ISO 2768; STEP ISO 10303 AP242; DXF/DWG. + + diff --git a/design/mechanical/tender/packages/BP-04_rolloff_roof/SOW.md b/design/mechanical/tender/packages/BP-04_rolloff_roof/SOW.md new file mode 100644 index 0000000..96628a6 --- /dev/null +++ b/design/mechanical/tender/packages/BP-04_rolloff_roof/SOW.md @@ -0,0 +1,263 @@ +# BP-04 — Statement of Work: Roll-off Roof "Disappearing Turret" (Structural Steel + Rail/Drive) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-04 — Roll-off roof ("disappearing turret") | +| Parts | NW-RR-001 (frame), -002 (purlins), -003 (rail beams), -004 (wheel brackets), -005 (drive bracket + stops), -006 (wind hold-down brackets) | +| Process | Structural steel cut + weld (AWS D1.1); laser-cut plate brackets; rail/drive install | +| Material | ASTM A500 Gr B HSS (frame/purlins); ASTM A36 (rail beams, brackets) | +| Finish | **Hot-dip galvanize per ASTM A123** (after fabrication) | +| Issue date / rev | 2026-08-06 / A | + +**Purpose.** This SOW specifies the fabrication, welding, tolerancing, finishing, rail/drive +installation, and commissioning requirements for the roll-off roof and its track/drive. It is read with +the STEP ISO 10303 AP242 solid master (weldment/bracket geometry authority), the DXF/DWG flat patterns +(plate brackets), the NW-RR control drawings (PDF), `weld_map.md` (joint schedule), and `acceptance.md`. +All dimensions are dual-unit, **mm primary, inch in brackets**, drawn from the parts registry +(`partspec.parts_for('BP-04')`) and `cut_list.csv` — **no dimension herein is invented.** + +**Standards applied.** MIL-STD-31000A (TDP); **AWS D1.1** (structural steel welding, incl. WPS/PQR and +welder qualification WPQ); **AWS A2.4** (weld & NDE symbols); **ASTM A500 Gr B** (HSS), **ASTM A36** +(steel), **ASTM A123** (hot-dip galvanize), **ASTM F1554 Gr36** (anchor bolts), **ASTM A574** (SHCS); +**ACI 318 Ch.17** (anchorage into concrete/foundation); **ASCE 7** (wind/snow/seismic loads); +**ASME Y14.5-2018** (bracket GD&T), Y14.100/Y14.24/Y14.1 (drawing practices); **ISO 286** (fits); +**ISO 2768** (general tolerances). + +> **PE-STAMP GATE (see README §0).** The roof and its supporting steel are a **structure**. Member +> sizes, weld sizes, and anchorage in this SOW are **ASSUMED design-intent** and are **not** releasable +> for construction until a licensed PE confirms them against the **ASCE 7 snow (10.8 kN [2.42 klbf] +> closed-roof) and survival-wind (9.2 kN [2.06 klbf] uplift)** cases and **stamps** the drawings. + +--- + +## 1. General requirements (all parts) + +1. **Material & certification.** HSS to **ASTM A500 Gr B**; plate and rail to **ASTM A36**. Furnish + mill certification / CoC with **heat/lot traceability**; mark each member with part number and + revision per ASME Y14.100. +2. **General tolerance.** **ISO 2768-mK** on plate features; frame/rail geometric tolerances per the + per-part tables (§2–§7). Break sharp edges; no burrs on plate brackets. +3. **Welding.** All welds per **AWS D1.1**. The bidder shall submit **WPS** supported by **PQR**, and + **welder qualification records (WPQ)** for each process/position, **before** production welding. + Prequalified joints per AWS D1.1 are acceptable where applicable; all others require a qualified WPS. + Weld & NDE symbols on the drawings and in `weld_map.md` are interpreted per **AWS A2.4**. +4. **Weld sizes are ASSUMED design-intent.** Every weld leg/throat and every CJP/PJP designation in + `weld_map.md` is **ASSUMED — PE/CWI to size** per the AWS D1.1 minimum-fillet-weld tables (governed + by the thinner part joined) and the PE's computed member forces. **Do not treat any weld size herein + as released.** +5. **GD&T.** Bracket geometric callouts per **ASME Y14.5-2018**; hole-pattern position per the drawing + (bracket hole patterns held to **0.25 mm [0.010 in]**, `cut_list`/drawing). +6. **Fabrication sequence.** Weld → dimensional/weld inspection → **hot-dip galvanize (ASTM A123, after + all welding)** → install wheels/drive → align → commission. Provide **vent and drain holes** on all + closed HSS sections for the galvanizing bath (locations **ASSUMED — bidder to detail**, must not + fall on a stiffness-critical face). +7. **Finish.** **Hot-dip galvanize per ASTM A123** on all steel after fabrication. Field-repair of + galvanize damaged by field welding/handling per the **bidder's approved procedure** (recognized + repair standard **ASTM A780**, supplementary to the register). Coating mass/thickness recorded. +8. **Fasteners.** Structural bolting and bracket fasteners per **ASTM A574** (SHCS) where used; anchors + per **ASTM F1554 Gr36** (see §7). Fastener schedule consolidated under BP-05 (NW-CO-014). + +--- + +## 2. NW-RR-001 — Roof frame perimeter (HSS) + +**Drawing:** NW-RR-001 (rev A). **Stock:** HSS **50.8 mm [2.000 in] sq × 3.2 mm [0.125 in] wall**, +total **12000.0 mm [472.441 in]** (four sides of the **3000.0 mm [118.110 in] × 3000.0 mm [118.110 in]** +square perimeter), cut allowance 3.0 mm, stock mass 56.98 kg (`cut_list.csv`). **Process:** cut + weld +(AWS D1.1). **Tolerance:** frame square **3 mm/m**, diagonal **5 mm [0.197 in]** across the frame. + +| Feature | Value (mm [in]) | Spec | +|---|---|---| +| Perimeter span × length | 3000.0 [118.110] × 3000.0 [118.110] | ASSUMED footprint (`P.ENCLOSURE`) | +| Member | HSS 2×2×1/8 (50.8 sq × 3.2 wall) | **ASSUMED design-intent** — PE to size vs snow/wind | +| Corner joints | fully welded, **gusseted** | AWS D1.1; size in `weld_map.md` (ASSUMED) | +| Frame square / diagonal | 3 mm/m; diagonal 5 mm [0.197 in] | acceptance datum | + +**Function.** The frame carries the purlins + roofing panel and transfers the **closed-roof snow load +(10.8 kN [2.42 klbf])** and **survival-wind uplift (9.2 kN [2.06 klbf])** into the wheels/hold-downs. +Corner joints are **fully welded and gusseted**. **Member size ASSUMED — PE to confirm vs ASCE 7.** + +**Provenance / flags.** Roof span/length = P.ENCLOSURE (ASSUMED footprint); member size ASSUMED +design-intent; corner-weld size ASSUMED (`weld_map.md` J1). + +--- + +## 3. NW-RR-002 — Roof rafters / purlins + +**Drawing:** NW-RR-002 (rev A). **Stock:** HSS **38.1 mm [1.500 in] sq × 3.2 mm [0.125 in] wall**, +total **15000.0 mm [590.551 in]** (**5 purlins × 3000.0 mm [118.110 in]** span), cut allowance 3.0 mm, +stock mass 52.23 kg (`cut_list.csv`). **Qty 5**, spaced **600.0 mm [23.622 in] o.c.** **Process:** +cut + weld to the frame (AWS D1.1). **Finish:** galvanize ASTM A123. + +| Feature | Value (mm [in]) | Spec | +|---|---|---| +| Span (each) | 3000.0 [118.110] | ASSUMED (`P.ENCLOSURE`) | +| Spacing | 600.0 [23.622] o.c. | ASSUMED design-intent | +| Count | 5 | ASSUMED design-intent | +| Member | HSS 2×1×1/8 **or** 38.1 sq × 3.2 wall | **ASSUMED — reconcile (see below)** | +| Purlin-to-frame joint | fillet both sides (or bolted clip) | AWS D1.1; `weld_map.md` J2 (ASSUMED) | + +> **Member-callout reconciliation (ASSUMED design-intent).** The registry names the member **"HSS +> 2×1×1/8"** (rectangular) while the modelled/ordered stock is **38.1 mm [1.500 in] square × 3.2 mm +> wall**. Both are carried as ASSUMED; **the PE shall reconcile the purlin section** against the ASCE 7 +> **snow case (10.8 kN [2.42 klbf] over the 9 m² [96.9 ft²] roof, ≈6.1× the roof dead weight)**, which +> is the **governing design driver** for the purlins and panel (`MECHANICAL_DESIGN.md` §10), and issue +> the released size. + +**Function.** Purlins carry the **metal roofing panel (NW-CO-012, BP-05)** and the snow load. A +**sloped/pitched, slippery metal roof sheds most snow** (ASCE 7 slope factor Cs → ~0 for a steep metal +roof); a **flat** roll-off roof must carry the full load and needs the **snow interlock** (§6). + +--- + +## 4. NW-RR-003 — Track rail beams (box track) + +**Drawing:** NW-RR-003 (rev A). **Stock:** box track **63.5 mm [2.500 in] sq × 4.0 mm [0.157 in] +wall**, **6000.0 mm [236.220 in] each**, **2 rails**, cut allowance 3.0 mm, stock mass 89.68 kg +(`cut_list.csv`). **Process:** cut + weld / bolt to the rail supports. **Tolerance:** rail straightness +**3 mm [0.118 in] over the run**; gauge **2 mm [0.079 in]**. + +| Feature | Value (mm [in]) | Spec | +|---|---|---| +| Rail length (each) | 6000.0 [236.220] = **2× roof length** | so the roof **fully clears** the aperture | +| Gauge (rail spacing) | 3000.0 [118.110] | = roof span; hold to ±2 mm [0.079 in] | +| Section | box track, 2.5 in [63.5 mm] class | **ASSUMED**; brand/section = COTS (BP-05) | +| Straightness | 3 mm [0.118 in] over run | acceptance datum | +| End stops | required, both ends, both rails | part of NW-RR-005 | + +**Function.** The **"disappearing turret" travels its own full length off the building** — hence the +**rail run = 2× the roof length (6000 mm [236.220 in])**. Each rail carries the roof + snow reaction on +the V-groove wheels. **Splices** (if the rail is furnished in shorter lengths) shall be **CJP groove +butt welds with backing, ground flush on the running surface** (`weld_map.md` J6, size/NDE ASSUMED). +Rail brand/section is a **suggested COTS category** (steel V-groove box track, README §6); the shop may +furnish proprietary track in lieu of fabricated box track **subject to PE approval**. **End stops are +MANDATORY** at both ends of both rails. + +--- + +## 5. NW-RR-004 — Wheel axle brackets + +**Drawing:** NW-RR-004 (rev A). **Stock:** plate **101.6 mm [4.000 in] × 76.2 mm [3.000 in] × 9.5 mm +[0.375 in]**, **qty 8**, cut allowance 3.0 mm, stock mass 4.63 kg. **Process:** laser cut + weld. +**Tolerance:** hole pattern **0.25 mm [0.010 in]**. + +| Feature | Value | Spec | +|---|---|---| +| Count | 8 (4 per side) | ASSUMED design-intent | +| Wheel bore | for **Ø101.6 mm [4.000 in] V-groove wheel** (COTS, NW-CO-010) | shop installs the wheel | +| Axle | **M16** | ASSUMED; bidder/PE to confirm shear/bending | +| Bracket-to-frame weld | fillet both sides, gusseted | AWS D1.1; `weld_map.md` J3 (ASSUMED, CRITICAL) | + +**Function.** The brackets carry the COTS **V-groove wheels (NW-CO-010, BP-05)** and transmit the roof +weight into the rails. **Size the wheels for ≥4× the wheel share of the roof (180 kg [397 lb]) + snow.** +4 in [101.6 mm] steel V-groove wheels are commonly rated ~3,000 lb [~13 kN] each, so **alignment and +weld integrity — not wheel capacity — govern** (`acceptance.md`). Bracket-to-frame welds are +**load-critical** and receive VT 100% + MT/PT (extent ASSUMED, §`acceptance.md`). + +--- + +## 6. NW-RR-005 — Drive bracket + end stops (and the drive/interlock scope) + +**Drawing:** NW-RR-005 (rev A). **Stock:** plate/angle **150.0 mm [5.906 in] × 100.0 mm [3.937 in] × +9.5 mm [0.375 in]**, **qty 4**, cut allowance 3.0 mm, stock mass 4.49 kg. **Process:** laser cut + weld. + +| Feature | Value | Spec | +|---|---|---| +| Drive | **rack-and-pinion / gate operator** (COTS, NW-CO-011) | shop installs + commissions | +| End stops | **4** + rubber bump pads | hard limits, both ends both rails | +| Drive-bracket weld | fillet, gusseted | AWS D1.1; `weld_map.md` J4 (ASSUMED) | + +**Drive sizing (from the proof — `MECHANICAL_DESIGN.md` §10).** The drive moves the **180 kg [397 lb]** +roof against rolling **88 N [19.8 lbf]** + **35 mph [15.6 m/s]** wind **147 N [33.1 lbf]** = **235 N +[52.8 lbf]** tractive force (**11.77 Nm [8.68 lbf·ft]** @ 50 mm [1.969 in] wheel) → **SF 2.1 vs a +~500 N [112 lbf] garage-door-class drive.** Because **wind is 62% of the tractive load, the drive is +WIND-sized** — specify a **positive, fail-safe close against the gust**. Suggested COTS category: +**light-commercial rack-and-pinion slide-gate operator** (README §6). + +**Interlocks (MANDATORY — REMEDIATION/finding).** +- **Wind interlock:** park at **25 mph [11.2 m/s]** (roof stays open only below this), emergency close + at **35 mph [15.6 m/s]** gust; open time 45 s < 60 s motor timeout. +- **Snow interlock (MANDATORY):** a fully snow-laden roof needs **627 N [141 lbf]** to roll — which + **exceeds the ~500 N drive** — so the roof **must never be commanded open under snow load**. The + safety monitor shall **hold the roof CLOSED while snow-loaded** (`MECHANICAL_DESIGN.md` §10). A + **pitched shedding roof** is the design alternative. +- Provide **soft-start/soft-stop, manual release, photo-beam/limit safety,** and interlock with the + building/telescope safety monitor. Interlock sensor/logic detail is **ASSUMED — bidder/PE to detail.** + +**Control power.** The drive + interlocks draw from the site power system — **grid + UPS (v1)** or the +**48 V LiFePO4 pack (v2, NW-CO-013, REMEDIATION of the power/autonomy FAIL)**; power source is a BP-05 +coordination item. + +--- + +## 7. NW-RR-006 — Wind hold-down anchor brackets (REMEDIATION — MANDATORY) + +**Drawing:** NW-RR-006 (rev A). **Stock:** plate **127.0 mm [5.000 in] × 101.6 mm [4.000 in] × 12.7 mm +[0.500 in]**, **qty 4**, cut allowance 3.0 mm, stock mass 5.15 kg. **Process:** laser cut + weld. +**Tolerance:** hole pattern **0.25 mm [0.010 in]**. + +| Feature | Value | Spec | +|---|---|---| +| Count | **4** | REMEDIATION basis | +| Anchor | **Ø0.75 in [19.05 mm] F1554 Gr36** (to enclosure foundation) | ACI 318 Ch.17; PE to size | +| Capacity (each) | **≥2 klbf [8.9 kN]** | proof-test target | +| Bracket weld | CJP or heavy fillet, gusseted | AWS D1.1; `weld_map.md` J5 (ASSUMED, **CRITICAL**) | + +> **REMEDIATION — MANDATORY (do not value-engineer out).** The survival-wind proof (`MECHANICAL_DESIGN.md` +> §7) found **gross roof uplift 9.2 kN [2.06 klbf]** vs **1.8 kN [397 lbf]** self-weight → **SF 0.19 — +> FAIL.** Net anchor demand is **7.4 kN [1.66 klbf]**. **Four hold-down brackets at ≥2 klbf [8.9 kN] +> each (35.6 kN [8 klbf] total) restore SF 3.9.** These brackets **clamp the roof/building against the +> 105 mph [46.9 m/s] survival uplift and are interlocked with the drive** (the roof cannot be driven +> while a hold-down is engaged, and must re-engage on close). + +**Anchorage.** The brackets anchor into the **enclosure/building foundation** (the telescope pier is +isolated — README §4). **Final anchor diameter, grade, embedment, and edge distance are PE to set per +ACI 318 Ch.17**; each anchor is **proof-pull-tested** per `acceptance.md`. + +--- + +## 8. Weatherproofing + +- Install the COTS **metal roofing panel + flashing (NW-CO-012, BP-05)** over the purlins: lapped/sealed + panel, **ridge and edge flashing, and perimeter gaskets** at the roof-to-wall closure when shut. +- Provide **drainage/weeps** so the closed roof sheds water; detail the **wall-top weather seal** that + the roof lands on. Panel lap, flashing, gasket, and drainage details are **ASSUMED — bidder to detail** + with the panel supplier; verify by the **roof-closed water test** (`acceptance.md`). +- A **pitched** panel is preferred (sheds snow per §3/§6); a flat panel triggers the snow interlock. + +--- + +## 9. Submittals & deliverables + +1. Finished, galvanized, weatherproofed, commissioned assembly (all six NW-RR parts). +2. Material / mill certs (A500 Gr B, A36) with heat/lot traceability. +3. **WPS + PQR + welder qualification (WPQ)** per AWS D1.1; as-built `weld_map.md`; **CWI** weld report. +4. **NDE reports** (VT 100% + MT/PT on critical welds; extent ASSUMED per `acceptance.md`). +5. **Galvanize cert** (ASTM A123 coating mass/thickness) + vent/drain detail + galv-repair procedure. +6. **Rail-alignment survey** (gauge, straightness, level, splice flushness). +7. **Drive-cycle + interlock commissioning report** (travel, tractive load/current, wind + snow interlocks). +8. **Hold-down pull-test record** (each anchor to the PE-set proof load). +9. **Weatherproofing (water) test** record. +10. **PE sign-off** — stamped structural drawings + field hold-point sign-offs. +11. RFI / nonconformance log resolving every ASSUMED value before it is built. + +--- + +## 10. ASSUMED design-intent register (this SOW) + +| # | ASSUMED value | Part(s) | Status | +|---|---|---|---| +| 1 | Frame HSS 2×2×1/8 member size | NW-RR-001 | ASSUMED — PE to size vs ASCE 7 snow/wind | +| 2 | Purlin section (HSS 2×1×1/8 vs 38.1 sq), spacing 600 mm, count 5 | NW-RR-002 | ASSUMED — PE to reconcile & size (snow-governed) | +| 3 | Box-track section (2.5 in class), rail supports/anchorage spacing | NW-RR-003 | ASSUMED — PE to detail; COTS track alt allowed | +| 4 | All weld sizes & joint types (CJP/PJP/fillet legs) | all | ASSUMED — PE/CWI to size per AWS D1.1 | +| 5 | Wheel axle M16; wheel bore/rating | NW-RR-004 | ASSUMED — size wheels ≥4× wheel share | +| 6 | Drive product + interlock sensor/logic | NW-RR-005 | ASSUMED — light-commercial rack-and-pinion (README §6) | +| 7 | Hold-down anchor Ø0.75 F1554, ≥2 klbf, embedment/edge | NW-RR-006 | ASSUMED REMEDIATION — PE per ACI 318 Ch.17 | +| 8 | Roof geometry 3.0×3.0 m / 180 kg | all | ASSUMED footprint (`P.ENCLOSURE`) | +| 9 | Galvanize vent/drain holes; galv field-repair (A780) | all | ASSUMED — bidder to detail | +| 10 | Weatherproofing detail (panel lap, flashing, gasket, drainage) | roof | ASSUMED — bidder to detail | +| 11 | Bracket GD&T / surface-finish values | NW-RR-004/-005/-006 | ASSUMED — PE to set | + diff --git a/design/mechanical/tender/packages/BP-04_rolloff_roof/acceptance.md b/design/mechanical/tender/packages/BP-04_rolloff_roof/acceptance.md new file mode 100644 index 0000000..e046d18 --- /dev/null +++ b/design/mechanical/tender/packages/BP-04_rolloff_roof/acceptance.md @@ -0,0 +1,179 @@ +# BP-04 — Acceptance & Inspection Plan: Roll-off Roof (Structural Steel + Rail/Drive) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-04 — Roll-off roof ("disappearing turret") | +| Parts | NW-RR-001, -002, -003, -004, -005, -006 | +| Method | Weld VT/NDE (AWS D1.1) · frame & rail dimensional survey · drive-cycle + interlock test · hold-down proof-pull · roof-closed water test | +| Issue date / rev | 2026-08-06 / A | + +**Scope.** This plan defines the inspection methods and accept/reject criteria for BP-04. Welds are +inspected per **AWS D1.1** (symbols per **AWS A2.4**); the rail run and frame are dimensionally +surveyed; the drive and its **wind + snow interlocks** are cycle-tested; the **mandatory 4× hold-downs** +are proof-pull-tested; and the closed roof is water-tested. All limits are dual-unit and derived from +`partspec` / the NW-RR control drawings and the design proofs — **nothing invented**. Because this is a +**structure**, the plan is executed under the **PE-approved ITP** and the PE's field hold-points. + +**Standards.** AWS D1.1 (weld acceptance, WPS/PQR/WPQ) & AWS A2.4 (symbols); ASTM A123 (galvanize); +ASTM A500 Gr B / A36 (materials); ASTM F1554 Gr36 (anchors) & ACI 318 Ch.17 (anchorage); ASCE 7 +(load basis); ISO 2768 (general tolerance); ASME Y14.5-2018 (bracket GD&T). + +--- + +## 1. Weld visual & NDE acceptance (AWS D1.1) + +1. **Qualification gate.** Before any acceptance, verify the **WPS + PQR** and **welder qualification + (WPQ)** are on file per AWS D1.1; production welding by unqualified procedure/welder is rejectable. +2. **Visual (VT) — 100% of all welds** by an **AWS Certified Welding Inspector (CWI)** to the AWS D1.1 + visual acceptance criteria (statically-loaded, unless the PE classifies a joint cyclic): no cracks; + full fusion; undersize, undercut, porosity, and profile within the D1.1 limits; weld sizes match the + **as-built `weld_map.md`**. +3. **Surface NDE — critical joints.** **MT (magnetic-particle)** or **PT (dye-penetrant)** per the + `weld_map.md` extents: **J3 (wheel brackets) 100%**, **J5 (hold-downs) 100%**, **J6 (rail splices) + 100% PT**, sampling elsewhere. **UT** on CJP groove welds (J5/J6) **if required by the PE**. + *(NDE method + extent are ASSUMED — PE/CWI to confirm; see §8.)* +4. **Reject/repair.** Reweld and re-inspect per AWS D1.1; log every disposition. + +| Weld group | Method | Extent | Accept criterion | +|---|---|---:|---| +| J1 frame corners | VT (+MT) | 100% VT / 25% MT | AWS D1.1 visual; no cracks; size per as-built | +| J2 purlin-to-frame | VT | 100% | AWS D1.1 visual | +| **J3 wheel brackets** | VT + **MT** | **100% / 100%** | AWS D1.1; no surface indications | +| J4 drive bracket | VT (+MT) | 100% / 25% | AWS D1.1 visual | +| **J5 hold-downs** | VT + **MT** (UT if CJP) | **100% / 100%** | AWS D1.1; **CRITICAL** — no indications | +| **J6 rail splices** | VT + **PT** (UT per PE) | **100% / 100%** | flush-ground running surface; no indications | +| J7 end stops / J8 rail supports / J9 gussets | VT | 100% | AWS D1.1 visual (J8 torque-check if bolted) | + +--- + +## 2. Material & galvanize acceptance + +| Characteristic | Requirement | Method | Class | +|---|---|---|---| +| HSS material | **ASTM A500 Gr B** mill cert, heat/lot traceable | certificate review | Critical | +| Plate / rail material | **ASTM A36** mill cert, heat/lot traceable | certificate review | Critical | +| Galvanize | **ASTM A123** coating mass/thickness | magnetic thickness gauge + cert | Major | +| Vent/drain holes | present on all closed HSS (ASSUMED locations) | visual | Major | +| Galv field-repair | bidder procedure (ASTM A780, supplementary) | visual + thickness | Major | + +--- + +## 3. Frame & bracket dimensional acceptance + +| Characteristic | Nominal (mm [in]) | Accept limit | Method | Class | +|---|---|---|---|---| +| Frame perimeter | 3000.0 [118.110] × 3000.0 [118.110] | ISO 2768 + frame-square | tape/total station | Major | +| Frame square | — | **≤ 3 mm/m** | diagonal check | **Critical** | +| Frame diagonal difference | — | **≤ 5 mm [0.197 in]** | cross-diagonal tape | **Critical** | +| Purlin spacing | 600.0 [23.622] o.c., ×5 | ISO 2768 | tape | Minor | +| Bracket plate envelope (NW-RR-004/-005/-006) | per §3 SOW | ISO 2768-mK | calipers | Minor | +| Bracket hole pattern | — | **≤ 0.25 mm [0.010 in]** | CMM / gauge | Major | + +--- + +## 4. Rail alignment survey (NW-RR-003) + +The rail run is the wheel path — **alignment and straightness govern**, not wheel capacity. + +| Characteristic | Nominal (mm [in]) | Accept limit | Method | Class | +|---|---|---|---|---| +| Rail length (each) | 6000.0 [236.220] = **2× roof length** | ISO 2768 | tape/total station | Major | +| Gauge (rail spacing) | 3000.0 [118.110] | **± 2 mm [0.079 in]** over the run | total station / trammel | **Critical** | +| Straightness | — | **≤ 3 mm [0.118 in] over the run** | string-line / laser | **Critical** | +| Level / co-planarity of the two rails | — | per PE *(ASSUMED — e.g. ≤ 3 mm)* | level / total station | Critical | +| Splice flushness (J6) on running surface | — | **flush, ground** — no step at the wheel path | straightedge / feeler | **Critical** | +| End stops present + engaged | 4, both ends both rails | hard stop + rubber pad | function check | Major | + +--- + +## 5. Drive-cycle & interlock test (NW-RR-005 + NW-CO-011) + +| Test | Requirement | Method | Class | +|---|---|---|---| +| Full open travel | Roof clears the aperture (travels its **full length, 3000 mm [118.110 in]**) | run to open end stop | **Critical** | +| Open/close time | **≤ 45 s** each (**< 60 s** motor timeout) | stopwatch | Major | +| Roof speed | ≈ 0.067 m/s (**< 0.30 m/s** ceiling) | travel/time | Minor | +| Tractive load / drive margin | Move force ≈ **235 N [52.8 lbf]**; drive **SF ≥ 2.0** vs the drive rating | inline load / motor current | **Critical** | +| Soft-start / soft-stop | smooth ramp; end-stop engagement without slam | observe | Major | +| Manual release | disengages drive; roof movable by hand; re-engages | function | **Critical (safety)** | +| Photo-beam / limit safety | reverses/stops on obstruction; limits stop travel | function | **Critical (safety)** | +| **Wind interlock** | **park ≤ 25 mph [11.2 m/s]; emergency close ≤ 35 mph [15.6 m/s] gust**; positive close against gust | simulate signal | **Critical (safety)** | +| **Snow interlock (MANDATORY)** | roof **held CLOSED while snow-loaded** — cannot be commanded open (snow-laden roll force **627 N [141 lbf] > drive**) | simulate snow signal | **Critical (safety)** | +| Drive ↔ hold-down interlock | drive inhibited while a hold-down is engaged; hold-downs re-engage on close | function | **Critical (safety)** | + +> **Why the interlocks are mandatory (grounding).** The drive is **WIND-sized** — at 35 mph the wind is +> **62%** of the 235 N tractive load, so a positive close against the gust is the governing requirement +> (`MECHANICAL_DESIGN.md` §10). A **fully snow-laden** roof needs **627 N [141 lbf]** to roll, which +> **exceeds the ~500 N [112 lbf] drive**, so opening under snow is prohibited by interlock. + +--- + +## 6. Hold-down proof-pull test (NW-RR-006 — MANDATORY / REMEDIATION) + +| Characteristic | Requirement | Method | Class | +|---|---|---|---| +| Anchor count | **4** hold-downs installed & interlocked | visual | **Critical** | +| Proof load per anchor | **≥ 2 klbf [8.9 kN]** (PE to set the proof value) | calibrated hydraulic pull / load cell | **Critical** | +| Group capacity vs demand | 4× ≥ 2 klbf = **35.6 kN [8 klbf]** vs net uplift **7.4 kN [1.66 klbf]** → **SF 3.9** | calc + test | **Critical** | +| Anchorage (into enclosure foundation) | per **ACI 318 Ch.17**; embedment/edge PE-set | PE field inspection | **Critical** | +| No slip / no concrete cone / no bracket yield at proof | pass | observe + measure | **Critical** | + +> **Grounding.** Survival-wind proof (`MECHANICAL_DESIGN.md` §7): **gross roof uplift 9.2 kN +> [2.06 klbf]** vs **1.8 kN [397 lbf]** self-weight → **SF 0.19 FAIL**; the **4× hold-downs restore +> SF 3.9**. This is the **REMEDIATION** and is **not** value-engineerable out. Anchors land in the +> **enclosure/building foundation** (the pier is isolated — README §4). + +--- + +## 7. Weatherproofing (roof-closed water test) + +| Characteristic | Requirement | Method | Class | +|---|---|---|---| +| Roof-closed rain test | no ingress at panel laps, ridge/edge flashing, perimeter gasket | spray/hose test | Major | +| Drainage / weeps | closed roof sheds water; no ponding | visual under test | Major | +| Wall-top weather seal | roof lands on and seals to the wall-top | visual + water test | Major | + +--- + +## 8. Sampling, documentation & sign-off + +- **Critical characteristics** (frame square/diagonal, rail gauge/straightness/splice, drive travel + + all safety interlocks, hold-down proof, critical welds J3/J5/J6) receive **100%** inspection. +- **Major / Minor** verified to ISO 2768 / drawing on the acceptance record. + +The bidder shall deliver: + +1. **CWI weld report** — VT + MT/PT/UT results keyed to the as-built `weld_map.md`, with dispositions. +2. **WPS / PQR / WPQ** records (AWS D1.1). +3. **Material & galvanize certs** (A500 Gr B, A36, A123 coating record). +4. **Frame + rail dimensional survey** (square, diagonal, gauge, straightness, splice flushness). +5. **Drive-cycle + interlock commissioning report** (travel, time, tractive load, wind + snow + hold-down interlocks, safeties). +6. **Hold-down proof-pull record** (each anchor, proof load, SF). +7. **Weatherproofing (water) test** record. +8. **PE field hold-point sign-offs** (fit-up, weld, galvanize, anchor set, commissioning) + stamped drawings. +9. **RFI / nonconformance** dispositions for every ASSUMED value. + +| Role | Name | Signature | Date | +|---|---|---|---| +| Bidder QA | | | | +| CWI (weld) | | | | +| Owner / PE review | | | | + +--- + +## 9. ASSUMED-value register (do NOT treat as final) + +| # | ASSUMED acceptance value | Part(s) / joint(s) | +|---|---|---| +| 1 | All weld sizes & the NDE method/extent (VT/MT/PT/UT %) | all welds (`weld_map.md`) | +| 2 | Member sizes the dimensional checks verify against | NW-RR-001/-002/-003 | +| 3 | Rail level / co-planarity limit | NW-RR-003 | +| 4 | Drive rating (→ the SF ≥ 2.0 gate) & interlock thresholds/logic | NW-RR-005 / NW-CO-011 | +| 5 | Hold-down proof load & anchorage (embedment/edge, ACI 318 Ch.17) | NW-RR-006 | +| 6 | Wheel axle / wheel rating (≥4× wheel share) | NW-RR-004 / NW-CO-010 | +| 7 | Weatherproofing detail & water-test method | roof / NW-CO-012 | +| 8 | Galvanize vent/drain + field-repair (A780) acceptance | all steel | +| 9 | Bracket GD&T / surface-finish acceptance values | NW-RR-004/-005/-006 | + diff --git a/design/mechanical/tender/packages/BP-04_rolloff_roof/weld_map.md b/design/mechanical/tender/packages/BP-04_rolloff_roof/weld_map.md new file mode 100644 index 0000000..27bc882 --- /dev/null +++ b/design/mechanical/tender/packages/BP-04_rolloff_roof/weld_map.md @@ -0,0 +1,93 @@ +# BP-04 — Weld Map / Joint Schedule (Roll-off Roof) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-04 — Roll-off roof ("disappearing turret") | +| Welding standard | **AWS D1.1** (structural steel; WPS/PQR + welder qualification WPQ) | +| Symbol standard | **AWS A2.4** (weld & NDE symbols) | +| Base metals | ASTM A500 Gr B HSS (frame/purlins); ASTM A36 (rail beams, brackets) | +| Finish | Hot-dip galvanize per ASTM A123 (**weld before galvanizing**) | +| Issue date / rev | 2026-08-06 / A | + +This joint schedule enumerates the weld groups of BP-04, the **AWS A2.4 weld symbol** to apply, the +joint type, the **ASSUMED** weld size, the process, and the NDE. It is read with `SOW.md` and +`acceptance.md`, and it is marked up **as-built** by the fabricator for the CWI report. + +> **⚠ ALL WELD SIZES AND JOINT TYPES BELOW ARE ASSUMED DESIGN-INTENT — PE/CWI TO CONFIRM.** No weld +> leg, throat, or CJP/PJP designation herein is a released value. The PE/CWI shall size each weld per +> the **AWS D1.1 minimum-fillet-weld tables** (governed by the **thinner part joined** — HSS walls are +> 3.2 mm [0.125 in]/4.0 mm [0.157 in]; brackets 9.5 mm [0.375 in]/12.7 mm [0.500 in]) and the PE's +> computed member forces for the **ASCE 7 snow (10.8 kN [2.42 klbf]) and survival-wind (9.2 kN +> [2.06 klbf] uplift)** cases. Prequalified WPS per AWS D1.1 are acceptable where applicable. + +--- + +## 1. AWS A2.4 symbol legend (as used in this schedule) + +| Symbol (text form) | AWS A2.4 meaning | +|---|---| +| `▷` fillet | Fillet weld; leg size to the left of the symbol (e.g. `6▷` = 6 mm leg). | +| `▷ (both sides)` | Fillet weld both sides of the joint (symbol on both sides of the reference line). | +| `V` / `CJP` | Complete-joint-penetration groove weld (bevel/V), full throat. | +| `PJP` | Partial-joint-penetration groove weld; effective throat specified. | +| `○` weld-all-around | Weld-all-around flag at the reference-line elbow (perimeter joint). | +| `▶` field weld | Field-weld flag (filled triangle at the elbow) — welds made at site, not in shop. | +| Tail `(VT)`, `(MT)`, `(PT)`, `(UT)` | NDE method called in the symbol tail (visual / mag-particle / dye-penetrant / ultrasonic). | + +*(Rendered on the PDF control drawings and the STEP/weldment model as true AWS A2.4 graphic symbols; +the text forms above are the Markdown transcription.)* + +--- + +## 2. Joint schedule + +| Joint | Location / parts joined | Joint type | AWS A2.4 symbol (ASSUMED size) | Process (ASSUMED) | NDE (ASSUMED extent) | Criticality | +|---|---|---|---|---|---|---| +| **J1** | **Frame corners** — NW-RR-001 HSS-to-HSS mitred corners + corner gussets | Mitred T/corner, fully welded + gusset | `○ weld-all-around`, CJP or `5▷` fillet all-around `(VT)` | GMAW / FCAW | **VT 100%** + **MT 25%** | High (frame integrity) | +| **J2** | **Purlin-to-frame** — NW-RR-002 → NW-RR-001 (5 T-joints) | HSS-to-HSS T-joint | `5▷ (both sides)` fillet `(VT)` *(bolted clip alt. permitted)* | GMAW / FCAW | **VT 100%** | Medium (snow path) | +| **J3** | **Wheel-bracket-to-frame** — NW-RR-004 → NW-RR-001 (8×) | Plate-to-HSS, gusseted | `6▷ (both sides)` fillet `(VT)(MT)` | GMAW / FCAW | **VT 100%** + **MT 100%** | **CRITICAL** (carries roof + snow into wheels) | +| **J4** | **Drive-bracket-to-frame** — NW-RR-005 → NW-RR-001 (drive mount) | Plate-to-HSS, gusseted | `6▷ (both sides)` fillet `(VT)` | GMAW / FCAW | **VT 100%** + **MT 25%** | Medium (drive reaction) | +| **J5** | **Hold-down bracket welds** — NW-RR-006 → frame/roof structure (4×) | Plate-to-HSS/plate, gusseted | **CJP** or `8▷ (both sides)` fillet `(VT)(MT)` | GMAW / FCAW / SMAW | **VT 100%** + **MT 100%** (UT if CJP) | **CRITICAL — REMEDIATION** (survival-wind uplift) | +| **J6** | **Rail splices** — NW-RR-003 box-track length-to-length (if spliced) | Butt joint, running surface | **CJP** groove, backing, **ground flush** on wheel path `(VT)(PT)` | GMAW / SMAW | **VT 100%** + **PT 100%** (UT per PE) | **CRITICAL** (wheel running surface) | +| **J7** | **End stops** — stop blocks → NW-RR-003 rail ends / NW-RR-005 (4×) | Plate/block-to-rail | `6▷` fillet, weld-all-around `(VT)` | GMAW / FCAW | **VT 100%** | Medium (impact) | +| **J8** | **Rail-to-support / foundation connection** — NW-RR-003 → rail support | Plate-to-HSS **or bolted** | `6▷ (both sides)` fillet `(VT)` **or** ASTM A574 bolting | GMAW / FCAW / bolt | **VT 100%** (torque check if bolted) | High (rail reactions) | +| **J9** | **Corner gussets / stiffeners** — gusset plates at J1/J3/J5 | Plate-to-HSS | `5▷` fillet, weld-all-around `(VT)` | GMAW / FCAW | **VT 100%** | Supports J1/J3/J5 | + +**Field vs shop.** J1–J7 and J9 are **shop welds** (weld → inspect → galvanize). J8 and any hold-down +attachment made at site are **field welds** (`▶` flag) — field-welded galvanized steel requires **galv +field-repair** per the bidder's approved procedure (ASTM A780, supplementary to the register). + +--- + +## 3. Welding controls (AWS D1.1) + +1. **WPS / PQR / WPQ.** Submit a **Welding Procedure Specification** for each joint/process/position, + supported by a **Procedure Qualification Record** (or invoke a prequalified WPS where AWS D1.1 + allows), and **Welder Qualification records (WPQ)** for every welder before production welding. +2. **Filler / preheat.** Filler metal, preheat, and interpass temperatures per the qualified WPS + (matching A500/A36 base metals) — **ASSUMED to the WPS; bidder to state.** +3. **Sequence & distortion.** Weld the frame (J1) and purlins (J2) with a sequence that holds the + **frame-square 3 mm/m / diagonal 5 mm [0.197 in]** tolerance (`acceptance.md`); back-gouge CJP roots + as required by the WPS. +4. **Galvanizing interaction.** Complete and inspect all shop welds **before** hot-dip galvanizing + (ASTM A123). Provide **vent/drain holes** for the bath on all closed HSS (locations ASSUMED — bidder + to detail, off stiffness-critical faces). Repair galvanize burned by field welds per §2 note. +5. **CWI.** An **AWS Certified Welding Inspector** performs/oversees the VT and the MT/PT/UT called + above and signs the weld report (`acceptance.md`). + +--- + +## 4. ASSUMED-value register (this weld map) + +| # | ASSUMED value | Joints | Status | +|---|---|---|---| +| 1 | Every fillet leg / throat size (5–8 mm shown) | J1–J9 | ASSUMED — PE/CWI per AWS D1.1 min-fillet tables | +| 2 | CJP vs PJP vs fillet choice at J1, J5, J6 | J1, J5, J6 | ASSUMED — PE to set from computed demand | +| 3 | NDE method + extent (VT/MT/PT/UT %) | all | ASSUMED — PE/CWI to set (critical joints 100%) | +| 4 | Welding process (GMAW/FCAW/SMAW) & filler/preheat | all | ASSUMED — per qualified WPS | +| 5 | Bolted vs welded alternative at J2, J8 | J2, J8 | ASSUMED — bidder option, PE to approve | +| 6 | Galv vent/drain-hole locations | HSS | ASSUMED — bidder to detail | +| 7 | Field-weld extent + galv repair (A780) | J8 + site | ASSUMED — bidder procedure | + diff --git a/design/mechanical/tender/packages/BP-05_cots_schedule/README.md b/design/mechanical/tender/packages/BP-05_cots_schedule/README.md new file mode 100644 index 0000000..a1a0bb0 --- /dev/null +++ b/design/mechanical/tender/packages/BP-05_cots_schedule/README.md @@ -0,0 +1,235 @@ +# BP-05 — COTS Procurement Schedule (Buy-to-Print / Off-the-Shelf) — Buyer README + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | **BP-05 — COTS procurement schedule** | +| Trade | Purchasing / integration (no fabrication; no fab control drawings) | +| Parts | NW-CO-001 … NW-CO-014 (14 catalogue line items; 0 fabrication drawings) | +| Branch / project | `design/mechanical-tender` — NIGHTWATCH Observatory Mount & Roll-off Tender | +| Issue date | 2026-08-06 | +| Revision | A | +| Governing TDP standard | MIL-STD-31000A (Technical Data Package) | + +This README is the entry point for the **purchasing agent / integrator** bidding BP-05. It defines the +**scope**, the **data package the buyer receives**, the **buyer deliverables**, the **PROCUREMENT +GATE (mandatory design-remediation items)**, and the items carried as **ASSUMED design-intent / +representative catalogue selections** the buyer must confirm. Read it with `SCHEDULE.md` (the purchase +table — item, part number, qty, key specs, supplier class + representative catalogue numbers) and +`fastener_schedule.md` (consolidated ASTM A574 SHCS + ASTM F1554 anchor buy) in this same folder. + +BP-05 is a **buy list, not a fabrication package** — every line is COTS (`drawing=False` in the parts +registry; `stock=('cots',)`), so no NW-CO item carries a fabrication control drawing. Where a line +interfaces to a fabricated part (bearing seats, drive registers, wheel bores, anchor holes), the +**mating dimension is fixed by the fabrication package (BP-01…BP-04)** and repeated here for the +buyer's convenience — the buyer selects a catalogue item that **fits that fixed interface**. + +--- + +## 0. PROCUREMENT GATE — READ FIRST (four items are MANDATORY design remediations) + +> **⚠ FOUR of the fourteen BP-05 lines are the physical embodiment of the design proof-out +> remediations. They are NOT interchangeable with the cheaper baseline parts they replace. Buying the +> baseline part instead re-opens a proof the design study recorded as FAIL.** + +The trade study and proof modules (`MECHANICAL_DESIGN.md` §9, `tradestudy/SECTION.md`) disqualified the +repository's baseline component choices on four counts. BP-05 carries the **remediated** parts. These +four are the gate: + +| # | Line | Remediation (buy this) | Replaces / fixes | Proof result | +|---|---|---|---|---| +| 1 | **NW-CO-003 / -004** | Angular-contact **7008 / 7006** super-precision pairs, back-to-back (DB) | deep-groove **6008 / 6006** | Bearing compliance was **98 % of the 43.5″ stiffness FAIL**; angular-contact pairs restore moment stiffness. | +| 2 | **NW-CO-005** | **On-axis absolute encoder** (BiSS-C ring, sub-arcsec), 2 off | motor-side-only encoding | Baseline **5.02″ RMS FAIL** vs 1.0″ target; on-axis ring reaches **0.54″**. | +| 3 | **NW-CO-008** | **Temperature-compensated focuser** | passive (fixed) focus | 22 K night swing walks focus **~10× depth-of-focus**. | +| 4 | **NW-CO-013** | **48 V LiFePO4 battery pack** (+ solar) | 12 V pack | 12 V gives **3.3 h vs 10 h** night autonomy; 48 V gives **13.4 h**. | + +> **Note on the 5th remediation.** The design study lists **five** proof-out remediations. The fifth — +> the **4× survival-wind hold-down anchors (survival uplift 9.2 kN → SF 3.9)** — is a **fabricated +> steel item, NW-RR-006, and lives in BP-04**, not here. Its anchor bolts are captured in +> `fastener_schedule.md` (ASTM F1554) for a consolidated buy, but the brackets themselves are BP-04 +> scope. BP-05 therefore carries **4 of the 5 remediations**. + +**Gate rule.** NW-CO-003, NW-CO-004, NW-CO-005 and NW-CO-008 are **MANDATORY for the buildable v1**. +NW-CO-013 (48 V pack) is **MANDATORY only for the off-grid v2**; **v1 may run grid + UPS** (see the +roadmap, `MECHANICAL_DESIGN.md` §9). A bid that substitutes a baseline part for any of the four v1 +remediations shall be raised as an **RFI/deviation** and is not accepted without engineering sign-off. + +**Optional line.** **NW-CO-009 counterweights are OPTIONAL** — the selected topology is the +**counterweight-FREE GEM** (torque proof SF 2.5/2.6; deletes 15.4 kg + 29 % RA inertia). Buy the +counterweight set **only** if the counterweighted variant is elected. See §6. + +--- + +## 1. Scope + +BP-05 covers **every purchased (off-the-shelf) component** in the NIGHTWATCH mount + roll-off +enclosure that is not fabricated under BP-01…BP-04. The buyer: + +1. **Procures the drivetrain** — the two Harmonic Drive LLC strain-wave gears (NW-CO-001 RA, + NW-CO-002 DEC), the two NEMA17 + 27:1 planetary stepper packages (NW-CO-006), and the two + microstepping drivers / controller (NW-CO-007). +2. **Procures the four v1 remediation items** — the angular-contact bearing pairs (NW-CO-003 7008, + NW-CO-004 7006), the on-axis absolute encoders (NW-CO-005), and the temperature-compensated + focuser (NW-CO-008). See §0. +3. **Procures the enclosure motion + weather hardware** — V-groove track wheels (NW-CO-010), the + roll-off roof drive / gate operator (NW-CO-011), and the metal roofing panel + flashing (NW-CO-012). +4. **Procures the power system** — the 48 V LiFePO4 battery pack + solar (NW-CO-013), mandatory for + off-grid v2. +5. **Consolidates the fastener buy** — ASTM A574 socket-head cap screws and ASTM F1554 anchor bolts + across all packages (NW-CO-014 → `fastener_schedule.md`). +6. **Optionally** procures the cast-iron counterweight set (NW-CO-009) if the counterweighted variant + is bid. +7. Delivers **manufacturer certificates of conformance (CoC), datasheets, and interface confirmations** + (bearing precision class, encoder protocol, drive bore/torque) so each buy is traceable against the + fixed fabricated interfaces. + +**Out of scope for BP-05** (the fabricated parts these items bolt into — separate packages): + +| Interface | Fabricated under | BP-05 item that mates to it | +|---|---|---| +| RA / DEC axis housings, bearing seats, drive registers | BP-01 (machining) | NW-CO-001/-002 drives; NW-CO-003/-004 bearings; NW-CO-005 encoders | +| RA / DEC drive spindles (carry bearing inner races, couple the strain-wave gears) | BP-02 (turned parts) | NW-CO-001/-002 drives; NW-CO-003/-004 bearings | +| Pier / adapter plate | BP-03 (pier) | NW-CO-014 anchors (F1554) via `fastener_schedule.md` | +| Roll-off roof frame, rails, wheel brackets, drive bracket, **wind hold-downs** | BP-04 (roll-off) | NW-CO-010 wheels; NW-CO-011 drive; NW-CO-012 roofing; F1554 hold-down anchors | + +--- + +## 2. Data package the buyer receives (per MIL-STD-31000A) + +| Item | Format / standard | Role | +|---|---|---| +| `SCHEDULE.md` | Markdown | The **purchase table**: item, NW-CO-xx, qty, key specs (from `partspec`), supplier CLASS + representative catalogue part numbers, remediation flags, per-item procurement notes. | +| `fastener_schedule.md` | Markdown | Consolidated **ASTM A574 SHCS + ASTM F1554 anchor** buy across BP-01…BP-04 interfaces. | +| `cots_schedule.csv` (rows NW-CO-001…-014, + NW-PF-004) | CSV / XLSX (BOM per ASME Y14.34) | Machine-readable buy list generated from the parts registry. | +| `master_bom.csv` (BP-05 rows) | CSV / XLSX | Roll-up into the full-project BOM. | +| Interface dimensions | Repeated in `SCHEDULE.md` from BP-01…BP-04 control drawings (PDF) + STEP AP242 solids | Fixed mating dims the catalogue part must satisfy (bearing bores/OD, drive bores, wheel bores, anchor holes). | + +**Order of precedence.** For a purchased part the **manufacturer's datasheet governs the part's own +dimensions and ratings**; the **fixed fabricated interface (BP-01…BP-04 control drawing / STEP AP242 +solid) governs the mating dimension the part must fit.** Where a catalogue part cannot meet a fixed +interface, the buyer raises an **RFI before ordering** — the interface is not changed to suit stock. +General tolerances of any buyer-detailed adapter per **ISO 2768**; fits per **ISO 286** (bearing seats +H7 / journals j6 are set by BP-01/BP-02, not here). All representative catalogue numbers below are +**"representative — confirm"**: they establish the class, rating, and interface, **not** a sole source. + +--- + +## 3. Line-item summary (source: `partspec.parts_for('BP-05')`) + +All 14 lines are COTS — no fabrication drawing, no stock mass. Key specs are quoted from the parts +registry (`gen/partspec.py`, traced to `calc/params.py` where marked SOURCED). Full specs, suppliers, +and representative catalogue numbers are in `SCHEDULE.md`. + +| Part no. | Name | Qty | Headline spec (from `partspec`) | Class | Status | +|---|---|---:|---|---|---| +| **NW-CO-001** | Harmonic drive, RA | 1 | CSF-32-100-2A-GR — ratio 100, rated 127 Nm [93.7 lbf·ft], peak 343 Nm [253 lbf·ft], hollow bore Ø80.0 mm [3.150 in] | Strain-wave (sole-class) | Core | +| **NW-CO-002** | Harmonic drive, DEC | 1 | CSF-25-80-2A-GR — ratio 80, rated 70 Nm [51.6 lbf·ft], peak 186 Nm [137 lbf·ft], hollow bore Ø64.0 mm [2.520 in] | Strain-wave (sole-class) | Core | +| **NW-CO-003** | Angular-contact bearing 7008 (RA pair) | 2 | Bore Ø40 mm [1.575 in], OD Ø68 mm [2.677 in], width 15 mm [0.591 in], DB back-to-back | Super-precision ABEC-7 | **REMEDIATION (v1 mandatory)** | +| **NW-CO-004** | Angular-contact bearing 7006 (DEC pair) | 2 | Bore Ø30 mm [1.181 in], OD Ø55 mm [2.165 in], width 13 mm [0.512 in], DB back-to-back | Super-precision ABEC-7 | **REMEDIATION (v1 mandatory)** | +| **NW-CO-005** | On-axis absolute encoder (RA + DEC) | 2 | Resolution < 1″; interface BiSS-C / SSI | Absolute ring + readhead | **REMEDIATION (v1 mandatory)** | +| **NW-CO-006** | Stepper NEMA17 + 27:1 planetary | 2 | 1.8° step, 27:1 planetary, Irun 1.5 A / Igoto 2.0 A, holding 0.45 Nm | Stepper + gearhead | Core | +| **NW-CO-007** | Motor driver (TMC5160) / OnStepX board | 2 | Irun 1.5 A, Igoto 2.0 A, 16 microsteps | Microstepping driver | Core | +| **NW-CO-008** | Temperature-compensated focuser | 1 | Temp coeff −2.5 steps/°C (ASSUMED) | Motorised absolute focuser | **REMEDIATION (v1 mandatory)** | +| **NW-CO-009** | Counterweights (5 kg ×2, 2.5 kg ×1) | 3 | 12.5 kg total available; shaft Ø31.75 mm [1.250 in] × 457.2 mm [18.000 in] (BP-02) | Cast iron | **OPTIONAL** | +| **NW-CO-010** | V-groove track wheels | 8 | Ø~4 in [~101.6 mm], ≥150 kg [≥331 lb] each | Steel V-groove | Core | +| **NW-CO-011** | Roof drive (gate operator) | 1 | Move force 235 N [52.8 lbf], SF 2.1 vs ~500 N [112 lbf] drive; fail-safe + wind interlock | Sliding-gate operator | Core | +| **NW-CO-012** | Metal roofing panel + flashing | 1 | Area ≈ 9.0 m² [≈96.9 ft²] (3.0 × 3.0 m) | Standing-seam / corrugated steel | Core | +| **NW-CO-013** | 48 V LiFePO4 battery pack + solar | 1 | 48 V bus, ~5 kWh, 13.4 h autonomy | LiFePO4 + PV | **REMEDIATION (v2 mandatory; v1 grid+UPS)** | +| **NW-CO-014** | Fastener schedule (SHCS, anchors) | 1 | ASTM A574 SHCS + ASTM F1554 Gr36 anchors — see `fastener_schedule.md` | Fastener buy | Core | + +--- + +## 4. Cross-package interfaces the buyer must satisfy + +Each purchased part fits a **fixed** fabricated feature. The buyer confirms the catalogue part meets +the mating dimension before ordering; **the mating dimension is not adjustable to suit stock.** + +| BP-05 item | Fits (fixed by) | Fixed mating dimension | +|---|---|---| +| NW-CO-001 (CSF-32) | RA housing drive register (NW-MH-001) + RA spindle (NW-TP-002) | Drive bore Ø80.0 mm [3.150 in]; wave-gen register; 8 × M4 on Ø104 mm [4.094 in] PCD | +| NW-CO-002 (CSF-25) | DEC housing drive register (NW-MH-002) + DEC spindle (NW-TP-003) | Drive bore Ø64.0 mm [2.520 in]; 8 × M4 on Ø83 mm [3.268 in] PCD | +| NW-CO-003 (7008 pair) | RA housing seat H7 (NW-MH-001) + RA journal j6 (NW-TP-002) | Seat bore Ø68 mm [2.677 in] H7; journal Ø40 mm [1.575 in] j6 | +| NW-CO-004 (7006 pair) | DEC housing seat H7 (NW-MH-002) + DEC journal j6 (NW-TP-003) | Seat bore Ø55 mm [2.165 in] H7; journal Ø30 mm [1.181 in] j6 | +| NW-CO-005 (encoder ring) | RA / DEC axis (on-axis, one per axis) | Ring bore to clear the axis; readhead standoff — **ASSUMED, confirm to axis geometry** | +| NW-CO-010 (V-groove wheels) | Wheel axle brackets (NW-RR-004), M16 axle | Wheel bore for M16 [Ø16 mm] axle; V-groove to match rail (NW-RR-003) | +| NW-CO-011 (roof drive) | Drive bracket (NW-RR-005) | Rack/chain mount; 180 kg [397 lb] roof; end-stop + wind interlock | +| NW-CO-012 (roofing) | Roof frame + purlins (NW-RR-001/-002) | ≈9.0 m² [≈96.9 ft²] skin; edge/ridge flashing to frame | +| F1554 anchors (`fastener_schedule.md`) | Pier top plate (NW-PF-002) + wind hold-downs (NW-RR-006) | 4 × Ø0.75 in [Ø19.05 mm] each group; PE to size final | + +--- + +## 5. Buyer deliverables + +1. **Purchase orders** placed against `SCHEDULE.md` and `fastener_schedule.md`, with the **four v1 + remediation lines** (NW-CO-003/-004/-005/-008) confirmed as the remediated parts, not baseline. +2. **Certificates of conformance (CoC) / datasheets** for every line, and specifically: + - Bearings (NW-CO-003/-004): **precision-class cert (≥ ABEC-7 / P4), DB-set matching cert, preload + class** — confirming they fit the H7 seats / j6 journals set by BP-01/BP-02. + - Encoders (NW-CO-005): **BiSS-C (or SSI) protocol confirmation and accuracy/resolution + (sub-arcsec)** datasheet — confirming < 1″ on-axis. + - Harmonic drives (NW-CO-001/-002): **rated/peak torque, ratio, and hollow-bore** datasheet — + confirming Ø80.0 / Ø64.0 mm bores and the 8 × M4 PCDs. + - Roof drive (NW-CO-011): **fail-safe behaviour + wind-interlock capability** confirmation. + - 48 V pack (NW-CO-013): **capacity (kWh), 48 V bus, cell chemistry (LiFePO4), safety listing**. +3. **Interface-fit confirmation** — a short matrix confirming each catalogue part meets the fixed + mating dimension in §4 (raise an RFI where it does not). +4. **Fastener CoC** — ASTM A574 SHCS mill cert / CoC and ASTM F1554 Gr36 anchor mill cert + + galvanize (ASTM A123) cert, per `fastener_schedule.md`. +5. **Deviation / RFI log** — every representative catalogue number confirmed to an actual ordered part, + and every ASSUMED value (§6) resolved before order. + +--- + +## 6. ASSUMED design-intent / representative selection — buyer & PE to confirm (NOT final) + +Carried as **ASSUMED design-intent** or **representative catalogue selection** in the parts registry; +**not** released procurement decisions. Confirm before ordering: + +- **All representative catalogue part numbers** in `SCHEDULE.md` (SKF/NSK/FAG bearings, Renishaw/ + Heidenhain encoder, LiftMaster/US-Automatic gate operator, ZWO/Optec/Pegasus focuser, EG4/Battle-Born + 48 V pack, DuraGates/CasterHQ wheels) are **"representative — confirm"**. They fix the **class, + rating, and interface**, not a sole source. Buyer confirms availability, lead time, and fit. +- **On-axis encoder ring geometry & interface** (NW-CO-005) — ring bore, readhead standoff, and + BiSS-C vs SSI are **ASSUMED**; confirm against the actual RA/DEC axis geometry and the OnStepX / + controller input. +- **Roof drive product & sizing** (NW-CO-011) — the **gate-operator class, fail-safe mode, and wind + interlock** are **ASSUMED design-intent** (enclosure proof: 235 N [52.8 lbf] move force, SF 2.1); + the specific operator, its duty rating for the 180 kg [397 lb] roof, and the interlock logic are + **PE/controls to confirm**. +- **V-groove wheel product & rail match** (NW-CO-010) — Ø4 in [101.6 mm], ≥150 kg [≥331 lb]/wheel is + **ASSUMED**; confirm the V-profile matches the BP-04 box-track section (NW-RR-003) and the ≥4× load + factor on the 180 kg roof + snow. +- **Roofing panel type/gauge & flashing** (NW-CO-012) — standing-seam vs corrugated, gauge, and + fastening are **ASSUMED**; confirm against the ASCE 7 snow/wind case and the purlin spacing (BP-04). +- **48 V pack sizing / v1 vs v2** (NW-CO-013) — ~5 kWh / 13.4 h autonomy is **ASSUMED**; the DGX duty + cycle drives ~59 % of the load. **v1 may run grid + UPS**; the pack is **mandatory only for off-grid + v2**. +- **Temp-comp focuser coefficient & coupling** (NW-CO-008) — the −2.5 steps/°C coefficient is + **ASSUMED**; confirm the focuser couples to the OTA drawtube and the temperature-compensation curve + against the 22 K night swing. +- **Motor + gearhead + driver products** (NW-CO-006/-007) — NEMA17 1.8° + 27:1 planetary + TMC5160 / + OnStepX are **SOURCED for ratings** (`P.MOTOR`) but the **specific catalogue motor, gearhead, and + controller board are representative** — confirm. +- **Counterweights are OPTIONAL** (NW-CO-009) — omitted in the counterweight-FREE build; buy only if + the counterweighted variant is elected. +- **Fastener grades, lengths, and torque** (NW-CO-014) — ASTM A574 SHCS and ASTM F1554 Gr36 grades + are **ASSUMED design-intent**; **lengths, quantities, and preload/torque are ASSUMED and are + bidder/PE to confirm** (weld sizes and any structural anchor sizing are **PE** items). See + `fastener_schedule.md`. + +--- + +## 7. References + +- Parts registry: `design/mechanical/tender/gen/partspec.py` → `parts_for('BP-05')` (single source of truth). +- BOM / COTS: `design/mechanical/tender/bom/{cots_schedule,master_bom}.csv`. +- Design basis: `design/mechanical/MECHANICAL_DESIGN.md` §9 (selected configuration, cost deltas, the + 5 remediations, v1/v2 roadmap); `design/mechanical/tradestudy/SECTION.md` (drive/encoder/bearing/ + enclosure/power selection rationale, disqualification gate). +- Interfaces: BP-01 (`../BP-01_mount_machining/`), BP-02 (`../BP-02_turned_parts/`), + BP-03 (`../BP-03_pier_foundation/`), BP-04 (roll-off) — fixed mating dimensions. +- Standards register: MIL-STD-31000A (TDP); ASME Y14.34 (associated lists/BOM), Y14.100, Y14.24, + Y14.1; ISO 286 (fits), ISO 2768 (general tolerances); ASTM A574 (SHCS), ASTM F1554 Gr36 (anchors), + ASTM A123 (galvanize). Encoder interface BiSS-C / SSI. Formats: STEP ISO 10303 AP242 (interface + solids), PDF (interface drawings), CSV/XLSX (BOM). diff --git a/design/mechanical/tender/packages/BP-05_cots_schedule/SCHEDULE.md b/design/mechanical/tender/packages/BP-05_cots_schedule/SCHEDULE.md new file mode 100644 index 0000000..edb606e --- /dev/null +++ b/design/mechanical/tender/packages/BP-05_cots_schedule/SCHEDULE.md @@ -0,0 +1,134 @@ +# BP-05 — COTS Procurement Schedule (Purchase Table) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Package | BP-05 — COTS procurement schedule | +| Source of truth | `partspec.parts_for('BP-05')` (14 lines) + `bom/cots_schedule.csv` | +| Issue date / Rev | 2026-08-06 / A | +| TDP / BOM standards | MIL-STD-31000A; ASME Y14.34 (associated lists / BOM) | + +**How to read this schedule.** Every line is COTS (buy, no fabrication drawing). *Key specs* are quoted +from the parts registry (SOURCED = traced to `calc/params.py`; ASSUMED = design-intent, confirm). +*Supplier class* is the component class the design requires. *Representative catalogue part no.* is a +**real part that meets the class/rating/interface — "representative, confirm"**, established via a short +supplier search; it is **not a sole source** and the buyer confirms availability, lead time, and fit. +Dual units throughout: mm primary, inch in brackets. **Bold "REMEDIATION"** = a mandatory design +proof-out fix (see §0 of `README.md`). Fasteners (NW-CO-014) are broken out in `fastener_schedule.md`. + +--- + +## A. Purchase table + +| # | NW-CO | Item | Qty | Key specs (from `partspec`) | Supplier CLASS | Representative catalogue part no. — *confirm* | Status | +|---|---|---|---:|---|---|---|---| +| 1 | **NW-CO-001** | Harmonic drive, RA | 1 | Ratio 100; rated 127 Nm [93.7 lbf·ft]; peak 343 Nm [253 lbf·ft]; hollow bore Ø80.0 mm [3.150 in] *(SOURCED `P.RA_DRIVE`)*; backlash ≤1.0 arcmin | Strain-wave gear | **Harmonic Drive LLC CSF-32-100-2A-GR** *(registry designation = mfr part; sole-class)* | Core | +| 2 | **NW-CO-002** | Harmonic drive, DEC | 1 | Ratio 80; rated 70 Nm [51.6 lbf·ft]; peak 186 Nm [137 lbf·ft]; hollow bore Ø64.0 mm [2.520 in] *(SOURCED `P.DEC_DRIVE`)*; backlash ≤1.0 arcmin | Strain-wave gear | **Harmonic Drive LLC CSF-25-80-2A-GR** *(registry designation = mfr part; sole-class)* | Core | +| 3 | **NW-CO-003** | Angular-contact bearing 7008 (RA pair) | 2 | Bore Ø40 mm [1.575 in]; OD Ø68 mm [2.677 in]; width 15 mm [0.591 in]; DB back-to-back; 15° contact; ABEC-7/P4 | Super-precision angular-contact ball bearing | **SKF 7008 CD/P4A** (universal-match: **7008 CDGA/P4A** for factory DB set); equiv **NSK 7008A5TYNSULP4 / FAG HCB7008-E-T-P4S** | **REMEDIATION — v1 mandatory** | +| 4 | **NW-CO-004** | Angular-contact bearing 7006 (DEC pair) | 2 | Bore Ø30 mm [1.181 in]; OD Ø55 mm [2.165 in]; width 13 mm [0.512 in]; DB back-to-back; 15° contact; ABEC-7/P4 | Super-precision angular-contact ball bearing | **SKF 7006 CD/P4A** (universal-match: **7006 CDGA/P4A**); equiv **NSK 7006A5 / FAG HCB7006** | **REMEDIATION — v1 mandatory** | +| 5 | **NW-CO-005** | On-axis absolute encoder (RA + DEC) | 2 | Resolution < 1″ (on-axis); interface BiSS-C / SSI; **corrects harmonic PE** | Absolute angle ring + readhead | **Renishaw RESOLUTE™ (BiSS-C) readhead + RESA30 rotary angle ring**, 26-bit (0.02″ resolution); equiv **Heidenhain RCN/ECA absolute ring** | **REMEDIATION — v1 mandatory** | +| 6 | **NW-CO-006** | Stepper motor NEMA17 + 27:1 planetary | 2 | Step 1.8°; planetary 27:1; Irun 1.5 A; Igoto 2.0 A; holding 0.45 Nm; 16 microsteps *(SOURCED `P.MOTOR`)* | Hybrid stepper + planetary gearhead | **StepperOnline 17HS19-2004S1 + PG27 gearhead** (equiv Oriental Motor PKP + gearhead) *— representative* | Core | +| 7 | **NW-CO-007** | Motor driver (TMC5160) / OnStepX board | 2 | Irun 1.5 A; Igoto 2.0 A; 16 microsteps *(SOURCED `P.MOTOR`)* | Microstepping stepper driver + controller | **Analog Devices/Trinamic TMC5160** on an **OnStepX** controller (e.g. MaxPCB / FYSETC) *— representative* | Core | +| 8 | **NW-CO-008** | Temperature-compensated focuser | 1 | Temp coeff −2.5 steps/°C *(ASSUMED)*; motorised absolute; compensates Al-tube focus drift over 22 K swing | Motorised absolute focuser w/ temp probe | **Optec TCF-S** (true temp-comp); equiv **ZWO EAF + temperature sensor** or **Pegasus Astro FocusCube v2** *— representative* | **REMEDIATION — v1 mandatory** | +| 9 | **NW-CO-009** | Counterweights (5 kg ×2, 2.5 kg ×1) | 3 | 12.5 kg total available *(SOURCED `P.COUNTERWEIGHTS`)*; fits shaft Ø31.75 mm [1.250 in] (NW-TP-001, BP-02) | Cast-iron mount counterweight | Generic 1.25 in-bore cast-iron CW discs (e.g. ADM / ZWO CW set) *— representative* | **OPTIONAL** (CW-free build omits) | +| 10 | **NW-CO-010** | V-groove track wheels | 8 | Ø~4 in [~101.6 mm]; ≥150 kg [≥331 lb] each; 4 per side; ≥4× on 180 kg roof + snow | Steel V-groove gate/track wheel | **DuraGates 4″ galvanized V-groove wheel** (≤~425 kg [~937 lb]/wheel) or **CasterHQ V-track wheel** *— representative* | Core | +| 11 | **NW-CO-011** | Roof drive (gate operator) | 1 | Move force 235 N [52.8 lbf]; SF 2.1 vs ~500 N [112 lbf] drive; rack-and-pinion/chain; **fail-safe + wind interlock** | Commercial sliding-gate operator | **LiftMaster SL3000UL** (1/2 HP, ≤~454 kg [1000 lb], 15 m travel); equiv **US Automatic Patriot RSL / Nice-Apollo** *— representative* | Core | +| 12 | **NW-CO-012** | Metal roofing panel + flashing | 1 | Area ≈ 9.0 m² [≈96.9 ft²] (3.0 × 3.0 m) *(SOURCED `P.ENCLOSURE`)*; weather skin + ridge/edge flashing + closures | Standing-seam / corrugated steel roofing | 24 ga standing-seam or corrugated galvanized/Galvalume panel + matching flashing/closures *(commodity — representative)* | Core | +| 13 | **NW-CO-013** | 48 V LiFePO4 battery pack + solar | 1 | 48 V bus; ~5 kWh; **13.4 h autonomy** (vs 12 V 3.3 h; night 10 h); DGX ≈59 % of load | 48 V LiFePO4 pack + PV/MPPT + hybrid inverter | **EG4 LifePower4 V2 48 V 100 Ah (5.12 kWh)** rack battery + 48 V hybrid inverter + PV array/MPPT; equiv **Battle Born / SOK** *— representative* | **REMEDIATION — v2 mandatory (v1 grid+UPS)** | +| 14 | **NW-CO-014** | Fastener schedule (SHCS, anchors) | 1 | ASTM A574 SHCS + ASTM F1554 Gr36 anchors *(grades ASSUMED)* | Fastener buy (consolidated) | See **`fastener_schedule.md`** | Core | + +--- + +## B. Per-item procurement notes + +**NW-CO-001 / -002 — Harmonic Drive LLC strain-wave gears (drivetrain core).** +The registry designations `CSF-32-100-2A-GR` and `CSF-25-80-2A-GR` are the **manufacturer part numbers** +(SOURCED from `P.RA_DRIVE.name` / `P.DEC_DRIVE.name`), so the class is effectively **sole-source** to +Harmonic Drive LLC's CSF-2A series (second-source only if a proven equivalent hollow-bore strain-wave +unit of identical ratio/torque/bore is qualified). Confirm the **hollow-bore** variant (Ø80.0 / +Ø64.0 mm) and the output-flange bolt pattern against the RA/DEC spindles (8 × M4 on Ø104 / Ø83 mm PCD, +BP-02). Rated torque governs continuous tracking; peak governs slew/wind gust. + +**NW-CO-003 / -004 — angular-contact bearing pairs (REMEDIATION, v1 mandatory).** +These **replace the baseline deep-groove 6008 / 6006** whose compliance was 98 % of the 43.5″ stiffness +FAIL. Buy **matched pairs mounted back-to-back (DB)** for moment stiffness — either universal-match +(CDGA/P4A, arrange DB in assembly) or a factory-matched DB set. Precision class **≥ ABEC-7 / ISO P4**. +Confirm bore/OD/width against the H7 housing seats (BP-01) and j6 spindle journals (BP-02): 7008 = +Ø40/Ø68/15 mm, 7006 = Ø30/Ø55/13 mm. Specify preload class and DB matching on the CoC. **Do not +substitute deep-groove** — it re-opens the stiffness FAIL. + +**NW-CO-005 — on-axis absolute encoder (REMEDIATION, v1 mandatory).** +Closes the tracking loop **on the axis** so it corrects the harmonic gear's periodic error. Baseline +motor-side-only encoding was DISQUALIFIED at **5.02″ RMS** (5× the 1.0″ gate); the on-axis ring reaches +**0.54″**. Buy an absolute angle ring + readhead with a **BiSS-C (or SSI) serial interface** and +**sub-arcsecond** accuracy/resolution (representative: Renishaw RESOLUTE + RESA30, 26-bit = 0.02″ +resolution). Confirm the **ring bore, readhead standoff, and interface** against the actual RA/DEC axis +geometry and the OnStepX / controller input — these are **ASSUMED, confirm**. One per axis (×2). + +**NW-CO-006 / -007 — steppers + drivers (drivetrain core).** +Ratings are SOURCED (`P.MOTOR`: 1.8° step, 27:1 planetary, Irun 1.5 A, Igoto 2.0 A, holding 0.45 Nm, +16 microsteps); the specific catalogue motor, gearhead, and controller board are **representative, +confirm**. The planetary periodic error is **mooted by the on-axis encoder** (NW-CO-005) — the servo +closes on the absolute ring for position and the motor encoder for velocity. TMC5160 on an OnStepX board +is the reference control stack. + +**NW-CO-008 — temperature-compensated focuser (REMEDIATION, v1 mandatory).** +Passive (fixed) focus walks **~10× depth-of-focus** over the 22 K night swing (thermal proof). Buy a +**motorised absolute focuser with a temperature probe and a compensation curve** (representative: Optec +TCF-S true temp-comp, or ZWO EAF + temp sensor, or Pegasus FocusCube v2). The **−2.5 steps/°C** +coefficient is **ASSUMED** — confirm against the as-built OTA and calibrate. Confirm mechanical coupling +to the OTA drawtube. + +**NW-CO-009 — counterweights (OPTIONAL).** +The selected topology is the **counterweight-FREE GEM** (torque proof SF 2.5/2.6; deletes 15.4 kg + +29 % RA inertia; cost delta −$110). **Buy only if the counterweighted variant is elected.** If bought, +the 12.5 kg set (5 + 5 + 2.5 kg) rides the Ø31.75 mm [1.250 in] shaft NW-TP-001 (BP-02, itself optional). + +**NW-CO-010 — V-groove track wheels (enclosure motion core).** +Eight wheels (4 per side) carry the 180 kg [397 lb] roof + snow on the BP-04 box track (NW-RR-003). +Ø~4 in [~101.6 mm], **≥150 kg [≥331 lb]/wheel** rated (target ≥4× on the loaded roof — representative +DuraGates 4″ carries ~425 kg [~937 lb]/wheel, ample). Confirm the **V-profile matches the rail section** +and the axle bore fits the M16 axle of the wheel brackets (NW-RR-004, BP-04). **ASSUMED, confirm.** + +**NW-CO-011 — roof drive / gate operator (enclosure motion core).** +Enclosure proof: **235 N [52.8 lbf] move force, SF 2.1** vs a ~500 N [112 lbf]-class drive. Buy a +**commercial sliding-gate operator** (rack-and-pinion or chain) rated for the 180 kg [397 lb] roof. +**Must be fail-safe and interlocked with the wind system** (no open above the gust limit; hold closed on +survival wind, coordinated with the NW-RR-006 hold-downs). Representative: LiftMaster SL3000UL. The +operator product, duty rating, and interlock logic are **ASSUMED design-intent — PE/controls confirm.** + +**NW-CO-012 — metal roofing panel + flashing (weather skin core).** +Weatherproof skin over the roll-off frame, ≈9.0 m² [≈96.9 ft²] (SOURCED roof 3.0 × 3.0 m). Panel type +(standing-seam vs corrugated), gauge, and fastening are **ASSUMED** — confirm against the ASCE 7 +snow/wind case and the purlin spacing (NW-RR-002, BP-04). Include **ridge/edge flashing, closures, and +gaskets** for a sealed roll-off joint. + +**NW-CO-013 — 48 V LiFePO4 battery pack + solar (REMEDIATION, v2 mandatory).** +Power proof: a 12 V pack gives only **3.3 h vs the 10 h** night; **48 V gives 13.4 h** (DGX ≈59 % of the +load). Buy a **48 V LiFePO4 pack (~5 kWh) + solar array/MPPT + 48 V hybrid inverter** for off-grid +autonomy. **Mandatory only for the off-grid v2**; **v1 may run grid + UPS** (`MECHANICAL_DESIGN.md` §9 +roadmap). Representative: EG4 LifePower4 V2 48 V 100 Ah (5.12 kWh). Sizing **ASSUMED — confirm** against +the final load schedule and DGX duty cycle. + +**NW-CO-014 — fastener schedule.** +Consolidated ASTM A574 SHCS + ASTM F1554 Gr36 anchor buy across BP-01…BP-04 → see +`fastener_schedule.md`. Grades are **ASSUMED design-intent**; lengths/quantities/torque are **ASSUMED, +bidder/PE to confirm**; structural anchor sizing is a **PE** item. + +--- + +## C. Remediation & option roll-up (buyer checklist) + +| Category | Lines | Rule | +|---|---|---| +| **v1 MANDATORY remediations** | NW-CO-003, NW-CO-004, NW-CO-005, NW-CO-008 | Must be the remediated parts — no baseline substitution without engineering sign-off. | +| **v2 MANDATORY remediation** | NW-CO-013 | Off-grid autonomy only; v1 may run grid + UPS. | +| **Core buy (build-critical)** | NW-CO-001, -002, -006, -007, -010, -011, -012, -014 | Required for the v1 build; representative catalogue numbers to confirm. | +| **OPTIONAL** | NW-CO-009 | Counterweight-FREE build omits; buy only for the counterweighted variant. | +| **5th remediation (elsewhere)** | NW-RR-006 (BP-04) | 4× wind hold-downs — fabricated steel, not BP-05; anchors captured in `fastener_schedule.md`. | + +> All representative catalogue part numbers are **"representative — confirm"**: they fix the class, +> rating, and interface, not a sole source. Confirm availability, lead time, and fit against the fixed +> BP-01…BP-04 interfaces (`README.md` §4) before placing any order; resolve every ASSUMED value via +> RFI first. diff --git a/design/mechanical/tender/packages/BP-05_cots_schedule/fastener_schedule.md b/design/mechanical/tender/packages/BP-05_cots_schedule/fastener_schedule.md new file mode 100644 index 0000000..227bf83 --- /dev/null +++ b/design/mechanical/tender/packages/BP-05_cots_schedule/fastener_schedule.md @@ -0,0 +1,93 @@ +# BP-05 / NW-CO-014 — Consolidated Fastener Schedule (SHCS + Anchors) + +> **ISSUED FOR BID / FOR PE REVIEW — NOT FOR CONSTRUCTION** + +| Field | Value | +|---|---| +| Part | NW-CO-014 — Fastener schedule (consolidated buy across BP-01…BP-04) | +| Governing specs | **ASTM A574** (alloy-steel socket-head cap screws), **ASTM F1554 Gr36** (anchor bolts), **ASTM A123** (galvanize, anchors) | +| Fits | ISO 286 (threaded fits), ISO 2768 (general) | +| Issue date / Rev | 2026-08-06 / A | + +This schedule consolidates the **threaded fasteners** implied by the mating interfaces in the parts +registry (`gen/partspec.py`, `key_dims_mm`) into a single buy. The **fastener grades are ASSUMED +design-intent**; **thread sizes are derived from the fixed interfaces** (SOURCED where a `key_dims` +entry names them); and **lengths, quantities, and torque are ASSUMED — bidder/PE to confirm** against +the as-built grip lengths and the governing structural loads. **Weld sizes and any structural anchor +sizing are PE items** and are NOT fixed here. + +Dual units throughout: mm primary, inch in brackets. + +--- + +## 1. Socket-head cap screws — ASTM A574 (alloy steel, high-strength) + +| Thread | Ø | Where used (interface) | Fixed by | Qty* | Length* | Status | +|---|---|---|---:|---:|---:|---| +| **M4** | Ø4 mm [0.157 in] | RA spindle → CSF-32 output flange, **8 × M4 on Ø104 mm [4.094 in] PCD** | NW-TP-002 / NW-MH-001 (SOURCED pattern) | 8 | ASSUMED | Core | +| **M4** | Ø4 mm [0.157 in] | DEC spindle → CSF-25 output flange, **8 × M4 on Ø83 mm [3.268 in] PCD** | NW-TP-003 / NW-MH-002 (SOURCED pattern) | 8 | ASSUMED | Core | +| **M6** | Ø6 mm [0.236 in] | Pier adapter → RA housing, **4 × M6** (also RA housing corner holes ×4) | NW-PF-003 → NW-MH-001 (SOURCED pattern) | 4 | ASSUMED | Core | +| **M8** | Ø8 mm [0.315 in] | DEC saddle clamp bar, **2 × M8** clamp screws | NW-MH-003 / NW-MH-002 (SOURCED pattern) | 2 | ASSUMED | Core | +| **M12** | Ø12 mm [0.472 in] | Counterweight shaft **stud end M12 × 40** + **M12** safety-stop | NW-TP-001 (SOURCED) | 2 | 40 mm [1.575 in] stud | **OPTIONAL** (CW-free build omits) | +| **M16** | Ø16 mm [0.630 in] | V-groove wheel **axles**, wheel-bracket bores (8 wheels) | NW-RR-004 (ASSUMED) | 8 | ASSUMED | Core *(axle/shoulder bolt — confirm type)* | + +\* **Quantities are the interface pattern counts; add make-up/spares. Lengths are ASSUMED** — set from +the as-built grip (housing wall + flange stack + thread engagement) and confirm. + +**SHCS notes.** +- **Grade.** ASTM A574 (alloy steel, ~property class 12.9 equivalent) is the **ASSUMED** grade per the + standards register. **Galvanic caution (ASSUMED, confirm):** several joints land in **Type III + hardcoat-anodized 6061-T6** (BP-01) and **passivated 303 SS** (BP-02); at those aluminium/stainless + interfaces the PE may elect **stainless SHCS to ISO 3506 A2-70 / A4-70** in lieu of A574 to avoid + galvanic coupling, accepting the lower proof load. Confirm per joint. +- **Torque / preload.** ASSUMED — set per fastener grade and joint (lubricated vs dry), bidder/PE to + specify. The M4 drive-flange screws are the precision-critical joints (they locate the strain-wave + output to the axis) — torque to a controlled value with thread-locker as required. +- **Finish.** Plain/black-oxide A574 for interior joints; where exposed to weather use stainless or a + suitably plated screw — confirm. + +--- + +## 2. Anchor bolts — ASTM F1554 Gr36 (galvanized per ASTM A123) + +| Group | Ø | Embedment / projection | Where used | Fixed by | Qty | Status | +|---|---|---|---|---|---:|---| +| **Pier anchor set** | Ø0.75 in [19.05 mm] | Embedment 304.8 mm [12.000 in]; projection 50.0 mm [1.969 in] | Cast into pier, carries the top plate | **NW-PF-004 (BP-03)** — cast via template | 4 | Core *(BP-03 buy line)* | +| **Wind hold-down anchors** | Ø0.75 in [19.05 mm] | ASSUMED (PE to size) | Clamp roof/enclosure against **9.2 kN survival uplift** | **NW-RR-006 (BP-04)** — 4× hold-down brackets | 4 | **REMEDIATION** *(SF 3.9; each ≥2 klbf [≈8.9 kN])* | + +**Anchor notes.** +- **The pier anchor set (4×) is procured under BP-03 as line NW-PF-004** and cast into the pier via a + setting template; it is repeated here so the F1554 buy can be consolidated. The **wind hold-down + anchors (4×)** serve the **enclosure/building foundation** (the pier is structurally isolated) and + belong to the BP-04 hold-down brackets NW-RR-006 (the **5th** proof-out remediation). +- **PE GATE.** Final anchor **diameter, grade, embedment, edge distance, and count are PE-sized per + ACI 318 Ch.17** for the governing wind/seismic uplift (bounding roof uplift ≈9.2 kN; each hold-down + ≥2 klbf gives SF 3.9). The Ø0.75 in F1554 Gr36 values here are **ASSUMED design-intent — PE to + confirm and stamp** (see BP-03 README §0, the PE-STAMP gate). +- **Finish.** Hot-dip galvanize per **ASTM A123**; supply mill cert / CoC + galvanize coating record + and the anchor proof/pull test per the BP-03 acceptance plan. + +--- + +## 3. Not covered here (flagged) + +- **Weld sizes / NDE** (BP-03 top plate, BP-04 roof frame, rails, brackets) — **ASSUMED design-intent; + bidder/PE to confirm** per **AWS D1.1** (steel) / **AWS D1.2** (aluminium), symbols per **AWS A2.4**, + with WPS/PQR welder qualification. Not a fastener buy. +- **Roofing panel fasteners / closures** (NW-CO-012) — supplied with the roofing system; type/spacing + ASSUMED, confirm to the ASCE 7 snow/wind case. +- **Rail-to-pier / rail-to-building bolting** (NW-RR-003) — structural connection; PE to detail. +- **Leveling nuts, washers, grout, thread-locker** — ASSUMED consumables; bidder to include. + +--- + +## 4. Consolidated buy summary + +| Spec | Sizes | Total pieces (interface count)* | +|---|---|---:| +| ASTM A574 SHCS | M4, M6, M8, (M12 optional), M16 | 8 + 8 + 4 + 2 + (2) + 8 = **32** (+ M12 ×2 optional) | +| ASTM F1554 Gr36 anchors, galv. A123 | Ø0.75 in [19.05 mm] | **8** (4 pier via NW-PF-004 + 4 hold-down via NW-RR-006) | + +\* **Interface counts only — add make-up, spares, and stack washers/nuts.** All lengths, torque, final +grade selection (A574 vs stainless at galvanic joints), and structural anchor sizing are **ASSUMED +design-intent — bidder/PE to confirm.** diff --git a/design/mechanical/tests/__init__.py b/design/mechanical/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/design/mechanical/tests/test_balance.py b/design/mechanical/tests/test_balance.py new file mode 100644 index 0000000..56d304c --- /dev/null +++ b/design/mechanical/tests/test_balance.py @@ -0,0 +1,75 @@ +"""Mass-balance proof: the mount must balance the payload with the available +12.5 kg inside the 18" shaft, AND the counterweight-free option must be shown +viable (inherited from the torque proof), deleting real steel and RA inertia.""" + +import math + +from design.mechanical.calc import balance, torque +from design.mechanical.calc import params as P +from design.mechanical.calc.budget import Verdict + + +def test_required_position_solves_the_moment_equation(): + """r_cw must satisfy m_cw * r_cw = m_payload * r_payload exactly.""" + r_cw = balance.required_counterweight_position_m(P.MN78) + m_pay = P.MN78.mass_kg + P.IMAGING_TRAIN.mass_kg + lhs = P.COUNTERWEIGHTS.weights_available_kg * r_cw + rhs = m_pay * torque.RA_CG_OFFSET_M + assert math.isclose(lhs, rhs, rel_tol=1e-12) + + +def test_counterweight_fits_within_the_shaft(): + """The 12.5 kg must balance the MN78 within the 18" (0.457 m) shaft.""" + r_cw = balance.required_counterweight_position_m(P.MN78) + assert r_cw <= P.COUNTERWEIGHTS.shaft_len_m + # Real seating margin, not right at the tip. + assert P.COUNTERWEIGHTS.shaft_len_m / r_cw >= 1.3 + + +def test_heavier_ota_needs_more_shaft(): + """MN78 (heavier) must need a longer r_cw than MN76 — model sanity.""" + r_light = balance.required_counterweight_position_m(P.MN76) + r_heavy = balance.required_counterweight_position_m(P.MN78) + assert r_heavy > r_light + + +def test_deleted_mass_is_weights_plus_shaft(): + """Counterweight-free deletes both the weights and the 303-SS shaft.""" + shaft = balance.shaft_mass_kg() + deleted = balance.deleted_mass_kg() + assert math.isclose(deleted, P.COUNTERWEIGHTS.weights_available_kg + shaft, rel_tol=1e-12) + # Shaft is real, non-trivial steel (rho*A*L ~ 2.9 kg). + assert 2.0 < shaft < 4.0 + # So total deleted noticeably exceeds the weights alone. + assert deleted > P.COUNTERWEIGHTS.weights_available_kg + 2.0 + + +def test_counterweight_free_cuts_ra_inertia_meaningfully(): + """Removing the counterweight drops a meaningful fraction of RA inertia.""" + del_I = balance.deleted_ra_inertia_kgm2(P.MN78) + i_pay = balance._ra_payload_inertia_kgm2(P.MN78) + assert del_I > 0.0 + reduction = del_I / (i_pay + del_I) + # Deleting ~15 kg of steel at ~0.29 m should cut ~20-40% of RA inertia. + assert 0.20 <= reduction <= 0.40 + + +def test_evaluate_passes_and_ties_to_torque_proof(): + """PASS requires: balances within the shaft AND cw-free viable per torque.""" + r = balance.evaluate("MN78") + assert r.verdict is Verdict.PASS + # safety_factor is the shaft fit factor (>1 => fits). + assert r.safety_factor is not None and r.safety_factor > 1.0 + # The counterweight-free verdict must be inherited, not silently asserted. + tq = torque.evaluate("MN78", counterweight_free=True) + assert tq.verdict in (Verdict.PASS, Verdict.MARGINAL) + assert any("torque" in a.lower() for a in r.assumptions) + assert any("Required shaft position r_cw" in ln.label for ln in r.lines) + + +def test_dec_balance_is_qualitative_and_small(): + """DEC needs no counterweight: residual moment after dovetail adjust is small.""" + r = balance.evaluate("MN78") + residual = next(ln for ln in r.lines if "DEC residual moment" in ln.label) + # Residual imbalance (18 kg x 20 mm ~ 3.5 Nm) is tiny vs the 70 Nm DEC drive. + assert residual.value < 0.1 * P.DEC_DRIVE.rated_torque_Nm diff --git a/design/mechanical/tests/test_bearings.py b/design/mechanical/tests/test_bearings.py new file mode 100644 index 0000000..842cc88 --- /dev/null +++ b/design/mechanical/tests/test_bearings.py @@ -0,0 +1,72 @@ +"""Bearing proof: rolling fatigue (L10) is astronomically long because the mount +barely turns, static safety is comfortable, and the governing finding is that the +'angular contact' 60xx are actually DEEP-GROOVE — angular-contact pairs are needed +for moment stiffness. FAIL is not expected here; the load capacity is a non-issue.""" + +import math + +from design.mechanical.calc import bearings +from design.mechanical.calc import params as P +from design.mechanical.calc.budget import Verdict + + +def test_load_model_adds_weight_and_moment_couple(): + """P = W + M/span; the moment couple must dominate the tiny direct weight.""" + W = 176.5 + lever = 0.20 + span = 0.0762 + P_load = bearings.bearing_radial_load(W, lever, span) + assert math.isclose(P_load, W + (W * lever) / span, rel_tol=1e-12) + # The overturning couple is the real driver, not the direct weight. + assert (W * lever) / span > W + + +def test_l10_life_is_astronomically_large(): + """At ~1 rev/sidereal day, L10 must exceed a million years on both axes.""" + ra = bearings.l10_years(P.BRG_RA_6008, bearings.ra_output_load(P.MN78)) + dec = bearings.l10_years(P.BRG_DEC_6006, bearings.dec_output_load(P.MN78)) + assert ra > 1.0e6 + assert dec > 1.0e6 + + +def test_fatigue_is_not_the_constraint(): + """L10 dwarfs any realistic service life (>> 1000x a 100-year observatory).""" + ra = bearings.l10_years(P.BRG_RA_6008, bearings.ra_output_load(P.MN78)) + assert ra > 1000 * 100 # >> 1000 service lifetimes of 100 years each + + +def test_l10_scales_with_load_cubed(): + """Doubling the load must cut L10 revolutions by ~8x (the (C/P)^3 law).""" + base = bearings.l10_revolutions(P.BRG_RA_6008, 500.0) + doubled = bearings.l10_revolutions(P.BRG_RA_6008, 1000.0) + assert math.isclose(base / doubled, 8.0, rel_tol=1e-9) + + +def test_static_safety_is_comfortable(): + """S0 = C0/P should clear the pass gate with real margin on both axes.""" + ra_S0 = bearings.static_safety(P.BRG_RA_6008, bearings.ra_output_load(P.MN78)) + dec_S0 = bearings.static_safety(P.BRG_DEC_6006, bearings.dec_output_load(P.MN78)) + assert ra_S0 >= bearings.STATIC_SAFETY_PASS + assert dec_S0 >= bearings.STATIC_SAFETY_PASS + # Genuinely comfortable, not marginal. + assert min(ra_S0, dec_S0) > 5.0 + + +def test_evaluate_passes_with_the_angular_contact_finding(): + """Verdict PASS on load capacity, but the deep-groove caveat must be loud.""" + r = bearings.evaluate("MN78") + assert r.verdict is Verdict.PASS + assert r.safety_factor is not None and r.safety_factor >= bearings.STATIC_SAFETY_PASS + text = (r.headline + " " + " ".join(r.assumptions)).lower() + assert "deep-groove" in text + assert "7008" in text and "7006" in text + assert "angular" in text + assert "stiffness" in text + + +def test_load_is_well_below_dynamic_rating(): + """Sanity: modelled loads are a small fraction of the dynamic ratings.""" + ra_load = bearings.ra_output_load(P.MN78) + dec_load = bearings.dec_output_load(P.MN78) + assert ra_load < 0.1 * P.BRG_RA_6008.C_dynamic_N + assert dec_load < 0.1 * P.BRG_DEC_6006.C_dynamic_N diff --git a/design/mechanical/tests/test_cad.py b/design/mechanical/tests/test_cad.py new file mode 100644 index 0000000..2801655 --- /dev/null +++ b/design/mechanical/tests/test_cad.py @@ -0,0 +1,160 @@ +""" +CAD proof: the parametric geometry is driven by params.py (not hand-typed), the +GEM elevation is physically sensible, the SVG previewer actually writes a valid +file, and every OpenSCAD part carries its render command + params.py traceability. +""" + +import math +from pathlib import Path + +from design.mechanical.cad import svg_preview as sp +from design.mechanical.calc import params as P + +CAD_DIR = Path(sp.__file__).resolve().parent +SCAD_FILES = ["ra_housing.scad", "dec_housing.scad", "pier_adapter.scad", + "counterweight_shaft.scad", "assembly.scad"] + + +# -------------------------------------------------------------------------- +# Vector maths of the polar axis. +# -------------------------------------------------------------------------- +def test_polar_axis_is_unit_at_latitude(): + ux, uy = sp.polar_axis_unit(P.SITE.latitude_deg) + assert math.isclose(math.hypot(ux, uy), 1.0, rel_tol=1e-12) + # Altitude of the polar axis equals the site latitude. + assert math.isclose(math.degrees(math.atan2(uy, ux)), P.SITE.latitude_deg, rel_tol=1e-9) + + +def test_perp_vectors_are_orthogonal_to_axis(): + lat = P.SITE.latitude_deg + ax = sp.polar_axis_unit(lat) + for perp in (sp._perp_up(lat), sp._perp_down(lat)): + dot = ax[0] * perp[0] + ax[1] * perp[1] + assert abs(dot) < 1e-12 + # up side points up, down side points down. + assert sp._perp_up(lat)[1] > 0 + assert sp._perp_down(lat)[1] < 0 + + +# -------------------------------------------------------------------------- +# Geometry is sourced from params.py, not hardcoded. +# -------------------------------------------------------------------------- +def test_dimensions_come_from_params(): + g = sp.build_geometry(P.MN78) + d = g["dims"] + assert d["pier_h"] == P.PIER.height_above_m + assert d["pier_d"] == P.PIER.diameter_m + assert d["tube_len"] == P.MN78.tube_length_m + assert d["tube_od"] == P.MN78.tube_od_m + assert d["cw_len"] == P.COUNTERWEIGHTS.shaft_len_m + assert d["cw_dia"] == P.COUNTERWEIGHTS.shaft_dia_m + + +def test_ota_case_changes_geometry(): + """Swapping the OTA case must change the drawn tube — proof it's parametric.""" + assert sp.build_geometry(P.MN76)["dims"]["tube_len"] != \ + sp.build_geometry(P.MN78)["dims"]["tube_len"] + + +# -------------------------------------------------------------------------- +# The elevation is physically sensible. +# -------------------------------------------------------------------------- +def _ymin(poly): + return min(p[1] for p in poly) + + +def _ymax(poly): + return max(p[1] for p in poly) + + +def test_head_sits_above_pier_top(): + g = sp.build_geometry() + pier_top = _ymax(g["parts"]["pier"]) + # RA housing and DEC head are carried above the pier top. + assert _ymin(g["parts"]["ra_housing"]) >= pier_top - 1e-6 + assert g["anchors"]["c_dec"][1] > pier_top + + +def test_ota_reaches_toward_pole_above_dec(): + """OTA points at the pole: its top must clear the DEC head (up the polar axis).""" + g = sp.build_geometry() + assert _ymax(g["parts"]["ota_tube"]) > _ymax(g["parts"]["dec_housing"]) + + +def test_counterweight_hangs_on_the_down_side(): + """CW shaft must sit below the DEC head and on the opposite side of the polar + axis from the OTA (classical GEM). 'Sides' are measured perpendicular to the + inclined axis, not by raw x — both parts shift up the +x polar axis.""" + g = sp.build_geometry() + c_dec = g["anchors"]["c_dec"] + up = sp._perp_up(g["lat"]) # unit vector _|_ axis, OTA side + + def perp_proj(poly): + cx = sum(p[0] for p in poly) / 4.0 - c_dec[0] + cy = sum(p[1] for p in poly) / 4.0 - c_dec[1] + return cx * up[0] + cy * up[1] + + assert _ymin(g["parts"]["cw_shaft"]) < c_dec[1] # hangs below the head + assert perp_proj(g["parts"]["ota_tube"]) > 0 # OTA on the up side + assert perp_proj(g["parts"]["cw_shaft"]) < 0 # CW on the down side + + +# -------------------------------------------------------------------------- +# SVG output: valid, dimensioned with the real numbers, and it writes. +# -------------------------------------------------------------------------- +def test_render_svg_is_valid_and_dimensioned(): + svg = sp.render_svg(P.MN78) + assert svg.startswith("") + assert " 2000 + assert out.read_text(encoding="utf-8").startswith("" in assembly diff --git a/design/mechanical/tests/test_dynamics.py b/design/mechanical/tests/test_dynamics.py new file mode 100644 index 0000000..6c778cd --- /dev/null +++ b/design/mechanical/tests/test_dynamics.py @@ -0,0 +1,69 @@ +"""First-mode dynamics proof: the structural first natural frequency must sit +well above the 10 Hz target and clear of the <2 Hz wind-gust and 1-5 Hz servo +bands. Unlike the static-deflection proof (which FAILS), the light payload keeps +sqrt(k/m) high, so dynamics PASSES -- two different physics, two verdicts.""" + +import math + +from design.mechanical.calc import dynamics +from design.mechanical.calc import params as P +from design.mechanical.calc.budget import Verdict + + +def test_pier_stiffness_is_positive_and_sane(): + """A 12 in x 36 in exposed concrete pier is stiff (tens of MN/m).""" + k = dynamics.pier_lateral_stiffness() + assert 1.0e7 < k < 1.0e8 + + +def test_bounce_mode_clears_target(): + """Translational bounce mode is well above 10 Hz (pier-dominated).""" + f = dynamics.first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=False) + assert f > P.NATURAL_FREQ_TARGET_HZ + + +def test_counterweights_lower_the_frequency(): + """Adding counterweight mass raises m_eff and lowers f_n.""" + f_free = dynamics.first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=False) + f_cw = dynamics.first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=True) + assert f_cw < f_free + # sqrt(m) scaling: 18 -> 30.5 kg should drop f by a factor ~sqrt(30.5/18). + assert math.isclose(f_free / f_cw, math.sqrt(30.5 / 18.0), rel_tol=0.02) + + +def test_rocking_mode_is_the_governing_lower_mode(): + """The overhung rocking mode (short bearing spans) is lower than the bounce + mode and is therefore the physically governing first mode -- yet still >10 Hz.""" + f_rock = dynamics.rocking_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + f_bounce = dynamics.first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=False) + assert f_rock < f_bounce + assert f_rock > P.NATURAL_FREQ_TARGET_HZ + + +def test_governing_mode_is_the_minimum(): + g = dynamics.governing_first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=True) + f_rock = dynamics.rocking_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + f_cw = dynamics.first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=True) + assert math.isclose(g, min(f_rock, f_cw), rel_tol=1e-12) + + +def test_first_mode_clears_wind_and_servo_bands(): + """The governing mode must be well above the 2 Hz wind and 5 Hz servo bands.""" + g = dynamics.governing_first_mode_hz(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM, + counterweighted=True) + assert g > 5.0 * 5.0 # comfortably above the ~5 Hz servo/guide bandwidth + + +def test_evaluate_passes_above_target(): + r = dynamics.evaluate("MN78") + assert r.verdict is Verdict.PASS + assert r.safety_factor is not None and r.safety_factor > 1.5 + # Governing mode line must exist and exceed the target. + gov = [ln for ln in r.lines if ln.label == "Governing first mode"] + assert gov and gov[0].value > P.NATURAL_FREQ_TARGET_HZ diff --git a/design/mechanical/tests/test_enclosure.py b/design/mechanical/tests/test_enclosure.py new file mode 100644 index 0000000..07b06cb --- /dev/null +++ b/design/mechanical/tests/test_enclosure.py @@ -0,0 +1,100 @@ +"""Roll-off roof proof: the drive must overcome rolling friction plus the 35 mph +emergency-close gust (the wind, not the roof weight, sizes it) and traverse within +the open time; the closed flat roof carries ~6x its dead weight in snow, which is +the governing structural load and mandates a snow interlock / sloped shedding roof. +The drive PASSes with a ~2x margin over a garage-door-class opener; snow is the +loud finding.""" + +import math + +from design.mechanical.calc import enclosure +from design.mechanical.calc import params as P +from design.mechanical.calc import units as u +from design.mechanical.calc.budget import Verdict + + +def test_zero_wind_force_is_rolling_only(): + """roof_move_force_N(0) must equal mu * m * g exactly.""" + f0 = enclosure.roof_move_force_N(0.0) + expected = enclosure.ROLLING_MU * P.ENCLOSURE.roof_mass_kg * u.G0 + assert math.isclose(f0, expected, rel_tol=1e-12) + assert math.isclose(f0, 88.26, abs_tol=0.5) + + +def test_wind_adds_and_dominates_the_tractive_load(): + """At the 35 mph gust the wind drag is added and is the majority of the load.""" + gust = P.ENV.wind_gust_close_ms + f0 = enclosure.roof_move_force_N(0.0) + fg = enclosure.roof_move_force_N(gust) + f_wind = fg - f0 + assert fg > f0 + assert math.isclose(f_wind, enclosure.roof_wind_drag_N(gust), rel_tol=1e-12) + # Wind is the bigger half of the tractive force -> the drive is wind-sized. + assert f_wind > f0 + assert 0.55 < f_wind / fg < 0.70 + assert 220.0 < fg < 250.0 + + +def test_wind_drag_scales_with_velocity_squared(): + """Doubling the wind must quadruple the drag term.""" + base = enclosure.roof_wind_drag_N(5.0) + doubled = enclosure.roof_wind_drag_N(10.0) + assert math.isclose(doubled / base, 4.0, rel_tol=1e-9) + + +def test_drive_torque_is_force_times_wheel_radius(): + gust = P.ENV.wind_gust_close_ms + t = enclosure.roof_drive_torque_Nm(gust) + assert math.isclose(t, enclosure.roof_move_force_N(gust) * enclosure.DRIVE_WHEEL_RADIUS_M, rel_tol=1e-12) + assert 10.0 < t < 13.0 + + +def test_required_speed_is_feasible_and_within_timeout(): + """Traversing 3 m in the 45 s open time is a gentle speed, well under the ceiling.""" + v = enclosure.roof_required_speed_ms() + assert math.isclose(v, P.ENCLOSURE.roof_length_m / P.ENCLOSURE.open_time_s, rel_tol=1e-12) + assert 0.0 < v < enclosure.FEASIBLE_ROOF_SPEED_MS + # Nominal open time must clear the motor timeout. + assert P.ENCLOSURE.open_time_s < P.ENCLOSURE.motor_timeout_s + + +def test_snow_load_matches_pressure_times_area_and_dwarfs_dead_weight(): + """Closed-roof snow = ground snow pressure * plan area, ~6x the roof weight.""" + snow = enclosure.roof_snow_load_N() + assert math.isclose(snow, P.ENV.ground_snow_load_Pa * enclosure.roof_area_m2(), rel_tol=1e-12) + assert 10_000.0 < snow < 11_500.0 + roof_weight = u.weight_N(P.ENCLOSURE.roof_mass_kg) + assert snow > 5.0 * roof_weight # snow dominates the structure + assert snow / roof_weight > 6.0 + + +def test_snow_laden_roof_cannot_be_driven(): + """A fully snow-laden roof needs more tractive force than the drive can give -> + it must never be commanded open under snow (interlock).""" + f_snow = enclosure.snow_laden_rolling_force_N() + assert f_snow > enclosure.DRIVE_FORCE_CAP_N + # And it is far larger than the dry-roof rolling force. + assert f_snow > 5.0 * enclosure.roof_move_force_N(0.0) + + +def test_evaluate_drive_passes_with_modest_margin_and_flags_snow(): + r = enclosure.evaluate() + # Drive clears rolling + the 35 mph gust, but only ~2x over a garage-door opener. + assert r.verdict in (Verdict.PASS, Verdict.MARGINAL) + assert r.safety_factor is not None and 1.8 < r.safety_factor < 2.6 + text = (r.headline + " " + " ".join(r.assumptions)).lower() + assert "snow" in text + assert "sloped" in text or "shed" in text + assert "interlock" in text + assert "wind" in text + # The key comparison lines are present. + assert any("Total tractive force" in ln.label for ln in r.lines) + assert any("snow load" in ln.label.lower() for ln in r.lines) + + +def test_evaluate_headline_reports_wind_dominance(): + """The finding that wind (not roof weight) sizes the drive must be stated.""" + r = enclosure.evaluate() + assert any("Wind fraction" in ln.label for ln in r.lines) + wind_line = next(ln for ln in r.lines if "Wind fraction" in ln.label) + assert wind_line.value > 50.0 diff --git a/design/mechanical/tests/test_encoder.py b/design/mechanical/tests/test_encoder.py new file mode 100644 index 0000000..e7d944a --- /dev/null +++ b/design/mechanical/tests/test_encoder.py @@ -0,0 +1,93 @@ +"""Tracking-error budget proof: the as-specified encoder chain must be shown to +MISS < 1 arcsec RMS, and the on-axis absolute ring to reach it. FAIL for the +baseline is the correct, intended result.""" + +import math + +from design.mechanical.calc import encoder +from design.mechanical.calc import params as P +from design.mechanical.calc.budget import Verdict + + +def test_quantization_rms_is_lsb_over_sqrt12(): + for enc in (P.ENC_MOTOR_AMT103, P.ENC_AXIS_AS5600, P.ENC_AXIS_RESA): + assert math.isclose( + encoder.quant_rms_arcsec(enc), + enc.resolution_arcsec / math.sqrt(12.0), + rel_tol=1e-12, + ) + + +def test_as5600_on_axis_quantization_is_about_316_arcsec(): + """The cheap 12-bit on-axis chip resolves only ~316" — a coarse homing + reference, ~91" quant RMS, useless for sub-arcsec tracking.""" + res = P.ENC_AXIS_AS5600.resolution_arcsec + assert 300.0 < res < 330.0, f"AS5600 resolution {res:.1f}\" not ~316\"" + assert math.isclose(res, 316.4, abs_tol=1.0) + # Its quantisation RMS alone dwarfs the whole 1 arcsec budget. + assert encoder.quant_rms_arcsec(P.ENC_AXIS_AS5600) > 50.0 + + +def test_motor_and_resa_resolutions_bracket_the_problem(): + assert math.isclose(P.ENC_MOTOR_AMT103.resolution_arcsec, 1.58, abs_tol=0.05) + assert P.ENC_AXIS_RESA.resolution_arcsec < 0.1 # genuinely sub-arcsec + + +def test_baseline_exceeds_one_arcsec_and_fails(): + base = encoder.tracking_rms_arcsec("baseline") + assert base > 1.0, f"baseline should miss the target, got {base:.2f}\"" + # Sanity on the dominant terms: harmonic PE + flexure, not quantisation. + c = encoder.tracking_components("baseline") + assert c["pe_residual"] > c["quant"] + assert c["flexure"] > c["quant"] + # Ballpark the RSS so the model can't silently drift. + assert 4.0 < base < 6.0 + + +def test_proposed_meets_sub_arcsecond(): + prop = encoder.tracking_rms_arcsec("proposed") + assert prop < 1.0, f"proposed on-axis ring should meet target, got {prop:.2f}\"" + # RESA quantisation must be negligible vs the servo/flexure floor. + c = encoder.tracking_components("proposed") + assert c["quant"] < 0.01 + assert prop < 0.9 # real margin below 1 arcsec + + +def test_proposed_beats_baseline_by_a_wide_margin(): + assert encoder.tracking_rms_arcsec("baseline") > \ + 5.0 * encoder.tracking_rms_arcsec("proposed") + + +def test_evaluate_baseline_verdict_is_fail(): + r = encoder.evaluate("baseline") + assert r.verdict is Verdict.FAIL + assert r.safety_factor is not None and r.safety_factor < 1.0 + + +def test_evaluate_proposed_verdict_passes(): + r = encoder.evaluate("proposed") + assert r.verdict is Verdict.PASS + assert r.safety_factor is not None and r.safety_factor > 1.0 + + +def test_evaluate_compare_reports_both_and_flags_the_requirement(): + r = encoder.evaluate() # default: compare + # As-specified design fails, so the module's verdict is FAIL. + assert r.verdict is Verdict.FAIL + # Headline names both numbers and the bold conclusion. + assert "REQUIRED" in r.headline + assert "FAIL" in r.headline.upper() + # Both totals are present as lines. + labels = [ln.label for ln in r.lines] + assert any("Baseline total tracking RMS" in x for x in labels) + assert any("Proposed total tracking RMS" in x for x in labels) + # Assumptions are stated (PE residual + flexure at minimum). + joined = " ".join(r.assumptions).lower() + assert "pe" in joined or "periodic" in joined or "repeatability" in joined + assert "flexure" in joined + + +def test_target_comes_from_params_not_hardcoded(): + assert P.TRACKING_RMS_TARGET_ARCSEC == 1.0 + r = encoder.evaluate() + assert str(P.TRACKING_RMS_TARGET_ARCSEC) in r.target or "1.0" in r.target diff --git a/design/mechanical/tests/test_pier.py b/design/mechanical/tests/test_pier.py new file mode 100644 index 0000000..89498c3 --- /dev/null +++ b/design/mechanical/tests/test_pier.py @@ -0,0 +1,114 @@ +"""Pier / foundation proof: a squat 12"x36" concrete pier is stiff, high in +frequency, and stable -- all four checks PASS -- but seismic overturning relies +on soil embedment (dead weight ALONE would not resist it), the one result worth +stating plainly.""" + +import math + +from design.mechanical.calc import dynamics, pier, wind +from design.mechanical.calc import params as P +from design.mechanical.calc import units as u +from design.mechanical.calc.budget import Verdict + + +def test_pier_stiffness_matches_dynamics_single_source(): + """pier.py and dynamics.py must compute the identical pier stiffness.""" + assert math.isclose(pier.pier_lateral_stiffness_N_per_m(), + dynamics.pier_lateral_stiffness(), rel_tol=1e-12) + + +def test_pier_stiffness_is_tens_of_MN_per_m(): + k = pier.pier_lateral_stiffness_N_per_m() + assert 1.0e7 < k < 1.0e8 + + +def test_tilt_is_a_small_fraction_of_pointing_budget(): + """Operational-wind pier tilt is well under an arcsec -- << the 5" budget.""" + f = wind.drag_force_N(P.ENV.wind_gust_close_ms, P.MN78) + tilt = pier.pier_tilt_arcsec(f) + assert 0.0 < tilt < 1.0 + assert P.DEFLECTION_TARGET_ARCSEC / tilt > 5.0 + + +def test_tilt_uses_conservative_tip_slope(): + """pier_tilt_arcsec is the tip slope (F L^2/2EI) = 1.5x the delta/L estimate.""" + f = 50.0 + delta = pier.pier_tip_deflection_m(f) + naive = u.rad_to_arcsec(delta / P.PIER.height_above_m) # delta/L + slope = pier.pier_tilt_arcsec(f) + assert math.isclose(slope / naive, 1.5, rel_tol=1e-9) + + +def test_shaft_mass_derived_from_geometry(): + """Counterweight shaft mass comes from its 303-SS geometry, ~3 kg.""" + m = pier.counterweight_shaft_mass_kg() + assert 2.0 < m < 4.0 + + +def test_first_mode_far_above_target(): + """Pier substructure first mode is hundreds of Hz -- pier is not the soft element.""" + m_tip = pier.tip_mass_kg(P.MN78, counterweighted=True) + f = pier.pier_first_mode_hz(m_tip) + assert f > 5.0 * P.NATURAL_FREQ_TARGET_HZ + # Heavier tip mass lowers frequency (sqrt scaling). + f_light = pier.pier_first_mode_hz(pier.tip_mass_kg(P.MN78, counterweighted=False)) + assert f_light > f + + +def test_seismic_base_shear_scales_with_sds_and_weight(): + V = pier.seismic_base_shear_N(200.0) + assert math.isclose(V, P.ENV.seismic_sds_g * u.weight_N(200.0), rel_tol=1e-12) + assert math.isclose(pier.seismic_base_shear_N(400.0) / V, 2.0, rel_tol=1e-9) + + +def test_embedment_is_what_makes_seismic_stable(): + """Honest finding: dead weight ALONE cannot resist overturning (SF < 1); + soil passive resistance over the embedment is what provides stability.""" + M_ot = pier.seismic_overturning_Nm(P.MN78) + Pp = pier.soil_passive_resultant_N() + D = P.PIER.embed_depth_m + W_total = u.weight_N(pier.pier_total_mass_kg() + pier.tip_mass_kg(P.MN78)) + M_resist_weight = W_total * (P.PIER.diameter_m / 2.0) + M_resist_passive = Pp * (2.0 * D / 3.0) + # Dead weight alone fails; embedment brings it comfortably over 2. + assert M_resist_weight / M_ot < 1.0 + assert (M_resist_passive + M_resist_weight) / M_ot > 2.0 + + +def test_sliding_resisted_by_passive_soil(): + """Passive soil resistance dwarfs the seismic base shear.""" + m_tip = pier.tip_mass_kg(P.MN78) + V = pier.seismic_base_shear_N(m_tip + pier.pier_exposed_mass_kg()) + assert pier.soil_passive_resultant_N() / V > 2.0 + + +def test_concrete_stress_far_below_capacity(): + """Axial + flexural stresses are tiny vs f'c and the modulus of rupture.""" + I = pier.pier_second_moment_m4() + A = pier.pier_area_m2() + M = pier.seismic_overturning_Nm(P.MN78) + sigma_bend = M * (P.PIER.diameter_m / 2.0) / I + sigma_axial = u.weight_N(pier.tip_mass_kg(P.MN78)) / A + # No net tension cracking: bending tension stays under modulus of rupture. + assert (sigma_bend - sigma_axial) < pier.modulus_of_rupture_Pa() + # Compression nowhere near f'c. + assert (sigma_bend + sigma_axial) < 0.05 * P.PIER.fc_Pa + + +def test_frost_embedment_adequate(): + """0.914 m embedment exceeds the assumed central-NV frost line.""" + assert P.PIER.embed_depth_m > pier.FROST_DEPTH_TYPICAL_M + + +def test_evaluate_passes_with_seismic_governing(): + r = pier.evaluate("MN78") + assert r.verdict is Verdict.PASS + assert r.safety_factor is not None and r.safety_factor > 2.0 + # The governing (minimum) SF should be a seismic term, not tilt/frequency + # (those are enormous), so the governing SF is well under the frequency SF. + m_tip = pier.tip_mass_kg(P.MN78) + sf_freq = pier.pier_first_mode_hz(m_tip) / P.NATURAL_FREQ_TARGET_HZ + assert r.safety_factor < sf_freq + assert any("Governing safety factor" in ln.label for ln in r.lines) + # Honesty: assumptions disclose the ASSUMED seismic S_DS and soil. + assert any("S_DS" in a for a in r.assumptions) diff --git a/design/mechanical/tests/test_power.py b/design/mechanical/tests/test_power.py new file mode 100644 index 0000000..1245e2c --- /dev/null +++ b/design/mechanical/tests/test_power.py @@ -0,0 +1,104 @@ +"""Night-time energy budget proof: the load is DGX-dominated (~60%), the UPS covers +graceful shutdown with a huge margin but only ~5% of a night, and the specified +400 W / 100 Ah solar+battery gives only ~3.3 h autonomy at 12 V -> it FAILS a 10 h +winter night. A 48 V pack (4x energy) or duty-cycling the DGX fixes it. FAIL on +off-grid autonomy is the honest, important result.""" + +import math + +from design.mechanical.calc import params as P +from design.mechanical.calc import power +from design.mechanical.calc.budget import Verdict + + +def test_mount_power_uses_params_currents_and_axis_count(): + """Mount draw = V_bus * I * n_axes, with IGOTO > IRUN.""" + idle = power.mount_power_W(slewing=False) + slew = power.mount_power_W(slewing=True) + assert math.isclose(idle, power.MOTOR_BUS_V * P.MOTOR.irun_A * power.N_AXIS_MOTORS, rel_tol=1e-12) + assert math.isclose(slew, power.MOTOR_BUS_V * P.MOTOR.igoto_A * power.N_AXIS_MOTORS, rel_tol=1e-12) + assert slew > idle + assert math.isclose(idle, 36.0, abs_tol=1e-9) + + +def test_night_load_sums_all_stated_loads(): + """night_load_W must equal the sum of the itemised breakdown, ~287 W.""" + load = power.night_load_W() + assert math.isclose(load, sum(power.load_breakdown_W().values()), rel_tol=1e-12) + assert math.isclose(load, 287.0, abs_tol=1.0) + # Slewing swaps mount idle->goto, raising the load a little. + assert power.night_load_W(slewing=True) > load + + +def test_dgx_dominates_the_load(): + """The assumed 170 W DGX Spark is the majority of the night load.""" + load = power.night_load_W() + assert power.DGX_SPARK_W / load > 0.5 + # Without the DGX the load more than halves. + assert (load - power.DGX_SPARK_W) < power.DGX_SPARK_W + + +def test_ups_covers_shutdown_with_large_margin_but_not_the_night(): + """UPS rides park+close with >5x margin, yet holds only a small % of a night.""" + margin = power.ups_shutdown_margin() + assert margin > 5.0 + assert math.isclose( + margin, + power.UPS_USABLE_WH / (power.night_load_W() * power.SHUTDOWN_TIME_MIN / 60.0), + rel_tol=1e-12, + ) + # The UPS cannot ride a full winter night. + night_need_wh = power.night_load_W() * power.WINTER_NIGHT_HR + assert 0.15 * night_need_wh > power.UPS_USABLE_WH + # Its runtime at the night load is consistent with the ~30 min spec. + assert 25.0 < power.ups_runtime_min() < 40.0 + + +def test_solar_autonomy_fails_the_night_at_12v_but_passes_at_48v(): + """The decisive, unspecified parameter is pack voltage.""" + a12 = power.solar_autonomy_hours(12.0) + a48 = power.solar_autonomy_hours(48.0) + assert 3.0 < a12 < 3.7 + assert a12 < power.WINTER_NIGHT_HR # 12 V pack FAILS a 10 h night + assert a48 > power.WINTER_NIGHT_HR # 48 V pack covers it + # 48 V holds exactly 4x the energy of 12 V. + assert math.isclose(a48 / a12, 4.0, rel_tol=1e-9) + + +def test_battery_energy_scales_with_pack_voltage(): + assert math.isclose(power.battery_usable_wh(12.0), 100.0 * 12.0 * power.BATTERY_USABLE_FRAC, rel_tol=1e-12) + assert math.isclose(power.battery_usable_wh(48.0), 4.0 * power.battery_usable_wh(12.0), rel_tol=1e-12) + + +def test_winter_harvest_is_less_than_a_nights_energy(): + """The 400 W array cannot even harvest one night's worth of energy per day -> + not self-sustaining off-grid with the DGX-dominated load.""" + harvest = power.solar_daily_harvest_wh() + night_need = power.night_load_W() * power.WINTER_NIGHT_HR + assert harvest < night_need + assert 0.3 < harvest / night_need < 0.6 + + +def test_dgx_thermal_needs_ventilation(): + """170 W at a natural ~2 ACH raises the enclosure by many kelvin -> ventilate.""" + dT = power.dgx_ventilation_dT() + assert dT > 8.0 # far above a benign rise + # More air changes cut the rise (inverse in ACH). + assert power.dgx_ventilation_dT(ach=10.0) < dT + assert math.isclose(power.dgx_ventilation_dT(ach=10.0) * 10.0, + power.dgx_ventilation_dT(ach=2.0) * 2.0, rel_tol=1e-9) + + +def test_evaluate_fails_autonomy_and_names_the_culprit(): + r = power.evaluate() + # Off-grid autonomy on the specified 12 V pack does NOT hold the night. + assert r.verdict is Verdict.FAIL + assert r.safety_factor is not None and r.safety_factor < 1.0 + text = (r.headline + " " + " ".join(r.assumptions)).lower() + assert "dgx" in text + assert "48" in text # the 48 V fix is called out + assert "autonomy" in text + # Load, UPS and solar lines are all present. + assert any("Night load (steady)" in ln.label for ln in r.lines) + assert any("UPS shutdown margin" in ln.label for ln in r.lines) + assert any("Autonomy @ 12 V" in ln.label for ln in r.lines) diff --git a/design/mechanical/tests/test_report.py b/design/mechanical/tests/test_report.py new file mode 100644 index 0000000..d6372b8 --- /dev/null +++ b/design/mechanical/tests/test_report.py @@ -0,0 +1,35 @@ +""" +Single-source-of-truth guard: the committed MECHANICAL_DESIGN.md must equal what +the calculator generates right now. If a proof's number changes, the report must +be regenerated (python3 -m design.mechanical.calc.report) or this fails — so the +report can never silently disagree with the code the way the repo's docs do today. +""" + +from design.mechanical.calc import report + + +def test_report_matches_calculator(): + generated = report.build_markdown() + committed = report.OUT.read_text() + assert generated == committed, ( + "MECHANICAL_DESIGN.md is stale — regenerate with " + "`python3 -m design.mechanical.calc.report`." + ) + + +def test_every_proof_headline_present(): + """Each proof's computed headline must actually appear in the report.""" + md = report.OUT.read_text() + for _num, mod in report.PROOFS: + r = mod.evaluate() + assert r.headline in md, f"{r.key} headline missing from report" + + +def test_verdict_counts_are_honest(): + """The at-a-glance summary must reflect the real verdict tally (6 PASS / 5 FAIL).""" + from design.mechanical.calc.budget import Verdict + results = [mod.evaluate() for _num, mod in report.PROOFS] + passes = sum(1 for r in results if r.verdict is Verdict.PASS) + fails = sum(1 for r in results if r.verdict is Verdict.FAIL) + assert passes == 6 and fails == 5, f"verdict tally changed: {passes} PASS / {fails} FAIL" + assert f"**{passes} PASS" in report.OUT.read_text() diff --git a/design/mechanical/tests/test_stiffness.py b/design/mechanical/tests/test_stiffness.py new file mode 100644 index 0000000..a560f53 --- /dev/null +++ b/design/mechanical/tests/test_stiffness.py @@ -0,0 +1,77 @@ +"""Static-deflection proof: the mount head must hold the optical axis within the +5 arcsec pointing target under gravity. This is where the repo's asserted +'< 5 arcsec' is finally computed -- and it FAILS, governed by the short-span +deep-groove bearings, which is the finding worth surfacing.""" + +import math + +from design.mechanical.calc import params as P +from design.mechanical.calc import stiffness +from design.mechanical.calc.budget import Verdict + + +def test_box_section_I_thicker_wall_is_stiffer(): + """12 mm walls give a larger second moment than 8 mm walls.""" + assert stiffness.box_section_I(P.RA_HOUSING_12MM) > stiffness.box_section_I(P.RA_HOUSING) + assert stiffness.box_section_I(P.DEC_HOUSING_12MM) > stiffness.box_section_I(P.DEC_HOUSING) + + +def test_8mm_baseline_worse_than_12mm(): + """The bold 12 mm variant must deflect less than the 8 mm baseline.""" + d8 = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + d12 = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + assert d8 > d12 + + +def test_both_variants_fail_5_arcsec_target(): + """Honest finding: neither wall thickness meets 5 arcsec -- both are ~8-9x over.""" + d8 = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + d12 = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + assert d8 > P.DEFLECTION_TARGET_ARCSEC + assert d12 > P.DEFLECTION_TARGET_ARCSEC + # Both blow the budget by a large factor (bearing-dominated), not marginally. + assert d8 > 8.0 * P.DEFLECTION_TARGET_ARCSEC + assert d12 > 8.0 * P.DEFLECTION_TARGET_ARCSEC + + +def test_bearing_compliance_dominates(): + """The short 63.5/76.2 mm bearing spans, not the beams, govern the deflection.""" + b = stiffness.deflection_breakdown(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + assert b["bearing_rad"] > b["beam_rad"] + # Bearings are the overwhelming majority of the error budget. + assert b["bearing_rad"] / b["total_rad"] > 0.90 + # And within the bearings, the softer/short-span DEC (6006) dominates the RA (6008). + assert b["dec_bearing_rad"] > b["ra_bearing_rad"] + + +def test_12mm_only_helps_the_beam_term(): + """Thicker walls stiffen the beams but not the bearings, so the win is tiny.""" + b8 = stiffness.deflection_breakdown(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + b12 = stiffness.deflection_breakdown(P.MN78, P.RA_HOUSING_12MM, P.DEC_HOUSING_12MM) + # Bearing terms are identical between wall thicknesses. + assert math.isclose(b8["bearing_rad"], b12["bearing_rad"], rel_tol=1e-9) + # Beam term improves, but the total barely moves (< 1 arcsec). + assert b12["beam_rad"] < b8["beam_rad"] + assert (b8["total_arcsec"] - b12["total_arcsec"]) < 1.0 + + +def test_heavier_ota_deflects_more(): + """MN78 (18 kg payload, longer lever) must deflect more than MN76.""" + d_light = stiffness.total_deflection_arcsec(P.MN76, P.RA_HOUSING, P.DEC_HOUSING) + d_heavy = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + assert d_heavy > d_light + + +def test_total_helper_matches_breakdown(): + b = stiffness.deflection_breakdown(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + d = stiffness.total_deflection_arcsec(P.MN78, P.RA_HOUSING, P.DEC_HOUSING) + assert math.isclose(d, b["total_arcsec"], rel_tol=1e-12) + + +def test_evaluate_reports_fail_with_sf_below_one(): + r = stiffness.evaluate("MN78") + assert r.verdict is Verdict.FAIL + assert r.safety_factor is not None and r.safety_factor < 1.0 + assert any("TOTAL deflection" in ln.label for ln in r.lines) + # The assumptions must disclose the ASSUMED bearing stiffness (honesty check). + assert any("N/um" in a for a in r.assumptions) diff --git a/design/mechanical/tests/test_tender.py b/design/mechanical/tests/test_tender.py new file mode 100644 index 0000000..605dbbe --- /dev/null +++ b/design/mechanical/tests/test_tender.py @@ -0,0 +1,78 @@ +""" +Tender consistency gates — the single-source-of-truth guard for the vendor +package. Every fabricated part must be fully specified, the BOM/cut-list/COTS +splits must be internally consistent, the proof-out remediations must be present +(and the FAILed baseline absent), and every fabricated part must have a +well-formed control drawing carrying the correct status. +""" + +from __future__ import annotations + +import csv +import xml.dom.minidom as minidom + +from design.mechanical.tender.gen import bom, drawing +from design.mechanical.tender.gen import partspec as ps + + +def test_every_fabricated_part_is_fully_specified(): + for p in ps.fabricated_parts(): + assert p.material and p.material != "n-a", f"{p.part_no} missing material" + assert p.finish, f"{p.part_no} missing finish" + assert p.tolerance, f"{p.part_no} missing tolerance" + assert p.qty >= 1, f"{p.part_no} bad qty" + assert p.provenance, f"{p.part_no} missing provenance" + assert p.stock_mass_kg() is not None, f"{p.part_no} has no computable stock mass" + assert p.key_dims_mm, f"{p.part_no} has no key dimensions" + + +def test_bom_covers_every_part(): + bom.generate_all() + rows = list(csv.DictReader((bom.OUT / "master_bom.csv").open())) + part_nos = {r["part_no"] for r in rows} + assert part_nos == {p.part_no for p in ps.ALL_PARTS} + assert len(rows) == len(ps.ALL_PARTS) + + +def test_cut_list_and_cots_split_is_consistent(): + bom.generate_all() + cut = list(csv.DictReader((bom.OUT / "cut_list.csv").open())) + cots = list(csv.DictReader((bom.OUT / "cots_schedule.csv").open())) + assert len(cut) == sum(1 for p in ps.ALL_PARTS if p.stock[0] != "cots") + assert len(cots) == sum(1 for p in ps.ALL_PARTS if p.stock[0] == "cots") + # no COTS item leaks into the cut list and vice-versa + cut_nos = {r["part_no"] for r in cut} + cots_nos = {r["part_no"] for r in cots} + assert cut_nos.isdisjoint(cots_nos) + + +def test_remediations_present_and_baseline_absent(): + names = " ".join(p.name for p in ps.ALL_PARTS) + # the five proof-out remediations must appear as real line items + assert "7008" in names and "7006" in names, "angular-contact bearing remediation missing" + assert "absolute encoder" in names.lower(), "on-axis encoder remediation missing" + assert "hold-down" in names.lower(), "wind hold-down anchor remediation missing" + assert "focuser" in names.lower(), "temp-comp focuser remediation missing" + assert "48 v" in names.lower() or "48v" in names.lower(), "48 V power remediation missing" + # No COTS bearing may SELECT the deep-groove baseline (it may only be named as + # the thing being replaced, inside provenance text). + bearing_selections = [p.name for p in ps.ALL_PARTS if "bearing" in p.name.lower()] + for n in bearing_selections: + assert "6008" not in n and "6006" not in n, f"deep-groove baseline still selected: {n}" + + +def test_every_fabricated_part_has_a_wellformed_drawing(): + drawing.generate_all() + for p in ps.fabricated_parts(): + svg = drawing.OUT / f"{p.part_no}.svg" + assert svg.exists(), f"missing drawing for {p.part_no}" + text = svg.read_text() + minidom.parseString(text) # raises if not well-formed XML + assert p.part_no in text + assert "ISSUED FOR BID" in text + assert "NOT FOR CONSTRUCTION" in text + + +def test_pier_and_roof_are_flagged_pe_gate(): + pier = next(p for p in ps.ALL_PARTS if p.part_no == "NW-PF-001") + assert "PE STAMP" in pier.notes.upper() diff --git a/design/mechanical/tests/test_thermal.py b/design/mechanical/tests/test_thermal.py new file mode 100644 index 0000000..90cfaf5 --- /dev/null +++ b/design/mechanical/tests/test_thermal.py @@ -0,0 +1,107 @@ +"""Thermal focus-stability proof: the f/8 has a larger depth of focus than the +f/6 (a point in its favour), but the 6061-T6 tube walks the focus by many depths +of focus across the diurnal swing -> passive focus FAILS and a temperature- +compensated focuser is required. The Astrositall mirror term is negligible and a +sub-2 W dew heater suffices. FAIL (for passive focus) is the honest verdict.""" + +import math + +from design.mechanical.calc import params as P +from design.mechanical.calc import thermal +from design.mechanical.calc.budget import Verdict + + +def test_depth_of_focus_f8_larger_than_f6(): + """DoF = 2*lambda*N^2 -> the slower f/8 tolerates more axial error than f/6.""" + dof_f6 = thermal.depth_of_focus_um(P.MN76) + dof_f8 = thermal.depth_of_focus_um(P.MN78) + assert dof_f8 > dof_f6 + # Scales as N^2: ratio must match (8/6)^2 within tight tolerance. + assert math.isclose(dof_f8 / dof_f6, (8.0 / 6.0) ** 2, rel_tol=1e-9) + + +def test_depth_of_focus_formula_values(): + """Absolute check against 2*lambda*N^2 in micrometres.""" + lam = P.OPTICAL.wavelength_m + assert math.isclose(thermal.depth_of_focus_um(P.MN76), 2 * lam * 36 * 1e6, rel_tol=1e-9) + assert math.isclose(thermal.depth_of_focus_um(P.MN78), 2 * lam * 64 * 1e6, rel_tol=1e-9) + # ~40 um at f/6, ~70 um at f/8. + assert 39.0 < thermal.depth_of_focus_um(P.MN76) < 40.0 + assert 70.0 < thermal.depth_of_focus_um(P.MN78) < 71.0 + + +def test_tube_defocus_per_kelvin_uses_aluminium_cte_and_tube_length(): + per_k = thermal.defocus_per_kelvin_um(P.MN78) + cte = P.MATERIALS["6061-T6"].cte + assert math.isclose(per_k, cte * P.MN78.tube_length_m * 1e6, rel_tol=1e-12) + # Longer MN78 tube expands more per K than the shorter MN76 tube. + assert thermal.defocus_per_kelvin_um(P.MN78) > thermal.defocus_per_kelvin_um(P.MN76) + + +def test_tube_defocus_exceeds_dof_within_diurnal_swing(): + """The governing claim: within the diurnal swing the aluminium tube walks the + focus past the depth of focus, for BOTH tubes (f/8's larger DoF still loses).""" + swing = P.ENV.diurnal_swing_c + for ota in (P.MN76, P.MN78): + defocus_over_swing = thermal.tube_defocus_um(ota, swing) + assert defocus_over_swing > thermal.depth_of_focus_um(ota) + # Equivalent statement: fewer kelvin to defocus than the swing spans. + assert thermal.kelvin_to_defocus(ota) < swing + + +def test_only_a_couple_kelvin_before_out_of_focus(): + """K-to-defocus is only ~2 K on both tubes -> passive focus is fragile.""" + for ota in (P.MN76, P.MN78): + k_edge = thermal.kelvin_to_defocus(ota) + assert 1.5 < k_edge < 3.0 + assert k_edge < thermal.DEW_MARGIN_K # even a small dew swing defocuses it + + +def test_tube_walks_many_depths_of_focus_over_swing(): + """Over the full ~22 K swing the tube should walk ~9-11 depths of focus.""" + swing = P.ENV.diurnal_swing_c + for ota in (P.MN76, P.MN78): + n_dof = thermal.tube_defocus_um(ota, swing) / thermal.depth_of_focus_um(ota) + assert n_dof > 5.0 # comfortably more than a single DoF + assert 8.0 < n_dof < 12.0 + + +def test_astrositall_mirror_term_is_negligible(): + """The low-expansion primary contributes ~2 orders of magnitude less than the + aluminium tube; on its own the mirror stays well inside the depth of focus.""" + for ota in (P.MN76, P.MN78): + mir_per_k = thermal.mirror_defocus_per_kelvin_um(ota) + tube_per_k = thermal.defocus_per_kelvin_um(ota) + assert mir_per_k < 0.01 * tube_per_k # >100x smaller + # Mirror-only focus shift over the whole swing stays within the DoF. + assert mir_per_k * P.ENV.diurnal_swing_c < thermal.depth_of_focus_um(ota) + + +def test_dew_heater_fits_within_the_all_sky_ring(): + """Holding the corrector a few K above dewpoint costs < the 2 W ring.""" + for ota in (P.MN76, P.MN78): + w = thermal.dew_heater_power_w(ota) + assert 0.0 < w < thermal.ALL_SKY_RING_W + # P = h*A*dT relationship holds. + w = thermal.dew_heater_power_w(P.MN78, h=10.0, dT=5.0) + assert math.isclose(w, 10.0 * thermal.corrector_area_m2(P.MN78) * 5.0, rel_tol=1e-12) + + +def test_evaluate_fails_passive_focus_and_flags_the_fix(): + r = thermal.evaluate("MN78") + # Passive focus does NOT hold the swing -> honest FAIL. + assert r.verdict is Verdict.FAIL + assert r.safety_factor is not None and r.safety_factor < 1.0 + text = (r.headline + " " + " ".join(r.assumptions)).lower() + assert "compensat" in text # temperature-compensated focuser + assert "required" in text + assert "astrositall" in text + assert "dew" in text or "heater" in text + # The comparison lines must be present. + assert any("Depth of focus" in ln.label for ln in r.lines) + assert any("per K" in ln.label for ln in r.lines) + + +def test_evaluate_both_ota_keys_fail_passive(): + for key in ("MN76", "MN78"): + assert thermal.evaluate(key).verdict is Verdict.FAIL diff --git a/design/mechanical/tests/test_torque.py b/design/mechanical/tests/test_torque.py new file mode 100644 index 0000000..24312fc --- /dev/null +++ b/design/mechanical/tests/test_torque.py @@ -0,0 +1,41 @@ +"""Torque-budget proof: the drives must carry the payload with margin, and the +counterweight-free configuration must be shown feasible (or not).""" + +from design.mechanical.calc import params as P +from design.mechanical.calc import torque +from design.mechanical.calc.budget import Verdict + + +def test_counterweight_free_ra_holding_has_margin(): + """The bold claim: harmonic holding torque covers the unbalanced payload.""" + ra = torque.ra_required_torque(P.MN78, counterweight_free=True) + sf = P.RA_DRIVE.rated_torque_Nm / ra["total"] + # Must clear the payload imbalance with a real continuous margin. + assert sf >= 2.0, f"counterweight-free RA SF too low: {sf:.2f}" + # Gravity imbalance should dominate the budget (it's the whole point). + assert ra["gravity"] > ra["wind"] + assert ra["gravity"] > ra["inertia"] + + +def test_dec_drive_covers_payload(): + dec = torque.dec_required_torque(P.MN78) + assert P.DEC_DRIVE.rated_torque_Nm / dec["total"] >= 2.0 + + +def test_peak_torque_covers_goto_transient(): + ra = torque.ra_required_torque(P.MN78, counterweight_free=True) + assert P.RA_DRIVE.peak_torque_Nm / ra["total"] >= 3.0 + + +def test_heavier_ota_still_passes_but_lower_sf(): + """MN78 (heavier) must have a lower SF than MN76 — sanity on the model.""" + light = torque.ra_required_torque(P.MN76, counterweight_free=True)["total"] + heavy = torque.ra_required_torque(P.MN78, counterweight_free=True)["total"] + assert heavy > light + + +def test_evaluate_returns_pass(): + r = torque.evaluate("MN78", counterweight_free=True) + assert r.verdict in (Verdict.PASS, Verdict.MARGINAL) + assert r.safety_factor is not None and r.safety_factor > 1.5 + assert any("RA total required" in ln.label for ln in r.lines) diff --git a/design/mechanical/tests/test_tradestudy.py b/design/mechanical/tests/test_tradestudy.py new file mode 100644 index 0000000..37e219b --- /dev/null +++ b/design/mechanical/tests/test_tradestudy.py @@ -0,0 +1,163 @@ +""" +Trade-study invariants (Phase B). + +The morphological box and weighted-Pugh scores are a pure function of the score +matrix in ``build_tradestudy.py``; these tests lock the load-bearing claims the +report makes so a silent edit to a score cannot drift the narrative: + + * every weighting scheme is a valid (sums-to-1) simplex, every score in 1..5; + * the encoder baseline is DISQUALIFIED by the hard 1.0" tracking gate and can + never be selected, even though its raw weighted score is high; + * the bold counterweight-FREE GEM wins under ALL three weightings (grounded in + torque RA SF 2.5 and balance deleting 15.4 kg + 29% RA inertia); + * the three documented sensitivity FLIPS actually occur (drive, encoder, + enclosure) and the STABLE picks actually stay put (topology, pier, frame); + * the three CSVs regenerate with the expected shape. +""" + +from __future__ import annotations + +import csv +import importlib.util +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +GEN = os.path.normpath(os.path.join(HERE, "..", "tradestudy", "build_tradestudy.py")) + + +def _load(): + spec = importlib.util.spec_from_file_location("build_tradestudy", GEN) + mod = importlib.util.module_from_spec(spec) + # Register before exec so dataclass field-type resolution can find the module. + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + + +TS = _load() + + +def _sub(key): + return next(s for s in TS.SUBSYSTEMS if s.key == key) + + +def test_weighting_schemes_are_valid_simplexes(): + for name, w in TS.WEIGHTS.items(): + assert set(w) == set(TS.CRITERIA), name + assert abs(sum(w.values()) - 1.0) < 1e-9, (name, sum(w.values())) + + +def test_all_scores_in_range(): + for sub in TS.SUBSYSTEMS: + for o in sub.options: + assert set(o.scores) == set(TS.CRITERIA), o.name + for c, v in o.scores.items(): + assert 1 <= v <= 5, (o.name, c, v) + + +def test_seven_subsystems_each_with_a_bold_option(): + assert len(TS.SUBSYSTEMS) == 7 + for sub in TS.SUBSYSTEMS: + roles = {o.role for o in sub.options} + assert "baseline" in roles, sub.key + assert "bold" in roles, sub.key + + +def test_encoder_baseline_is_gated_out_despite_high_score(): + """The as-specified encoder scores well but FAILS the 1.0" target => gated.""" + enc = _sub("encoder") + base = next(o for o in enc.options if o.role == "baseline") + assert base.gate_fail is True + # Its raw weighted score is actually the highest in the subsystem ... + raw = {o.name: TS.weighted(o.scores, TS.WEIGHTS["default"]) for o in enc.options} + assert raw[base.name] == max(raw.values()), "baseline should be the raw-score leader" + # ... yet it is never selected under any weighting because of the gate. + for scheme in TS.WEIGHTS: + assert TS.selected(enc, TS.WEIGHTS[scheme]) is not base + + +def test_encoder_selection_requires_on_axis_feedback(): + """Every qualifying encoder option carries an on-axis absolute element.""" + enc = _sub("encoder") + for scheme in TS.WEIGHTS: + sel = TS.selected(enc, TS.WEIGHTS[scheme]) + assert "on-axis" in sel.name.lower(), (scheme, sel.name) + + +def test_counterweight_free_gem_wins_everywhere(): + """The bold topology pick is robust across all three weightings.""" + top = _sub("topology") + bold = next(o for o in top.options if o.role == "bold") + assert "FREE" in bold.name + for scheme in TS.WEIGHTS: + assert TS.selected(top, TS.WEIGHTS[scheme]) is bold, scheme + + +def test_documented_sensitivity_flips_occur(): + """Precision-heavy weighting must flip drive, encoder, and enclosure.""" + d, p = TS.WEIGHTS["default"], TS.WEIGHTS["precision"] + + drive = _sub("drive") + assert TS.selected(drive, d).role == "baseline" # NEMA17+planetary+harmonic + assert TS.selected(drive, p).name == "Torque-motor direct drive" + + enc = _sub("encoder") + assert "Hybrid" in TS.selected(enc, d).name + assert TS.selected(enc, p).name == "On-axis high-res absolute ring" + + enc_sub = _sub("enclosure") + assert TS.selected(enc_sub, d).role == "baseline" # plain roll-off + assert TS.selected(enc_sub, p).name == "Roll-off + active thermal" + + +def test_stable_picks_do_not_move(): + """Topology, pier, and frame winners are weighting-invariant.""" + for key, want in [("topology", "Counterweight-FREE GEM"), + ("pier", "Concrete Sonotube"), + ("frame", "6061-T6 CNC plates")]: + sub = _sub(key) + picks = {TS.selected(sub, TS.WEIGHTS[s]).name for s in TS.WEIGHTS} + assert picks == {want}, (key, picks) + + +def test_mechanical_ota_winner_is_the_light_in_production_tube(): + """On the six mechanical axes alone, the lightest in-production tube wins; + the program's retention of the heavier MN78 is a science override, not a + mechanical result (this is why the stiffness proof FAILs on MN78).""" + ota = _sub("ota") + for scheme in TS.WEIGHTS: + assert TS.selected(ota, TS.WEIGHTS[scheme]).name == "ES-MN152 f/4.8", scheme + mn78 = next(o for o in ota.options if o.role == "baseline") + mn76 = next(o for o in ota.options if "MN76" in o.name) + # Among the sourced Intes pair, the lighter MN76 out-scores the doc-selected MN78. + assert (TS.weighted(mn76.scores, TS.WEIGHTS["default"]) + > TS.weighted(mn78.scores, TS.WEIGHTS["default"])) + + +def test_csvs_regenerate_with_expected_shape(): + p1 = TS.write_morphological() + p2 = TS.write_pugh() + p3 = TS.write_sensitivity() + n_opts = sum(len(s.options) for s in TS.SUBSYSTEMS) + + for path in (p1, p2, p3): + assert os.path.exists(path) + + with open(p1) as fh: + rows = list(csv.reader(fh)) + assert rows[0] == ["subsystem", "option", "role", "description", "proof_citation"] + assert len(rows) == 1 + n_opts + + with open(p2) as fh: + rows = list(csv.DictReader(fh)) + assert len(rows) == n_opts + # Exactly one SELECTED per subsystem (7 total). + assert sum(1 for r in rows if r["selected"] == "SELECTED") == 7 + assert sum(1 for r in rows if r["gate"] == "DISQUALIFIED") == 1 + + with open(p3) as fh: + rows = list(csv.DictReader(fh)) + assert len(rows) == n_opts + for scheme_col in ("win_default", "win_precision", "win_cost"): + assert sum(1 for r in rows if r[scheme_col] == "*") == 7 diff --git a/design/mechanical/tests/test_wind.py b/design/mechanical/tests/test_wind.py new file mode 100644 index 0000000..4b6520d --- /dev/null +++ b/design/mechanical/tests/test_wind.py @@ -0,0 +1,97 @@ +"""Wind-load proof: this module OWNS the authoritative drag calc. Two regimes -- +tiny operational drag (roof open, 35 mph max) feeding the torque/pier budgets, +and the governing survival case (roof closed, 105 mph) where the near-flat roof +develops uplift far beyond its own weight -- an honest FAIL that surfaces the +repo's missing hold-down/anchorage spec.""" + +import math + +from design.mechanical.calc import params as P +from design.mechanical.calc import torque, wind +from design.mechanical.calc.budget import Verdict + + +def test_dynamic_pressure_matches_half_rho_v2(): + V = P.ENV.wind_gust_close_ms + q = wind.dynamic_pressure_Pa(V) + assert math.isclose(q, 0.5 * P.SITE.air_density * V ** 2, rel_tol=1e-12) + # At 1800 m the air is thinner than sea level -> q below the 1.225 figure. + assert q < 0.5 * 1.225 * V ** 2 + + +def test_drag_grows_with_square_of_speed(): + """Doubling wind speed quadruples drag.""" + f1 = wind.drag_force_N(10.0, P.MN78) + f2 = wind.drag_force_N(20.0, P.MN78) + assert math.isclose(f2 / f1, 4.0, rel_tol=1e-9) + + +def test_drag_is_authoritative_and_matches_torque_copy(): + """wind.py owns the drag; torque.py's private copy must agree exactly.""" + for V in (P.ENV.wind_park_ms, P.ENV.wind_gust_close_ms): + for ota in (P.MN76, P.MN78): + f_wind = wind.drag_force_N(V, ota) + f_torque = torque._drag_force(V, ota, P.ENV, P.SITE) + assert math.isclose(f_wind, f_torque, rel_tol=1e-12) + + +def test_operational_drag_is_small_but_heavier_tube_sees_more(): + """35 mph gust drag on the OTA is only tens of newtons; MN78 > MN76 (bigger tube).""" + f_gust_78 = wind.drag_force_N(P.ENV.wind_gust_close_ms, P.MN78) + f_gust_76 = wind.drag_force_N(P.ENV.wind_gust_close_ms, P.MN76) + assert 20.0 < f_gust_78 < 60.0 + assert f_gust_78 > f_gust_76 # larger projected area + + +def test_park_threshold_lower_than_gust(): + assert wind.drag_force_N(P.ENV.wind_park_ms, P.MN78) < \ + wind.drag_force_N(P.ENV.wind_gust_close_ms, P.MN78) + + +def test_wind_moment_base_exceeds_top(): + """Moment at the pier base (longer lever) exceeds the pier-top moment.""" + ota = P.MN78 + f = wind.drag_force_N(P.ENV.wind_gust_close_ms, ota) + m_base = wind.ota_wind_moment_Nm(P.ENV.wind_gust_close_ms, ota) + m_top = f * wind.MOUNT_HEAD_HEIGHT_M + assert m_base > m_top + assert math.isclose(m_base, f * (P.PIER.height_above_m + wind.MOUNT_HEAD_HEIGHT_M), + rel_tol=1e-12) + + +def test_roof_uplift_formula_and_magnitude(): + """Survival uplift = q_survival * GCp * A_roof, several kN on the 9 m^2 roof.""" + q = wind.dynamic_pressure_Pa(P.ENV.survival_wind_ms) + expect = q * P.ENV.roof_uplift_gcp * (P.ENCLOSURE.roof_span_m * P.ENCLOSURE.roof_length_m) + assert math.isclose(wind.roof_uplift_N(), expect, rel_tol=1e-12) + assert 8.0e3 < wind.roof_uplift_N() < 11.0e3 + + +def test_roof_selfweight_cannot_resist_uplift(): + """The headline finding: roof dead weight resists only ~1/5 of the uplift.""" + sf = wind.roof_weight_N() / wind.roof_uplift_N() + assert sf < 0.3 + # Net uplift the (unspecified) anchors must carry is the bulk of the gross. + assert wind.roof_net_uplift_N() > 0.7 * wind.roof_uplift_N() + + +def test_assumed_anchors_would_remediate(): + """Four typical 2 klbf anchors turn the FAIL into a real margin -- the fix.""" + capacity = wind.ROOF_ANCHOR_COUNT * wind.ROOF_ANCHOR_ALLOW_N + assert capacity / wind.roof_uplift_N() > 2.0 + + +def test_survival_governs_over_operational(): + """Survival roof uplift dwarfs the operational gust drag by orders of magnitude.""" + f_gust = wind.drag_force_N(P.ENV.wind_gust_close_ms, P.MN78) + assert wind.roof_uplift_N() > 100.0 * f_gust + + +def test_evaluate_fails_on_survival_uplift(): + """As-specified (no anchors in repo) the roof FAILS survival uplift -- the + important, honest result. Assumptions must disclose the ASSUMED survival wind.""" + r = wind.evaluate("MN78") + assert r.verdict is Verdict.FAIL + assert r.safety_factor is not None and r.safety_factor < 1.0 + assert any("uplift" in ln.label.lower() for ln in r.lines) + assert any("ASSUMED" in a and "mph" in a for a in r.assumptions) diff --git a/design/mechanical/tradestudy/SECTION.md b/design/mechanical/tradestudy/SECTION.md new file mode 100644 index 0000000..728a098 --- /dev/null +++ b/design/mechanical/tradestudy/SECTION.md @@ -0,0 +1,183 @@ +## Trade Study (Phase B) — Morphological Box + Weighted-Pugh Selection + +This section does not compute physics; it **selects** among design permutations by +scoring them against the verdicts the Phase-C proof modules actually computed, so +the configuration that goes into CAD is grounded rather than asserted. The scoring +matrix, the three CSVs below, and their invariants are a pure function encoded in +`design/mechanical/tradestudy/build_tradestudy.py` and locked by +`design/mechanical/tests/test_tradestudy.py` (10 tests, green). + +**Grounding — the Phase-C verdict ledger the scores are anchored to:** + +| Proof | Verdict | Governing number | +|---|---|---| +| torque | PASS | counterweight-FREE MN78: RA SF 2.5, DEC SF 2.6 vs rated | +| stiffness | **FAIL** | 43.3" vs 5" target — bearing compliance at 63.5/76.2 mm spans governs | +| dynamics | PASS | first mode 65 Hz vs 10 Hz (SF 6.5) | +| encoder | **FAIL** | baseline 5.02" RMS vs 1.0"; on-axis RESA ring 0.54" | +| balance | PASS | 12.5 kg balances 18 kg (fit 1.59); CW-free deletes 15.4 kg + 29% RA inertia | +| bearings | PASS + finding | load never governs; 60xx are deep-groove — use angular-contact 7008/7006 pairs | +| wind | **FAIL** | roof uplift SF 0.19 — hold-down anchors mandatory (SF 3.9 with 4x 2 klbf) | +| pier | PASS | governing SF 6.8, f_n 152 Hz | +| thermal | **FAIL** (passive) | 22 K swing walks focus ~10x DoF — temp-compensated focuser required | +| enclosure | PASS + finding | drive SF 2.1; snow case governs the roof, snow interlock required | +| power | **FAIL** (off-grid) | 12 V pack 3.3 h vs 10 h night; 48 V -> 13.4 h; DGX = 59% of load | + +**Scoring convention.** Every criterion is *higher-is-better* on a 1–5 scale (for +cost and sourcing, 5 = cheapest / lowest risk), so a weighted score stays in 1–5. +Default weights: **stiffness 0.22, precision 0.22, cost 0.15, buildability 0.15, +thermal 0.10, sourcing-risk 0.16**. Crucially, precision against the hard 1.0" +tracking target is a **pass/fail GATE**, not merely a weighted term: an option that +fails it is *disqualified* regardless of its weighted score. + +### 1. Morphological (Zwicky) matrix — the option space + +`morphological.csv`. Bold picks are marked **[BOLD]**. + +| Subsystem | Baseline | Alternatives | Bold | +|---|---|---|---| +| OTA | MN78 f/8 (14 kg) | MN76 f/6 (9 kg), MN86 8", ES-MN152 | APM-LZOS apo triplet | +| Mount topology | Counterweighted GEM | Fork + derotator | **Counterweight-FREE GEM** | +| Axis drive | NEMA17 + 27:1 + harmonic | Direct-to-harmonic | **Torque-motor direct drive** | +| Encoder | AMT103 + AS5600 | Hybrid (motor + on-axis abs.) | **On-axis high-res absolute ring** | +| Pier | Concrete Sonotube | Steel-concrete hybrid | **Isolated pier-in-pier** | +| Enclosure | Roll-off roof | Clamshell dome | **Roll-off + active thermal** | +| Frame | 6061-T6 CNC plates | Steel weldment | **Cast housings** | + +### 2. Weighted-Pugh scores (default weights) + +`pugh_scores.csv`. Winner of each subsystem in **bold**; the encoder baseline is +`DISQUALIFIED` by the tracking gate. + +| Subsystem | Option | stiff | prec | cost | build | therm | src | **Weighted** | +|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +| OTA | MN78 f/8 (baseline) | 2 | 4 | 3 | 3 | 4 | 2 | 2.94 | +| OTA | MN76 f/6 | 4 | 3 | 3 | 4 | 3 | 2 | 3.21 | +| OTA | MN86 8" | 1 | 4 | 2 | 2 | 3 | 1 | 2.16 | +| OTA | APM-LZOS **[BOLD]** | 2 | 5 | 1 | 3 | 3 | 2 | 2.76 | +| OTA | **ES-MN152** | 4 | 3 | 5 | 4 | 3 | 5 | **3.99** | +| Topology | Counterweighted GEM (baseline) | 3 | 3 | 3 | 4 | 3 | 4 | 3.31 | +| Topology | **Counterweight-FREE GEM [BOLD]** | 3 | 4 | 4 | 4 | 3 | 4 | **3.68** | +| Topology | Fork + derotator | 3 | 2 | 2 | 2 | 3 | 2 | 2.32 | +| Drive | **NEMA17 + 27:1 + harmonic (baseline)** | 3 | 2 | 4 | 4 | 3 | 4 | **3.24** | +| Drive | Direct-to-harmonic | 3 | 3 | 3 | 3 | 3 | 4 | 3.16 | +| Drive | Torque-motor DD **[BOLD]** | 3 | 5 | 1 | 2 | 2 | 2 | 2.73 | +| Encoder | AMT103 + AS5600 (baseline) | 3 | 1 | 5 | 4 | 3 | 5 | 3.33 `DISQ` | +| Encoder | On-axis RESA ring **[BOLD]** | 3 | 5 | 1 | 2 | 3 | 2 | 2.83 | +| Encoder | **Hybrid (motor + on-axis abs.)** | 3 | 4 | 2 | 3 | 3 | 3 | **3.07** | +| Pier | **Concrete Sonotube (baseline)** | 4 | 3 | 5 | 4 | 4 | 5 | **4.09** | +| Pier | Isolated pier-in-pier **[BOLD]** | 4 | 4 | 3 | 3 | 4 | 4 | 3.70 | +| Pier | Steel-concrete hybrid | 3 | 3 | 3 | 3 | 2 | 3 | 2.90 | +| Enclosure | **Roll-off roof (baseline)** | 3 | 3 | 5 | 5 | 3 | 5 | **3.92** | +| Enclosure | Clamshell dome | 3 | 3 | 2 | 2 | 2 | 2 | 2.44 | +| Enclosure | Roll-off + active thermal **[BOLD]** | 3 | 4 | 4 | 4 | 5 | 4 | 3.88 | +| Frame | **6061-T6 CNC plates (baseline)** | 3 | 3 | 4 | 4 | 4 | 4 | **3.56** | +| Frame | Steel weldment | 4 | 3 | 3 | 2 | 3 | 3 | 3.07 | +| Frame | Cast housings **[BOLD]** | 4 | 4 | 2 | 1 | 3 | 2 | 2.83 | + +Two results are worth pausing on. First, the **encoder baseline has the *highest* +raw weighted score in its subsystem (3.33) yet is not selectable** — it fails the +1.0" gate (encoder proof: 5.02" RMS). A naive weighted sum would have kept a design +that misses the headline requirement by 5x; the gate is what prevents that, and it +is the single most important structural feature of this study. Second, the **OTA +math prefers the light in-production ES-MN152 (3.99), and among the Intes pair MN76 +(3.21) out-scores the doc-'selected' MN78 (2.94)** — MN78 is the *worst* realistic +tube on the six mechanical axes, precisely because its 14 kg / 1.4 m tube is the +lever the stiffness proof FAILs on. + +### 3. Sensitivity — precision-heavy and cost-heavy re-weighting + +`sensitivity.csv`. Precision-heavy = {prec 0.40, stiff 0.20, cost 0.10, build 0.10, +therm 0.08, src 0.12}; cost-heavy = {cost 0.35, src 0.20, build 0.15, stiff 0.12, +prec 0.10, therm 0.08}. + +| Subsystem | Default winner | Precision-heavy | Cost-heavy | Robust? | +|---|---|---|---|---| +| OTA | ES-MN152 (3.99) | ES-MN152 (3.74) | ES-MN152 (4.37) | **stable** | +| Topology | CW-FREE GEM (3.68) | CW-FREE GEM (3.72) | CW-FREE GEM (3.80) | **stable** | +| Drive | NEMA17+harmonic (3.24) | **Torque-motor DD (3.30)** | NEMA17+harmonic (3.60) | flips (prec) | +| Encoder | Hybrid (3.07) | **RESA ring (3.38)** | Hybrid (2.75) | flips (prec) | +| Pier | Sonotube (4.09) | Sonotube (3.82) | Sonotube (4.45) | **stable** | +| Enclosure | Roll-off (3.92) | **Roll-off + thermal (3.88)** | Roll-off (4.40) | flips (prec) | +| Frame | 6061 CNC (3.56) | 6061 CNC (3.40) | 6061 CNC (3.56/3.78) | **stable** | + +The winner changes in exactly three subsystems, and **all three flips are in the +same direction**: weighting the sub-arcsec mission more heavily pulls the drive to a +zero-gear-PE torque motor, the encoder to the full on-axis ring, and the enclosure +to active thermal control — i.e. the bold options are the *precision* options, and +they win the moment precision dominates. Four subsystems (OTA, topology, pier, +frame) never move. + +### 4. Selected permutation + rationale + +The delivered build takes the default-weight winners, with two deliberate, +disclosed departures. It comprises **4 baselines, 2 bold picks, and 1 mandatory +bold-adjacent upgrade**: + +1. **OTA — MN78 f/8 (science override of the mechanical winner).** The mechanical + Pugh winner is ES-MN152 under every weighting, and MN76 beats MN78 among the + Intes pair. MN78 is retained for its f/8 imaging scale and 0.134 obstruction — + an optical/science requirement that lives *outside* the six mechanical criteria. + This retention is not free: it is exactly why the stiffness proof FAILs, and it + is the reason item 8 below is mandatory. If the bearing remediation proves + insufficient, **MN76 is the drop-in mechanical hedge** (shorter CG lever, lower + RA torque). +2. **Mount topology — Counterweight-FREE GEM [BOLD].** Robust winner under all three + weightings. Grounded: torque proof gives RA SF 2.5 / DEC SF 2.6 on the harmonic + drives' back-drive resistance, and the balance proof shows deleting the shaft + + weights removes **15.4 kg and 29% of the RA inertia** — a strict improvement in + what the drives and pier must carry, the same principle the ZWO AM5 / RST-135 + exploit. +3. **Axis drive — NEMA17 + 27:1 planetary + harmonic (baseline).** Wins default and + cost-heavy; torque proof PASS. The planetary's periodic error — the only reason + to consider deleting it — is **mooted by the on-axis encoder (item 4), which + corrects everything upstream of the axis**, so the cheap standard OnStepX + drivetrain is retained. (Under a precision-only view it flips to the torque + motor; that is a documented, deferred upgrade path.) +4. **Encoder — Hybrid: motor encoder (velocity) + on-axis absolute (position) + [mandatory on-axis upgrade].** The as-specified baseline is **DISQUALIFIED** — + encoder proof: 5.02" RMS, 5x over target, because the AS5600's ~91" quantisation + makes it homing-grade only and the servo must close on the motor encoder, which + cannot see harmonic PE or mount flexure. Some form of on-axis absolute feedback + is *required*, not optional (proof: on-axis ring reaches 0.54" RMS). The hybrid + wins among qualifying options at default/cost weights; the **full on-axis RESA + ring [BOLD]** is the selected upgrade when precision is weighted heavily and is + the only option that reaches sub-arcsec outright. +5. **Pier — Concrete Sonotube (baseline).** Wins under all weightings (4.09). The + pier proof already passes with governing SF 6.8 and f_n 152 Hz, so the bold + isolated pier-in-pier would be **gold-plating the one part that is not the + constraint** — the head/bearings are. Retained as baseline. +6. **Enclosure — Roll-off + active thermal [BOLD].** A statistical tie with the + plain roll-off at default weights (3.88 vs 3.92, within noise), broken toward the + bold variant by two proof findings the plain roof ignores: the **thermal FAIL** + (22 K diurnal swing walks focus ~10x depth-of-focus) and the **power finding** + that the DGX dumps ~14 K into the enclosure at 2 ACH ("ventilate or locate + outside"). Active ventilation + insulation + day pre-cooling is the direct remedy, + and it becomes the outright winner the instant precision is weighted up. +7. **Frame — 6061-T6 CNC plates (baseline).** Wins under all weightings (3.56). The + key insight from the stiffness proof: the **aluminium plates are *not* the + governing compliance — the bearings are** — so switching to a steel weldment or + cast housings spends stiffness budget in the wrong place. Retain the machinable, + DIY-friendly 6061 frame and fix the bearings instead (item 8). + +**Cross-cutting actions the trade study surfaces but cannot select away.** Three +Phase-C FAILs are not closable by any of the seven morphological axes and are +booked here as required detail-design actions: + +- **Stiffness (governing FAIL, 43.3" vs 5"):** fix at the *component* level, not the + configuration level — replace the deep-groove 6008/6006 with **matched + angular-contact 7008/7006 pairs (back-to-back) on larger spans** (bearings + + stiffness proofs). No frame or topology choice in the box moves this number. +- **Wind (FAIL, roof uplift SF 0.19):** hold-down anchors are **mandatory and + currently unspecified** — 4x 2 klbf anchors restore SF 3.9 (wind proof). Pair with + the snow interlock the enclosure proof requires. +- **Off-grid power (FAIL):** the selected build assumes grid + UPS (16x shutdown + margin). True autonomy needs a **48 V pack (13.4 h) or a DGX duty-cycle**, not the + specified 12 V / 100 Ah pack (3.3 h). Orthogonal to the enclosure/pier choices. + +**Net:** the selected permutation banks the two clearly-won bold improvements +(counterweight-free GEM, active-thermal enclosure) plus the mandatory on-axis +encoder, keeps the baseline where the baseline already passes (drive, pier, frame), +and honestly flags that the OTA choice, the stiffness fix, the wind anchors, and the +power autonomy are decisions the science mission and detail design must own — the +trade study cannot make them disappear. diff --git a/design/mechanical/tradestudy/build_tradestudy.py b/design/mechanical/tradestudy/build_tradestudy.py new file mode 100644 index 0000000..17c5c54 --- /dev/null +++ b/design/mechanical/tradestudy/build_tradestudy.py @@ -0,0 +1,297 @@ +""" +NIGHTWATCH mechanical trade study — morphological (Zwicky) box + weighted Pugh. + +This is Phase B of the mechanical-design proof. It does NOT compute physics; it +*scores* the design permutations against the verdicts the Phase-C proof modules +(torque, stiffness, dynamics, encoder, balance, bearings, wind, pier, thermal, +enclosure, power) actually computed, so the selection is grounded, not asserted. + +Scoring convention +------------------ +Every criterion is "higher is better" on a 1..5 scale (5 = best): + 1 poor · 2 below par · 3 adequate/baseline · 4 good · 5 excellent +For the cost and sourcing criteria, 5 therefore means *cheapest* / *lowest +sourcing risk*. A weighted score is sum(weight_i * score_i), so it stays in 1..5. + +Requirement gate +---------------- +A weighted score can rank an option that FAILS a hard requirement above one that +meets it (see the encoder subsystem). Precision against the 1.0" tracking target +is a pass/fail GATE, not merely a weighted criterion: any option that fails the +gate is DISQUALIFIED from selection regardless of its weighted score. This is the +central lesson of the study and is encoded explicitly (``gate_fail``). + +Outputs (written next to this file): + morphological.csv the Zwicky box: every option, its role, proof citation + pugh_scores.csv the six criteria scores + default weighted total + rank + sensitivity.csv default vs precision-heavy vs cost-heavy re-ranking +""" + +from __future__ import annotations + +import csv +import os +from dataclasses import dataclass, field + +# -------------------------------------------------------------------------- +# Criteria and the three weighting schemes (each sums to 1.0). +# -------------------------------------------------------------------------- +CRITERIA = ["stiffness", "precision", "cost", "buildability", "thermal", "sourcing"] + +WEIGHTS = { + # Task default weights. + "default": {"stiffness": 0.22, "precision": 0.22, "cost": 0.15, + "buildability": 0.15, "thermal": 0.10, "sourcing": 0.16}, + # Precision-heavy: the sub-arcsec imaging mission dominates. + "precision": {"stiffness": 0.20, "precision": 0.40, "cost": 0.10, + "buildability": 0.10, "thermal": 0.08, "sourcing": 0.12}, + # Cost-heavy: amateur / grant-limited build, minimise spend and sourcing risk. + "cost": {"stiffness": 0.12, "precision": 0.10, "cost": 0.35, + "buildability": 0.15, "thermal": 0.08, "sourcing": 0.20}, +} +for name, w in WEIGHTS.items(): + assert abs(sum(w.values()) - 1.0) < 1e-9, (name, sum(w.values())) + + +@dataclass +class Option: + name: str + role: str # baseline | alternative | bold + desc: str # one-line morphological description + proof: str # grounding citation from the Phase-C proofs + scores: dict # criterion -> 1..5 + gate_fail: bool = False # disqualified: fails a hard requirement + + +@dataclass +class Subsystem: + key: str + title: str + options: list = field(default_factory=list) + + +def S(stiffness, precision, cost, build, thermal, sourcing): + return {"stiffness": stiffness, "precision": precision, "cost": cost, + "buildability": build, "thermal": thermal, "sourcing": sourcing} + + +# ========================================================================== +# The morphological matrix (Zwicky box): 7 subsystems, each baseline + alts. +# Scores are engineering judgement anchored to the proof verdicts cited. +# ========================================================================== +SUBSYSTEMS: list[Subsystem] = [ + Subsystem("ota", "Optical tube assembly", [ + Option("MN78 f/8 (14 kg)", "baseline", + "180 mm Mak-Newt, 1440 mm tube, 0.134 obstruction; doc-'selected'", + "stiffness: MN78 is the tube the FAIL is computed on (43.3\" vs 5\")", + S(2, 4, 3, 3, 4, 2)), + Option("MN76 f/6 (9 kg)", "alternative", + "178 mm Mak-Newt, 700 mm tube, 0.25 obstruction; lighter/shorter", + "torque: lighter tube => lower RA torque; stiffness: shorter CG lever", + S(4, 3, 3, 4, 3, 2)), + Option("MN86 f/6 (8-inch)", "alternative", + "203 mm Mak-Newt; more aperture, heavier and longer tube", + "stiffness/torque: heaviest tube => worst mount loading", + S(1, 4, 2, 2, 3, 1)), + Option("APM-LZOS apo triplet", "bold", + "refractor, zero obstruction, premium optics; long/heavy/costly", + "thermal: glass soak vs metal-tube focus walk; precision: no obstruction", + S(2, 5, 1, 3, 3, 2)), + Option("ES-MN152 f/4.8", "alternative", + "152 mm Mak-Newt, in-production mass-market; cheap/light", + "torque/stiffness: lightest, in-production; lowest sourcing risk", + S(4, 3, 5, 4, 3, 5)), + ]), + Subsystem("topology", "Mount topology", [ + Option("Counterweighted GEM", "baseline", + "German equatorial + 12.5 kg counterweight on 457 mm shaft", + "balance: 12.5 kg balances 18 kg payload at r=288 mm (fit 1.59)", + S(3, 3, 3, 4, 3, 4)), + Option("Counterweight-FREE GEM", "bold", + "delete the shaft+weights; harmonic back-drive holds the imbalance", + "torque: RA SF 2.5 PASS; balance: deletes 15.4 kg + 29% RA inertia", + S(3, 4, 4, 4, 3, 4)), + Option("Fork + field derotator", "alternative", + "symmetric fork, no meridian flip; derotator adds a rotation axis", + "precision: derotator injects a continuous field-rotation error term", + S(3, 2, 2, 2, 3, 2)), + ]), + Subsystem("drive", "Axis drive train", [ + Option("NEMA17 + 27:1 + harmonic", "baseline", + "stepper + planetary + CSF harmonic (100:1 RA / 80:1 DEC)", + "torque: PASS; encoder: on-axis ring moots upstream planetary PE", + S(3, 2, 4, 4, 3, 4)), + Option("Direct-to-harmonic", "alternative", + "delete planetary; larger motor straight into the harmonic", + "torque: 0.45 Nm x100 = 45 Nm ~ 49.9 Nm need => bigger motor required", + S(3, 3, 3, 3, 3, 4)), + Option("Torque-motor direct drive", "bold", + "frameless torque motor on-axis, zero gear PE; needs on-axis encoder", + "encoder: only DD+ring reaches seeing-limited; power: holding-heat cost", + S(3, 5, 1, 2, 2, 2)), + ]), + Subsystem("encoder", "Feedback / encoder chain", [ + Option("AMT103 motor + AS5600 axis", "baseline", + "fine motor encoder + 12-bit on-axis chip (homing-grade)", + "encoder: 5.02\" RMS FAILS the 1.0\" target => DISQUALIFIED", + S(3, 1, 5, 4, 3, 5), gate_fail=True), + Option("On-axis high-res absolute ring", "bold", + "Renishaw RESA-class absolute ring, sub-arcsec LSB, on the axis", + "encoder: 0.54\" RMS => MEETS sub-arcsec (only option that does)", + S(3, 5, 1, 2, 3, 2)), + Option("Hybrid (motor + on-axis absolute)", "alternative", + "motor encoder for velocity + on-axis absolute for position correction", + "encoder: on-axis correction closes the PE+flexure gap the motor can't see", + S(3, 4, 2, 3, 3, 3)), + ]), + Subsystem("pier", "Pier / foundation", [ + Option("Concrete Sonotube", "baseline", + "12-inch x 36-inch concrete pier on a 36-inch embedment", + "pier: governing SF 6.8 PASS, f_n 152 Hz (SF 15)", + S(4, 3, 5, 4, 4, 5)), + Option("Isolated pier-in-pier", "bold", + "inner OTA pier isolated from the building slab / enclosure floor", + "pier: core stiffness unchanged; isolates roof-drive/footfall vibration", + S(4, 4, 3, 3, 4, 4)), + Option("Steel-concrete hybrid", "alternative", + "steel column on a concrete footing; faster to erect", + "pier: slender steel column => lower f_n; thermal bending of the column", + S(3, 3, 3, 3, 2, 3)), + ]), + Subsystem("enclosure", "Enclosure", [ + Option("Roll-off roof", "baseline", + "flat roll-off roof, garage-door-class drive", + "enclosure: drive SF 2.1 PASS; wind: uplift SF 0.19 => anchors mandatory", + S(3, 3, 5, 5, 3, 5)), + Option("Clamshell dome", "alternative", + "rotating split-shell dome; better wind screen, worse flush", + "thermal: enclosed dome traps the DGX 14 K heat dump", + S(3, 3, 2, 2, 2, 2)), + Option("Roll-off + active thermal", "bold", + "roll-off base + insulation + forced ventilation + day pre-cooling", + "thermal: FAIL focus-walk remedy; power: exhausts DGX 14 K enclosure dump", + S(3, 4, 4, 4, 5, 4)), + ]), + Subsystem("frame", "Mount-head frame / housings", [ + Option("6061-T6 CNC plates", "baseline", + "bolt-together CNC aluminium housings (E=68.9 GPa)", + "stiffness: plates are NOT the governing compliance (bearings are)", + S(3, 3, 4, 4, 4, 4)), + Option("Steel weldment", "alternative", + "welded steel frame, ~3x modulus; heavier, weld distortion", + "stiffness: 3x E cuts only the small beam term; bearings still govern", + S(4, 3, 3, 2, 3, 3)), + Option("Cast housings", "bold", + "cast iron/Al monolithic housings; best damping, integral bearing seats", + "stiffness: integral large-span AC bearing seats attack the FAIL; foundry risk", + S(4, 4, 2, 1, 3, 2)), + ]), +] + + +# ========================================================================== +# Scoring +# ========================================================================== +def weighted(scores: dict, weights: dict) -> float: + return round(sum(weights[c] * scores[c] for c in CRITERIA), 4) + + +def ranked(sub: Subsystem, weights: dict, apply_gate: bool = True): + """Return options sorted best-first under a weighting. + + Gate-failed options are pushed below all qualifying options (they cannot be + selected) but keep their raw weighted score for transparency. + """ + def keyfn(o: Option): + return (0 if (apply_gate and o.gate_fail) else 1, weighted(o.scores, weights)) + return sorted(sub.options, key=keyfn, reverse=True) + + +def selected(sub: Subsystem, weights: dict) -> Option: + return ranked(sub, weights, apply_gate=True)[0] + + +# ========================================================================== +# CSV emitters +# ========================================================================== +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def write_morphological(): + path = os.path.join(HERE, "morphological.csv") + with open(path, "w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["subsystem", "option", "role", "description", "proof_citation"]) + for sub in SUBSYSTEMS: + for o in sub.options: + w.writerow([sub.title, o.name, o.role, o.desc, o.proof]) + return path + + +def write_pugh(): + path = os.path.join(HERE, "pugh_scores.csv") + with open(path, "w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["subsystem", "option", "role"] + CRITERIA + + ["weighted_default", "rank_default", "gate", "selected"]) + for sub in SUBSYSTEMS: + order = ranked(sub, WEIGHTS["default"], apply_gate=True) + rank_of = {id(o): i + 1 for i, o in enumerate(order)} + sel = selected(sub, WEIGHTS["default"]) + for o in sub.options: + w.writerow( + [sub.title, o.name, o.role] + + [o.scores[c] for c in CRITERIA] + + [weighted(o.scores, WEIGHTS["default"]), + rank_of[id(o)], + "DISQUALIFIED" if o.gate_fail else "ok", + "SELECTED" if o is sel else ""] + ) + return path + + +def write_sensitivity(): + path = os.path.join(HERE, "sensitivity.csv") + with open(path, "w", newline="") as fh: + w = csv.writer(fh) + w.writerow(["subsystem", "option", "role", + "w_default", "w_precision", "w_cost", + "win_default", "win_precision", "win_cost"]) + for sub in SUBSYSTEMS: + sel_d = selected(sub, WEIGHTS["default"]) + sel_p = selected(sub, WEIGHTS["precision"]) + sel_c = selected(sub, WEIGHTS["cost"]) + for o in sub.options: + w.writerow([ + sub.title, o.name, o.role, + weighted(o.scores, WEIGHTS["default"]), + weighted(o.scores, WEIGHTS["precision"]), + weighted(o.scores, WEIGHTS["cost"]), + "*" if o is sel_d else "", + "*" if o is sel_p else "", + "*" if o is sel_c else "", + ]) + return path + + +def summary() -> str: + lines = [] + for sub in SUBSYSTEMS: + sel_d = selected(sub, WEIGHTS["default"]) + sel_p = selected(sub, WEIGHTS["precision"]) + sel_c = selected(sub, WEIGHTS["cost"]) + changed = "STABLE" if (sel_d is sel_p is sel_c) else "CHANGES" + lines.append( + f"{sub.title:32s} default={sel_d.name:34s} " + f"prec={sel_p.name:34s} cost={sel_c.name:34s} [{changed}]" + ) + return "\n".join(lines) + + +if __name__ == "__main__": + p1 = write_morphological() + p2 = write_pugh() + p3 = write_sensitivity() + print("wrote:", p1, p2, p3, sep="\n ") + print("\nPer-subsystem winners under each weighting:") + print(summary()) diff --git a/design/mechanical/tradestudy/morphological.csv b/design/mechanical/tradestudy/morphological.csv new file mode 100644 index 0000000..bd3f7d7 --- /dev/null +++ b/design/mechanical/tradestudy/morphological.csv @@ -0,0 +1,24 @@ +subsystem,option,role,description,proof_citation +Optical tube assembly,MN78 f/8 (14 kg),baseline,"180 mm Mak-Newt, 1440 mm tube, 0.134 obstruction; doc-'selected'","stiffness: MN78 is the tube the FAIL is computed on (43.3"" vs 5"")" +Optical tube assembly,MN76 f/6 (9 kg),alternative,"178 mm Mak-Newt, 700 mm tube, 0.25 obstruction; lighter/shorter",torque: lighter tube => lower RA torque; stiffness: shorter CG lever +Optical tube assembly,MN86 f/6 (8-inch),alternative,"203 mm Mak-Newt; more aperture, heavier and longer tube",stiffness/torque: heaviest tube => worst mount loading +Optical tube assembly,APM-LZOS apo triplet,bold,"refractor, zero obstruction, premium optics; long/heavy/costly",thermal: glass soak vs metal-tube focus walk; precision: no obstruction +Optical tube assembly,ES-MN152 f/4.8,alternative,"152 mm Mak-Newt, in-production mass-market; cheap/light","torque/stiffness: lightest, in-production; lowest sourcing risk" +Mount topology,Counterweighted GEM,baseline,German equatorial + 12.5 kg counterweight on 457 mm shaft,balance: 12.5 kg balances 18 kg payload at r=288 mm (fit 1.59) +Mount topology,Counterweight-FREE GEM,bold,delete the shaft+weights; harmonic back-drive holds the imbalance,torque: RA SF 2.5 PASS; balance: deletes 15.4 kg + 29% RA inertia +Mount topology,Fork + field derotator,alternative,"symmetric fork, no meridian flip; derotator adds a rotation axis",precision: derotator injects a continuous field-rotation error term +Axis drive train,NEMA17 + 27:1 + harmonic,baseline,stepper + planetary + CSF harmonic (100:1 RA / 80:1 DEC),torque: PASS; encoder: on-axis ring moots upstream planetary PE +Axis drive train,Direct-to-harmonic,alternative,delete planetary; larger motor straight into the harmonic,torque: 0.45 Nm x100 = 45 Nm ~ 49.9 Nm need => bigger motor required +Axis drive train,Torque-motor direct drive,bold,"frameless torque motor on-axis, zero gear PE; needs on-axis encoder",encoder: only DD+ring reaches seeing-limited; power: holding-heat cost +Feedback / encoder chain,AMT103 motor + AS5600 axis,baseline,fine motor encoder + 12-bit on-axis chip (homing-grade),"encoder: 5.02"" RMS FAILS the 1.0"" target => DISQUALIFIED" +Feedback / encoder chain,On-axis high-res absolute ring,bold,"Renishaw RESA-class absolute ring, sub-arcsec LSB, on the axis","encoder: 0.54"" RMS => MEETS sub-arcsec (only option that does)" +Feedback / encoder chain,Hybrid (motor + on-axis absolute),alternative,motor encoder for velocity + on-axis absolute for position correction,encoder: on-axis correction closes the PE+flexure gap the motor can't see +Pier / foundation,Concrete Sonotube,baseline,12-inch x 36-inch concrete pier on a 36-inch embedment,"pier: governing SF 6.8 PASS, f_n 152 Hz (SF 15)" +Pier / foundation,Isolated pier-in-pier,bold,inner OTA pier isolated from the building slab / enclosure floor,pier: core stiffness unchanged; isolates roof-drive/footfall vibration +Pier / foundation,Steel-concrete hybrid,alternative,steel column on a concrete footing; faster to erect,pier: slender steel column => lower f_n; thermal bending of the column +Enclosure,Roll-off roof,baseline,"flat roll-off roof, garage-door-class drive",enclosure: drive SF 2.1 PASS; wind: uplift SF 0.19 => anchors mandatory +Enclosure,Clamshell dome,alternative,"rotating split-shell dome; better wind screen, worse flush",thermal: enclosed dome traps the DGX 14 K heat dump +Enclosure,Roll-off + active thermal,bold,roll-off base + insulation + forced ventilation + day pre-cooling,thermal: FAIL focus-walk remedy; power: exhausts DGX 14 K enclosure dump +Mount-head frame / housings,6061-T6 CNC plates,baseline,bolt-together CNC aluminium housings (E=68.9 GPa),stiffness: plates are NOT the governing compliance (bearings are) +Mount-head frame / housings,Steel weldment,alternative,"welded steel frame, ~3x modulus; heavier, weld distortion",stiffness: 3x E cuts only the small beam term; bearings still govern +Mount-head frame / housings,Cast housings,bold,"cast iron/Al monolithic housings; best damping, integral bearing seats",stiffness: integral large-span AC bearing seats attack the FAIL; foundry risk diff --git a/design/mechanical/tradestudy/pugh_scores.csv b/design/mechanical/tradestudy/pugh_scores.csv new file mode 100644 index 0000000..fa2d001 --- /dev/null +++ b/design/mechanical/tradestudy/pugh_scores.csv @@ -0,0 +1,24 @@ +subsystem,option,role,stiffness,precision,cost,buildability,thermal,sourcing,weighted_default,rank_default,gate,selected +Optical tube assembly,MN78 f/8 (14 kg),baseline,2,4,3,3,4,2,2.94,3,ok, +Optical tube assembly,MN76 f/6 (9 kg),alternative,4,3,3,4,3,2,3.21,2,ok, +Optical tube assembly,MN86 f/6 (8-inch),alternative,1,4,2,2,3,1,2.16,5,ok, +Optical tube assembly,APM-LZOS apo triplet,bold,2,5,1,3,3,2,2.76,4,ok, +Optical tube assembly,ES-MN152 f/4.8,alternative,4,3,5,4,3,5,3.99,1,ok,SELECTED +Mount topology,Counterweighted GEM,baseline,3,3,3,4,3,4,3.31,2,ok, +Mount topology,Counterweight-FREE GEM,bold,3,4,4,4,3,4,3.68,1,ok,SELECTED +Mount topology,Fork + field derotator,alternative,3,2,2,2,3,2,2.32,3,ok, +Axis drive train,NEMA17 + 27:1 + harmonic,baseline,3,2,4,4,3,4,3.24,1,ok,SELECTED +Axis drive train,Direct-to-harmonic,alternative,3,3,3,3,3,4,3.16,2,ok, +Axis drive train,Torque-motor direct drive,bold,3,5,1,2,2,2,2.73,3,ok, +Feedback / encoder chain,AMT103 motor + AS5600 axis,baseline,3,1,5,4,3,5,3.33,3,DISQUALIFIED, +Feedback / encoder chain,On-axis high-res absolute ring,bold,3,5,1,2,3,2,2.83,2,ok, +Feedback / encoder chain,Hybrid (motor + on-axis absolute),alternative,3,4,2,3,3,3,3.07,1,ok,SELECTED +Pier / foundation,Concrete Sonotube,baseline,4,3,5,4,4,5,4.09,1,ok,SELECTED +Pier / foundation,Isolated pier-in-pier,bold,4,4,3,3,4,4,3.7,2,ok, +Pier / foundation,Steel-concrete hybrid,alternative,3,3,3,3,2,3,2.9,3,ok, +Enclosure,Roll-off roof,baseline,3,3,5,5,3,5,3.92,1,ok,SELECTED +Enclosure,Clamshell dome,alternative,3,3,2,2,2,2,2.44,3,ok, +Enclosure,Roll-off + active thermal,bold,3,4,4,4,5,4,3.88,2,ok, +Mount-head frame / housings,6061-T6 CNC plates,baseline,3,3,4,4,4,4,3.56,1,ok,SELECTED +Mount-head frame / housings,Steel weldment,alternative,4,3,3,2,3,3,3.07,2,ok, +Mount-head frame / housings,Cast housings,bold,4,4,2,1,3,2,2.83,3,ok, diff --git a/design/mechanical/tradestudy/sensitivity.csv b/design/mechanical/tradestudy/sensitivity.csv new file mode 100644 index 0000000..54d60ad --- /dev/null +++ b/design/mechanical/tradestudy/sensitivity.csv @@ -0,0 +1,24 @@ +subsystem,option,role,w_default,w_precision,w_cost,win_default,win_precision,win_cost +Optical tube assembly,MN78 f/8 (14 kg),baseline,2.94,3.16,2.86,,, +Optical tube assembly,MN76 f/6 (9 kg),alternative,3.21,3.18,3.07,,, +Optical tube assembly,MN86 f/6 (8-inch),alternative,2.16,2.56,1.96,,, +Optical tube assembly,APM-LZOS apo triplet,bold,2.76,3.28,2.18,,, +Optical tube assembly,ES-MN152 f/4.8,alternative,3.99,3.74,4.37,*,*,* +Mount topology,Counterweighted GEM,baseline,3.31,3.22,3.35,,, +Mount topology,Counterweight-FREE GEM,bold,3.68,3.72,3.8,*,*,* +Mount topology,Fork + field derotator,alternative,2.32,2.28,2.2,,, +Axis drive train,NEMA17 + 27:1 + harmonic,baseline,3.24,2.92,3.6,*,,* +Axis drive train,Direct-to-harmonic,alternative,3.16,3.12,3.2,,, +Axis drive train,Torque-motor direct drive,bold,2.73,3.3,2.07,,*, +Feedback / encoder chain,AMT103 motor + AS5600 axis,baseline,3.33,2.74,4.05,,, +Feedback / encoder chain,On-axis high-res absolute ring,bold,2.83,3.38,2.15,,*, +Feedback / encoder chain,Hybrid (motor + on-axis absolute),alternative,3.07,3.3,2.75,*,,* +Pier / foundation,Concrete Sonotube,baseline,4.09,3.82,4.45,*,*,* +Pier / foundation,Isolated pier-in-pier,bold,3.7,3.8,3.5,,, +Pier / foundation,Steel-concrete hybrid,alternative,2.9,2.92,2.92,,, +Enclosure,Roll-off roof,baseline,3.92,3.64,4.4,*,,* +Enclosure,Clamshell dome,alternative,2.44,2.6,2.22,,, +Enclosure,Roll-off + active thermal,bold,3.88,3.88,3.96,,*, +Mount-head frame / housings,6061-T6 CNC plates,baseline,3.56,3.4,3.78,*,*,* +Mount-head frame / housings,Steel weldment,alternative,3.07,3.1,2.97,,, +Mount-head frame / housings,Cast housings,bold,2.83,3.18,2.37,,,