Files
rocketry/src/engine/equations.js
2026-03-03 16:43:30 +00:00

520 lines
17 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.
import { machFromAreaRatio, areaRatioFromMach } from './numerics.js'
// Each equation:
// id unique key
// name display name
// formula formula string shown to user
// variables all variable ids involved
// solvers map of { variableId: fn(knownValues) => number }
// A solver may return NaN/Infinity to signal infeasibility.
export const EQUATIONS = [
// ── Thrust ─────────────────────────────────────────────────────────────
{
id: 'fundamental_thrust',
name: 'Fundamental Thrust Equation',
formula: 'F = ṁ·Vₑ + (pₑ pₐ)·Aₑ',
category: 'Thrust',
variables: ['F', 'mdot', 'Ve', 'pe', 'pa', 'Ae'],
solvers: {
F: v => v.mdot * v.Ve + (v.pe - v.pa) * v.Ae,
mdot: Object.assign(
v => (v.F - ('Ae' in v ? (v.pe - v.pa) * v.Ae : 0)) / v.Ve,
{
requires: known => {
const adapted = 'pe' in known && 'pa' in known && known.pe === known.pa
return adapted ? ['F', 'Ve', 'pe', 'pa'] : ['F', 'Ve', 'pe', 'pa', 'Ae']
},
},
),
Ve: v => (v.F - (v.pe - v.pa) * v.Ae) / v.mdot,
pe: v => (v.F - v.mdot * v.Ve) / v.Ae + v.pa,
pa: v => v.pe - (v.F - v.mdot * v.Ve) / v.Ae,
Ae: v => (v.F - v.mdot * v.Ve) / (v.pe - v.pa),
},
},
{
id: 'thrust_from_isp',
name: 'Thrust from Isp',
formula: 'F = ṁ·Isp·g₀',
category: 'Thrust',
variables: ['F', 'mdot', 'Isp', 'g0'],
solvers: {
F: v => v.mdot * v.Isp * v.g0,
mdot: v => v.F / (v.Isp * v.g0),
Isp: v => v.F / (v.mdot * v.g0),
g0: v => v.F / (v.mdot * v.Isp),
},
},
// ── Effective Exhaust Velocity ─────────────────────────────────────────
{
id: 'ceff_def',
name: 'Effective Exhaust Velocity',
formula: 'cₑ = F / ṁ',
category: 'Thrust',
variables: ['ceff', 'F', 'mdot'],
solvers: {
ceff: v => v.F / v.mdot,
F: v => v.ceff * v.mdot,
mdot: v => v.F / v.ceff,
},
},
// ── Specific Impulse ───────────────────────────────────────────────────
{
id: 'isp_from_ve',
name: 'Isp from Effective Exhaust Velocity',
formula: 'Isp = cₑ / g₀',
category: 'Specific Impulse',
variables: ['Isp', 'ceff', 'g0'],
solvers: {
Isp: v => v.ceff / v.g0,
ceff: v => v.Isp * v.g0,
g0: v => v.ceff / v.Isp,
},
},
{
id: 'isp_from_thrust',
name: 'Isp from Thrust & Mass Flow',
formula: 'Isp = F / (ṁ·g₀)',
category: 'Specific Impulse',
variables: ['Isp', 'F', 'mdot', 'g0'],
solvers: {
Isp: v => v.F / (v.mdot * v.g0),
F: v => v.Isp * v.mdot * v.g0,
mdot: v => v.F / (v.Isp * v.g0),
g0: v => v.F / (v.Isp * v.mdot),
},
},
// ── Characteristic Velocity ────────────────────────────────────────────
{
id: 'cstar_def',
name: 'Characteristic Velocity',
formula: 'c* = p₀·Aₜ / ṁ',
category: 'Nozzle Performance',
variables: ['cstar', 'p0', 'At', 'mdot'],
solvers: {
cstar: v => (v.p0 * v.At) / v.mdot,
p0: v => (v.cstar * v.mdot) / v.At,
At: v => (v.cstar * v.mdot) / v.p0,
mdot: v => (v.p0 * v.At) / v.cstar,
},
},
{
id: 'cstar_from_cf_isp',
name: 'c* from Thrust Coefficient & Isp',
formula: 'c* = Isp·g₀ / Cꜰ',
category: 'Nozzle Performance',
variables: ['cstar', 'Isp', 'g0', 'CF'],
solvers: {
cstar: v => (v.Isp * v.g0) / v.CF,
Isp: v => (v.cstar * v.CF) / v.g0,
g0: v => (v.cstar * v.CF) / v.Isp,
CF: v => (v.Isp * v.g0) / v.cstar,
},
},
// ── Thrust Coefficient ─────────────────────────────────────────────────
{
id: 'thrust_coefficient',
name: 'Thrust Coefficient',
formula: 'Cꜰ = F / (p₀·Aₜ)',
category: 'Nozzle Performance',
variables: ['CF', 'F', 'p0', 'At'],
solvers: {
CF: v => v.F / (v.p0 * v.At),
F: v => v.CF * v.p0 * v.At,
p0: v => v.F / (v.CF * v.At),
At: v => v.F / (v.CF * v.p0),
},
},
// ── Tsiolkovsky Rocket Equation ────────────────────────────────────────
{
id: 'tsiolkovsky_ve',
name: 'Tsiolkovsky Rocket Equation (cₑ)',
formula: 'Δv = cₑ·ln(m₀/mf)',
category: 'Rocket Equation',
variables: ['dv', 'ceff', 'm0', 'mf'],
solvers: {
dv: v => v.ceff * Math.log(v.m0 / v.mf),
ceff: v => v.dv / Math.log(v.m0 / v.mf),
m0: v => v.mf * Math.exp(v.dv / v.ceff),
mf: v => v.m0 / Math.exp(v.dv / v.ceff),
},
},
{
id: 'tsiolkovsky_isp',
name: 'Tsiolkovsky Rocket Equation (Isp)',
formula: 'Δv = Isp·g₀·ln(m₀/mf)',
category: 'Rocket Equation',
variables: ['dv', 'Isp', 'g0', 'm0', 'mf'],
solvers: {
dv: v => v.Isp * v.g0 * Math.log(v.m0 / v.mf),
Isp: v => v.dv / (v.g0 * Math.log(v.m0 / v.mf)),
g0: v => v.dv / (v.Isp * Math.log(v.m0 / v.mf)),
m0: v => v.mf * Math.exp(v.dv / (v.Isp * v.g0)),
mf: v => v.m0 / Math.exp(v.dv / (v.Isp * v.g0)),
},
},
{
id: 'mass_ratio',
name: 'Mass Ratio',
formula: 'MR = m₀ / mf',
category: 'Rocket Equation',
variables: ['MR', 'm0', 'mf'],
solvers: {
MR: v => v.m0 / v.mf,
m0: v => v.MR * v.mf,
mf: v => v.m0 / v.MR,
},
},
{
id: 'propellant_mass',
name: 'Propellant Mass',
formula: 'mₚ = m₀ mf',
category: 'Rocket Equation',
variables: ['mp', 'm0', 'mf'],
solvers: {
mp: v => v.m0 - v.mf,
m0: v => v.mp + v.mf,
mf: v => v.m0 - v.mp,
},
},
{
id: 'mass_fraction',
name: 'Propellant Mass Fraction',
formula: 'ζ = mₚ / m₀',
category: 'Rocket Equation',
variables: ['zeta', 'mp', 'm0'],
solvers: {
zeta: v => v.mp / v.m0,
mp: v => v.zeta * v.m0,
m0: v => v.mp / v.zeta,
},
},
{
id: 'burn_time',
name: 'Burn Time',
formula: 'tᵦ = mₚ / ṁ',
category: 'Rocket Equation',
variables: ['tb', 'mp', 'mdot'],
solvers: {
tb: v => v.mp / v.mdot,
mp: v => v.tb * v.mdot,
mdot: v => v.mp / v.tb,
},
},
// ── Isentropic Flow Relations ──────────────────────────────────────────
{
id: 'isentropic_temp',
name: 'Isentropic Temperature Ratio',
formula: 'T/T₀ = (1 + (γ1)/2·M²)⁻¹',
category: 'Isentropic Flow',
variables: ['T', 'T0', 'M', 'gamma'],
solvers: {
T: v => v.T0 / (1 + (v.gamma - 1) / 2 * v.M * v.M),
T0: v => v.T * (1 + (v.gamma - 1) / 2 * v.M * v.M),
M: v => Math.sqrt((v.T0 / v.T - 1) * 2 / (v.gamma - 1)),
gamma: v => {
// T/T0 = 1/(1 + (γ-1)/2·M²) → T0/T - 1 = (γ-1)/2·M²
// γ = 1 + 2(T0/T - 1)/M²
return 1 + 2 * (v.T0 / v.T - 1) / (v.M * v.M)
},
},
},
{
id: 'isentropic_pressure',
name: 'Isentropic Pressure Ratio',
formula: 'p/p₀ = (1 + (γ1)/2·M²)^(γ/(γ1))',
category: 'Isentropic Flow',
variables: ['p_static', 'p0', 'M', 'gamma'],
solvers: {
p_static: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p0 / Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
p0: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p_static * Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
M: v => {
// p0/p = (1 + (γ-1)/2·M²)^(γ/(γ-1))
// (p0/p)^((γ-1)/γ) = 1 + (γ-1)/2·M²
const ratio = Math.pow(v.p0 / v.p_static, (v.gamma - 1) / v.gamma)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'isentropic_density',
name: 'Isentropic Density Ratio',
formula: 'ρ/ρ₀ = (1 + (γ1)/2·M²)^(1/(γ1))',
category: 'Isentropic Flow',
variables: ['rho', 'rho0', 'M', 'gamma'],
solvers: {
rho: v => {
const exp = 1 / (v.gamma - 1)
return v.rho0 / Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
rho0: v => {
const exp = 1 / (v.gamma - 1)
return v.rho * Math.pow(1 + (v.gamma - 1) / 2 * v.M * v.M, exp)
},
M: v => {
const ratio = Math.pow(v.rho0 / v.rho, v.gamma - 1)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'speed_of_sound',
name: 'Speed of Sound',
formula: 'a = √(γ·R·T)',
category: 'Isentropic Flow',
variables: ['a_sound', 'gamma', 'R', 'T'],
solvers: {
a_sound: v => Math.sqrt(v.gamma * v.R * v.T),
T: v => (v.a_sound * v.a_sound) / (v.gamma * v.R),
R: v => (v.a_sound * v.a_sound) / (v.gamma * v.T),
gamma: v => (v.a_sound * v.a_sound) / (v.R * v.T),
},
},
{
id: 'flow_velocity',
name: 'Flow Velocity',
formula: 'v = M·a',
category: 'Isentropic Flow',
variables: ['v_flow', 'M', 'a_sound'],
solvers: {
v_flow: v => v.M * v.a_sound,
M: v => v.v_flow / v.a_sound,
a_sound: v => v.v_flow / v.M,
},
},
// ── Nozzle Geometry ────────────────────────────────────────────────────
{
id: 'expansion_ratio',
name: 'Nozzle Expansion Ratio',
formula: 'ε = Aₑ / Aₜ',
category: 'Nozzle Geometry',
variables: ['eps', 'Ae', 'At'],
solvers: {
eps: v => v.Ae / v.At,
Ae: v => v.eps * v.At,
At: v => v.Ae / v.eps,
},
},
{
id: 'area_ratio_mach',
name: 'Isentropic Area Ratio (supersonic)',
formula: 'ε = (1/Mₑ)·[(2/(γ+1))·(1+(γ1)/2·Mₑ²)]^((γ+1)/(2(γ1)))',
category: 'Nozzle Geometry',
variables: ['eps', 'Me', 'gamma'],
solvers: {
eps: v => areaRatioFromMach(v.Me, v.gamma),
Me: v => machFromAreaRatio(v.eps, v.gamma, true),
},
},
{
id: 'choked_mass_flow',
name: 'Choked Throat Mass Flow',
formula: 'ṁ = Aₜ·p₀·√(γ/(R·T₀))·(2/(γ+1))^((γ+1)/(2(γ1)))',
category: 'Nozzle Geometry',
variables: ['mdot', 'At', 'p0', 'gamma', 'R', 'T0'],
solvers: {
mdot: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
return v.At * v.p0 * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
},
At: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.p0 * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
return v.mdot / coeff
},
p0: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.At * Math.sqrt(v.gamma / (v.R * v.T0)) * Math.pow(2 / (v.gamma + 1), exp)
return v.mdot / coeff
},
T0: v => {
const exp = (v.gamma + 1) / (2 * (v.gamma - 1))
const coeff = v.At * v.p0 * Math.pow(2 / (v.gamma + 1), exp)
// mdot = coeff * sqrt(gamma/(R*T0))
// mdot/coeff = sqrt(gamma/(R*T0))
// (mdot/coeff)^2 = gamma/(R*T0)
// T0 = gamma/(R * (mdot/coeff)^2)
const ratio = v.mdot / coeff
return v.gamma / (v.R * ratio * ratio)
},
},
},
// ── Exit Conditions ────────────────────────────────────────────────────
// Order matters for the greedy solver: exit_pressure must come before
// exit_temperature so that Mₑ is in `known` when exit_temperature runs.
// If exit_pressure appeared after exit_temperature, the solver would
// reach exit_velocity first (with Mₑ freshly set) and use Vₑ = Isp·g₀
// to back-calculate an unphysical Tₑ > T₀.
{
id: 'exit_pressure',
name: 'Nozzle Exit Pressure',
formula: 'pₑ = p₀·(1 + (γ1)/2·Mₑ²)^(γ/(γ1))',
category: 'Nozzle Geometry',
variables: ['pe', 'p0', 'Me', 'gamma'],
solvers: {
pe: v => {
const exp = v.gamma / (v.gamma - 1)
return v.p0 / Math.pow(1 + (v.gamma - 1) / 2 * v.Me * v.Me, exp)
},
p0: v => {
const exp = v.gamma / (v.gamma - 1)
return v.pe * Math.pow(1 + (v.gamma - 1) / 2 * v.Me * v.Me, exp)
},
Me: v => {
const ratio = Math.pow(v.p0 / v.pe, (v.gamma - 1) / v.gamma)
return Math.sqrt((ratio - 1) * 2 / (v.gamma - 1))
},
},
},
{
id: 'exit_temperature',
name: 'Nozzle Exit Temperature',
formula: 'Tₑ = T₀ / (1 + (γ1)/2·Mₑ²)',
category: 'Nozzle Geometry',
variables: ['Te', 'T0', 'Me', 'gamma'],
solvers: {
Te: v => v.T0 / (1 + (v.gamma - 1) / 2 * v.Me * v.Me),
T0: v => v.Te * (1 + (v.gamma - 1) / 2 * v.Me * v.Me),
Me: v => Math.sqrt((v.T0 / v.Te - 1) * 2 / (v.gamma - 1)),
gamma: v => 1 + 2 * (v.T0 / v.Te - 1) / (v.Me * v.Me),
},
},
{
id: 'exit_velocity',
name: 'Nozzle Exit Velocity',
formula: 'Vₑ = Mₑ·√(γ·R·Tₑ)',
category: 'Nozzle Geometry',
variables: ['Ve', 'Me', 'gamma', 'R', 'Te'],
solvers: {
Ve: v => v.Me * Math.sqrt(v.gamma * v.R * v.Te),
Me: v => v.Ve / Math.sqrt(v.gamma * v.R * v.Te),
Te: v => (v.Ve / v.Me) ** 2 / (v.gamma * v.R),
R: v => (v.Ve / v.Me) ** 2 / (v.gamma * v.Te),
gamma: v => (v.Ve / v.Me) ** 2 / (v.R * v.Te),
},
},
// ── Performance ────────────────────────────────────────────────────────
{
id: 'twr',
name: 'Thrust-to-Weight Ratio',
formula: 'TWR = F / (mᵥ·g₀)',
category: 'Performance',
variables: ['TWR', 'F', 'm_vehicle', 'g0'],
solvers: {
TWR: v => v.F / (v.m_vehicle * v.g0),
F: v => v.TWR * v.m_vehicle * v.g0,
m_vehicle: v => v.F / (v.TWR * v.g0),
g0: v => v.F / (v.TWR * v.m_vehicle),
},
},
{
id: 'total_impulse',
name: 'Total Impulse',
formula: 'J = F·tᵦ',
category: 'Performance',
variables: ['J', 'F', 'tb'],
solvers: {
J: v => v.F * v.tb,
F: v => v.J / v.tb,
tb: v => v.J / v.F,
},
},
{
id: 'isp_from_impulse',
name: 'Isp from Total Impulse',
formula: 'Isp = J / (mₚ·g₀)',
category: 'Performance',
variables: ['Isp', 'J', 'mp', 'g0'],
solvers: {
Isp: v => v.J / (v.mp * v.g0),
J: v => v.Isp * v.mp * v.g0,
mp: v => v.J / (v.Isp * v.g0),
g0: v => v.J / (v.Isp * v.mp),
},
},
// ── Propellant ─────────────────────────────────────────────────────────
{
id: 'of_ratio',
name: 'Oxidiser/Fuel Ratio',
formula: 'O/F = ṁₒₓ / ṁf',
category: 'Propellant',
variables: ['OF', 'mdot_ox', 'mdot_f'],
solvers: {
OF: v => v.mdot_ox / v.mdot_f,
mdot_ox: v => v.OF * v.mdot_f,
mdot_f: v => v.mdot_ox / v.OF,
},
},
{
id: 'total_mass_flow',
name: 'Total Mass Flow',
formula: 'ṁ = ṁₒₓ + ṁf',
category: 'Propellant',
variables: ['mdot', 'mdot_ox', 'mdot_f'],
solvers: {
mdot: v => v.mdot_ox + v.mdot_f,
mdot_ox: v => v.mdot - v.mdot_f,
mdot_f: v => v.mdot - v.mdot_ox,
},
},
{
id: 'of_mass_split',
name: 'Mass Flow Split from O/F',
formula: 'ṁ_f = ṁ/(1+OF), ṁ_ox = ṁ·OF/(1+OF)',
category: 'Propellant',
variables: ['mdot', 'OF', 'mdot_f', 'mdot_ox'],
solvers: {
mdot_f: Object.assign(v => v.mdot / (1 + v.OF), { requires: () => ['mdot', 'OF'] }),
mdot_ox: Object.assign(v => v.mdot * v.OF / (1 + v.OF), { requires: () => ['mdot', 'OF'] }),
},
},
]
// Equation presets: named groups that seed the workspace with a useful set of variables
export const EQUATION_PRESETS = [
{ id: 'fundamental_thrust', label: 'Fundamental Thrust', equationIds: ['fundamental_thrust'] },
{ id: 'rocket_equation', label: 'Rocket Equation', equationIds: ['tsiolkovsky_isp', 'mass_ratio', 'propellant_mass', 'burn_time'] },
{ id: 'nozzle_full', label: 'Full Nozzle', equationIds: ['exit_pressure', 'exit_temperature', 'exit_velocity', 'area_ratio_mach', 'choked_mass_flow', 'thrust_coefficient', 'cstar_def'] },
{ id: 'isentropic', label: 'Isentropic Flow', equationIds: ['isentropic_temp', 'isentropic_pressure', 'speed_of_sound', 'flow_velocity'] },
{ id: 'performance', label: 'Performance', equationIds: ['twr', 'total_impulse', 'isp_from_impulse', 'isp_from_thrust'] },
{ id: 'propellant', label: 'Propellant Mix', equationIds: ['of_ratio', 'total_mass_flow', 'burn_time'] },
]