This commit is contained in:
2026-03-03 16:43:30 +00:00
commit 03452517b5
58 changed files with 13181 additions and 0 deletions

519
src/engine/equations.js Normal file
View File

@@ -0,0 +1,519 @@
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'] },
]