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'] }, ]