Files
rocketry/src/engine/rocketDesignCalcs.js
2026-03-04 19:40:46 +00:00

357 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
},
]
}