357 lines
13 KiB
JavaScript
357 lines
13 KiB
JavaScript
// 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,
|
||
},
|
||
]
|
||
}
|