// Pure calculation functions for rocket vehicle design (no React) import { STRUCTURAL_MATERIALS } from './knowledgebaseData.js' const G0 = 9.80665 // m/s² standard gravity const R_HE = 2077 // J/kg/K — specific gas constant for helium const T_PRESSURANT = 293 // K — standard pressurant storage temperature /** * Generate nose cone profile points (r vs x) for a given shape. * All shapes share L = 2R. * * @param {string} shape — 'conical' | 'tangentOgive' | 'vonKarman' * @param {number} R — base radius (m) * @param {number} L — nose length (m) * @param {number} n — number of points (default 24) * @returns {Array<{x, r}>} profile points from tip (x=0) to base (x=L) */ function noseConeProfile(shape, R, L, n = 24) { const points = [] for (let i = 0; i < n; i++) { const x = (i / (n - 1)) * L let r if (shape === 'tangentOgive') { // Tangent ogive: ρ = (R² + L²) / (2R) const rho = (R * R + L * L) / (2 * R) r = Math.sqrt(rho * rho - (L - x) * (L - x)) - (rho - R) } else if (shape === 'vonKarman') { // Von Kármán (LD-Haack): θ = acos(1 − 2x/L), r = (R/√π) * √(θ − sin(2θ)/2) const theta = Math.acos(1 - 2 * x / L) r = (R / Math.sqrt(Math.PI)) * Math.sqrt(theta - Math.sin(2 * theta) / 2) } else { // Default conical: r(x) = R * (x / L) r = R * (x / L) } points.push({ x, r: Math.max(0, r) }) } return points } /** * Calculate full rocket geometry and performance from engine data and rocket inputs. * * @param {object} engineData — results section from engine JSON (allThermo, nozzleGeometry, etc.) * @param {object} rocketInputs — { * outerRadius, // m — vehicle outer radius * burnTime, // s * arrangement, // 'coaxial' | 'tandem' * innerPropellant, // 'fuel' | 'ox' (coaxial only — which propellant goes in inner tank) * rhoFuel, // kg/m³ * rhoOx, // kg/m³ * payloadMass, // kg * payloadBayLength, // m * tankMaterialId, // 'al_6061_t6' | 'ss_304' | ... (structural material) * tankSafetyFactor, // safety factor for hoop stress * feedSystem, // 'pressure_fed' | 'pump_fed' * ullagePercent, // % extra volume above propellant for gases * otherStructFraction, // fraction of propellant mass for non-tank structure * engineDryMass, // kg (from engine structural results, null if not available) * noseConeShape, // 'conical' | 'tangentOgive' | 'vonKarman' * } * @returns {object|null} */ export function calcRocketGeometry(engineData, rocketInputs) { if (!engineData || !rocketInputs) return null const { outerRadius, burnTime, arrangement, innerPropellant, rhoFuel, rhoOx, payloadMass, payloadBayLength, tankMaterialId, tankSafetyFactor, feedSystem, ullagePercent, otherStructFraction, engineDryMass, noseConeShape, } = rocketInputs const Isp = engineData?.allThermo?.Isp const F = engineData?.allThermo?.F const mdot = engineData?.allThermo?.mdot const OF = engineData?.allThermo?.OF const p0 = engineData?.allThermo?.p0 ?? 6.9e6 // Chamber pressure, default 6.9 MPa // Flow rates: use direct values if solved, otherwise derive from mdot + OF let mdot_f = engineData?.allThermo?.mdot_f let mdot_ox = engineData?.allThermo?.mdot_ox if (mdot && OF && isFinite(mdot) && isFinite(OF) && OF > 0) { if (!mdot_f || !isFinite(mdot_f)) mdot_f = mdot / (1 + OF) if (!mdot_ox || !isFinite(mdot_ox)) mdot_ox = mdot * OF / (1 + OF) } // Require at minimum: radius, propellant densities, and flow rates from an engine if ( !outerRadius || outerRadius <= 0 || !rhoFuel || !rhoOx || !mdot_f || !mdot_ox || !isFinite(mdot_f) || !isFinite(mdot_ox) ) return null const R = outerRadius const tb = (burnTime && burnTime > 0) ? burnTime : 30 // ── Propellant volumes ────────────────────────────────────────────── const m_fuel = mdot_f * tb const m_ox = mdot_ox * tb const V_fuel = m_fuel / rhoFuel const V_ox = m_ox / rhoOx const V_prop = V_fuel + V_ox // ── Nose cone (height = 2R) ─────────────────────────────────────────── const L_nose = 2 * R const noseProfilePoints = noseConeProfile(noseConeShape ?? 'conical', R, L_nose, 24) // ── Payload bay ───────────────────────────────────────────────────── const L_payload = payloadBayLength ?? 0 // ── Engine section (from nozzle geometry if available) ────────────── const ng = engineData?.nozzleGeometry const cg = engineData?.chamberGeometry const L_engine = (ng?.Ln ?? 0) + (cg?.Lc ?? 0) // ── Tank geometry with domes and ullage ────────────────────────────── let L_tank_fuel, L_tank_fuel_cyl, L_tank_ox, L_tank_ox_cyl, L_tank, r_inner, m_tank_fuel, m_tank_ox, m_tank_total let t_wall = null const ullage = ullagePercent ?? 5 const materialId = tankMaterialId ?? 'al_6061_t6' const material = STRUCTURAL_MATERIALS.find(m => m.id === materialId) || STRUCTURAL_MATERIALS[0] const SF = tankSafetyFactor ?? 2.0 // Tank pressure (Pa) const p_tank = feedSystem === 'pump_fed' ? 2e6 : p0 * 1.2 // Hoop stress wall thickness (same for all tanks at same pressure and radius) t_wall = (p_tank * R) / (material.yieldStrength / SF) if (arrangement === 'coaxial') { // Coaxial: hemispherical domes only on outer tank; inner tank is annular (flat bulkheads) const V_inner = innerPropellant === 'fuel' ? V_fuel : V_ox const V_outer = innerPropellant === 'fuel' ? V_ox : V_fuel r_inner = R * Math.sqrt(V_inner / V_prop) if (r_inner >= R) r_inner = R * 0.7 // Outer tank: account for dome volume const V_dome_outer = (4 / 3) * Math.PI * R * R * R const V_eff_outer = V_outer * (1 + ullage / 100) const L_tank_cyl_outer = Math.max(0, (V_eff_outer - V_dome_outer) / (Math.PI * R * R)) // Inner annular tank (no domes): full volume in cylindrical section const A_annulus = Math.PI * (R * R - r_inner * r_inner) const L_tank_cyl_inner = V_inner / A_annulus // Total outer tank length const L_tank_outer = L_tank_cyl_outer + 2 * R // Both tanks are coaxial, so effective length is the longer one L_tank = Math.max(L_tank_outer, L_tank_cyl_inner) // For compatibility with tandem display L_tank_fuel = L_tank L_tank_fuel_cyl = L_tank_cyl_outer L_tank_ox = L_tank L_tank_ox_cyl = L_tank_cyl_inner // Coaxial tank mass: outer tank with domes + inner annular section const m_cyl_outer = 2 * Math.PI * R * t_wall * L_tank_cyl_outer * material.density const m_dome_outer = 4 * Math.PI * R * R * t_wall * material.density const m_tank_outer = m_cyl_outer + m_dome_outer const m_cyl_inner = 2 * Math.PI * r_inner * t_wall * L_tank_cyl_inner * material.density const m_tank_inner = m_cyl_inner // Inner annular tank, no domes m_tank_total = m_tank_outer + m_tank_inner } else { // Tandem: both tanks have hemispherical domes // Effective volumes include ullage const V_eff_fuel = V_fuel * (1 + ullage / 100) const V_eff_ox = V_ox * (1 + ullage / 100) // Dome volume for each tank const V_dome_one = (4 / 3) * Math.PI * R * R * R // Cylindrical sections L_tank_fuel_cyl = Math.max(0, (V_eff_fuel - V_dome_one) / (Math.PI * R * R)) L_tank_ox_cyl = Math.max(0, (V_eff_ox - V_dome_one) / (Math.PI * R * R)) // Total tank lengths including domes L_tank_fuel = L_tank_fuel_cyl + 2 * R L_tank_ox = L_tank_ox_cyl + 2 * R L_tank = L_tank_fuel + L_tank_ox r_inner = null // Tank mass: cylindrical + dome shells (two hemispheres = full sphere surface area) m_tank_fuel = (2 * Math.PI * R * t_wall * L_tank_fuel_cyl * material.density) + (4 * Math.PI * R * R * t_wall * material.density) m_tank_ox = (2 * Math.PI * R * t_wall * L_tank_ox_cyl * material.density) + (4 * Math.PI * R * R * t_wall * material.density) m_tank_total = m_tank_fuel + m_tank_ox } // ── Pressurant system (pressure-fed only) ───────────────────────────── let m_pressurant = 0 if (feedSystem === 'pressure_fed') { const m_He = (p_tank * V_prop) / (R_HE * T_PRESSURANT) const m_bottle = m_He * 4 // Conservative bottle mass estimate m_pressurant = m_He + m_bottle } // ── Total vehicle length ──────────────────────────────────────────── const totalLength = L_nose + L_payload + L_tank + L_engine // ── Mass budget ───────────────────────────────────────────────────── const m_prop = m_fuel + m_ox const m_other_struct = m_prop * (otherStructFraction ?? 0.05) const m_struct = m_tank_total + m_other_struct const m_payload = payloadMass ?? 0 const m_engine = engineDryMass ?? 0 const m_dry = m_struct + m_payload + m_engine + m_pressurant const m_wet = m_prop + m_dry const massRatio = m_wet / m_dry // ── Tsiolkovsky delta-v ───────────────────────────────────────────── const deltaV = Isp && isFinite(Isp) && massRatio > 1 ? Isp * G0 * Math.log(massRatio) : null // ── TWR at liftoff ────────────────────────────────────────────────── const TWR = F && isFinite(F) && m_wet > 0 ? F / (m_wet * G0) : null return { // Dimensions outerRadius: R, L_nose, L_payload, L_tank, L_tank_fuel, L_tank_ox, L_tank_fuel_cyl, L_tank_ox_cyl, L_engine, totalLength, r_inner, // Mass m_fuel, m_ox, V_fuel, V_ox, m_prop, m_struct, m_payload, m_engine, m_dry, m_wet, m_tank_total, m_pressurant, // Tank structure t_wall, // Performance massRatio, deltaV, TWR, // Config arrangement, innerPropellant: arrangement === 'coaxial' ? innerPropellant : null, // Nozzle geometry for 3D model (null → fall back to a generic flare) nozzleExitRadius: ng?.re ?? null, nozzleThroatRadius: ng?.rt ?? null, chamberLength: cg?.Lc ?? 0, nozzleLength: ng?.Ln ?? 0, // Nose cone profile for 3D model noseProfilePoints, } } /** * Returns a list of { key, label, ok, value } requirement objects so the UI * can show exactly what is missing before the calc can run. */ export function diagnoseRocketInputs(engineCalcData, rocketInputs) { const t = engineCalcData?.allThermo ?? {} const mdot = t.mdot const OF = t.OF const mdot_f_raw = t.mdot_f const mdot_ox_raw = t.mdot_ox // Derived flow rates (same logic as calcRocketGeometry) let mdot_f = mdot_f_raw let mdot_ox = mdot_ox_raw if (mdot && OF && isFinite(mdot) && isFinite(OF) && OF > 0) { if (!mdot_f || !isFinite(mdot_f)) mdot_f = mdot / (1 + OF) if (!mdot_ox || !isFinite(mdot_ox)) mdot_ox = mdot * OF / (1 + OF) } const R = rocketInputs?.outerRadius const bt = rocketInputs?.burnTime const fmt = v => (v != null && isFinite(v)) ? v.toPrecision(4) : null return [ { key: 'mdot', label: 'Total mass flow (ṁ)', ok: !!(mdot && isFinite(mdot)), value: fmt(mdot), hint: 'Enter ṁ or Thrust + Isp on the Engine page', }, { key: 'OF', label: 'O/F ratio', ok: !!(OF && isFinite(OF) && OF > 0), value: fmt(OF), hint: 'Enter O/F ratio on the Engine page', }, { key: 'mdot_f', label: 'Fuel flow rate (ṁ_f)', ok: !!(mdot_f && isFinite(mdot_f)), value: fmt(mdot_f), hint: mdot && OF ? 'Derived from ṁ + O/F' : 'Needs ṁ and O/F both solved', }, { key: 'mdot_ox', label: 'Oxidizer flow rate (ṁ_ox)', ok: !!(mdot_ox && isFinite(mdot_ox)), value: fmt(mdot_ox), hint: mdot && OF ? 'Derived from ṁ + O/F' : 'Needs ṁ and O/F both solved', }, { key: 'outerRadius', label: 'Outer diameter', ok: !!(R && R > 0), value: R ? `${(R * 2000).toFixed(0)} mm` : null, hint: 'Enter outer diameter in Vehicle Geometry section', }, { key: 'burnTime', label: 'Burn time', ok: true, // always ok — falls back to 30 s value: bt ? `${bt} s` : '30 s (default)', hint: null, }, ] }