init
This commit is contained in:
163
src/engine/engineDesignCalcs.js
Normal file
163
src/engine/engineDesignCalcs.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// Pure calculation functions for engine design (no React)
|
||||
|
||||
/**
|
||||
* Combustion chamber geometry from thermodynamic results and design inputs.
|
||||
* Returns null if required thermodynamic values are not yet available.
|
||||
*/
|
||||
export function calcChamber(thermo, chamber) {
|
||||
const At = thermo.At
|
||||
if (!At || !isFinite(At)) return null
|
||||
|
||||
const { Lstar, contractionRatio, convAngleDeg } = chamber
|
||||
if (!Lstar || !contractionRatio || !convAngleDeg) return null
|
||||
|
||||
const Ac = contractionRatio * At
|
||||
const rc = Math.sqrt(Ac / Math.PI)
|
||||
const rt = Math.sqrt(At / Math.PI)
|
||||
const Dc = 2 * rc
|
||||
const Dt = 2 * rt
|
||||
|
||||
const Vc = Lstar * At
|
||||
const theta = (convAngleDeg * Math.PI) / 180
|
||||
const L_conv = (rc - rt) / Math.tan(theta)
|
||||
const V_conv = (Math.PI / 3) * L_conv * (rc * rc + rc * rt + rt * rt)
|
||||
const L_cyl = Math.max(0, (Vc - V_conv) / Ac)
|
||||
const Lc = L_cyl + L_conv
|
||||
|
||||
return { Dc, Dt, rc, rt, Ac, At, Lc, L_cyl, L_conv, Vc, V_conv, contractionRatio }
|
||||
}
|
||||
|
||||
/**
|
||||
* Nozzle geometry from thermodynamic results and nozzle design inputs.
|
||||
* Returns null if required thermodynamic values are not yet available.
|
||||
*/
|
||||
export function calcNozzle(thermo, nozzle) {
|
||||
const At = thermo.At
|
||||
const Ae = thermo.Ae
|
||||
if (!At || !Ae || !isFinite(At) || !isFinite(Ae)) return null
|
||||
|
||||
const rt = Math.sqrt(At / Math.PI)
|
||||
const re = Math.sqrt(Ae / Math.PI)
|
||||
const Dt = 2 * rt
|
||||
const De = 2 * re
|
||||
|
||||
const { type, divAngleDeg } = nozzle
|
||||
let Ln
|
||||
if (type === 'bell') {
|
||||
// 80% bell nozzle length relative to equivalent conical at 15°
|
||||
Ln = 0.8 * (re - rt) / Math.tan((15 * Math.PI) / 180)
|
||||
} else {
|
||||
// conical
|
||||
const theta = (divAngleDeg * Math.PI) / 180
|
||||
Ln = (re - rt) / Math.tan(theta)
|
||||
}
|
||||
|
||||
return { Dt, De, rt, re, Ln, type }
|
||||
}
|
||||
|
||||
/**
|
||||
* Injector orifice sizing for a given element type.
|
||||
* Returns null if required thermodynamic values are not yet available.
|
||||
*/
|
||||
export function calcInjector(thermo, injector) {
|
||||
const { mdot_f, mdot_ox, p0 } = thermo
|
||||
if (!mdot_f || !mdot_ox || !p0 || !isFinite(mdot_f) || !isFinite(mdot_ox) || !isFinite(p0)) return null
|
||||
|
||||
const { type, N, dpFraction, Cd, rhoFuel, rhoOx } = injector
|
||||
if (!N || !dpFraction || !Cd || !rhoFuel || !rhoOx) return null
|
||||
|
||||
const deltaP = dpFraction * p0
|
||||
|
||||
const v_f = Cd * Math.sqrt(2 * deltaP / rhoFuel)
|
||||
const v_ox = Cd * Math.sqrt(2 * deltaP / rhoOx)
|
||||
|
||||
// N elements, each element has one fuel and one oxidiser orifice
|
||||
const A_f_each = mdot_f / (N * rhoFuel * v_f)
|
||||
const A_ox_each = mdot_ox / (N * rhoOx * v_ox)
|
||||
|
||||
const d_f = Math.sqrt(4 * A_f_each / Math.PI)
|
||||
const d_ox = Math.sqrt(4 * A_ox_each / Math.PI)
|
||||
|
||||
return { deltaP, v_f, v_ox, A_f_each, A_ox_each, d_f, d_ox, N, type }
|
||||
}
|
||||
|
||||
/**
|
||||
* Cooling analysis based on selected method.
|
||||
* For regenerative cooling: simplified Bartz heat flux estimate.
|
||||
* For film cooling: propellant fraction and Isp penalty estimate.
|
||||
* For ablative/uncooled: informational only.
|
||||
*/
|
||||
export function calcCooling(thermo, cooling, chamberGeom) {
|
||||
const { p0, T0, cstar, mdot } = thermo
|
||||
const { method } = cooling
|
||||
|
||||
if (method === 'regenerative') {
|
||||
if (!p0 || !T0 || !cstar || !chamberGeom || !isFinite(p0)) return { method }
|
||||
|
||||
const { Dt, Dc, Lc } = chamberGeom
|
||||
// Simplified Bartz heat flux [W/m²] using typical exhaust gas properties
|
||||
const mu = 6e-5 // Pa·s — typical rocket exhaust dynamic viscosity
|
||||
const cp = 2000 // J/(kg·K)
|
||||
const Pr = 0.7
|
||||
const T_wall = 800 // K — assumed hot-gas-side wall temperature
|
||||
|
||||
const q_est = (0.026 / Math.pow(Dt, 0.2)) *
|
||||
(Math.pow(mu, 0.2) * cp / Math.pow(Pr, 0.6)) *
|
||||
Math.pow(p0 / cstar, 0.8) *
|
||||
(T0 - T_wall)
|
||||
|
||||
const chamberArea = Math.PI * Dc * Lc
|
||||
const q_total = q_est * chamberArea
|
||||
|
||||
const { channelCount } = cooling
|
||||
const channelArea = channelCount > 0 ? q_total / channelCount : 0
|
||||
|
||||
return { method, q_est, q_total, channelCount, channelArea }
|
||||
}
|
||||
|
||||
if (method === 'film') {
|
||||
const { filmFraction } = cooling
|
||||
const mdot_film = (mdot || 0) * filmFraction
|
||||
// Film cooling reduces effective propellant utilisation — rough Isp penalty
|
||||
const ispPenalty = filmFraction * 100 // % estimate
|
||||
return { method, filmFraction, mdot_film, ispPenalty }
|
||||
}
|
||||
|
||||
const notes = {
|
||||
ablative: 'Ablative liner — consult manufacturer data for material thickness and char rate.',
|
||||
uncooled: 'Uncooled — confirm combustion gas temperature is within material thermal limits for the burn duration.',
|
||||
}
|
||||
return { method, note: notes[method] ?? '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed system sizing.
|
||||
* Pressure-fed: pressurant mass estimate.
|
||||
* Pump-fed: pump head and turbine power estimate.
|
||||
*/
|
||||
export function calcFeedSystem(thermo, feedSystem, burnTime) {
|
||||
const { p0, mdot_f, mdot_ox } = thermo
|
||||
if (!p0 || !mdot_f || !mdot_ox || !isFinite(p0)) return null
|
||||
|
||||
const { type, feedFactor, rhoFuel, rhoOx, pressurantR, pressurantT } = feedSystem
|
||||
const tb = burnTime && burnTime > 0 ? burnTime : 30
|
||||
|
||||
const V_fuel = (tb * mdot_f) / (rhoFuel || 800)
|
||||
const V_ox = (tb * mdot_ox) / (rhoOx || 1140)
|
||||
const V_prop = V_fuel + V_ox
|
||||
|
||||
if (type === 'pressure_fed') {
|
||||
const p_tank = p0 * (feedFactor || 1.3)
|
||||
const R_press = pressurantR || 2077 // J/(kg·K) — helium
|
||||
const T_press = pressurantT || 300 // K — ambient temperature
|
||||
const m_press = (p_tank * V_prop) / (R_press * T_press)
|
||||
return { type, p_tank, V_fuel, V_ox, V_prop, m_press }
|
||||
}
|
||||
|
||||
// pump-fed
|
||||
const p_tank = 0.5e6 // 0.5 MPa typical inlet tank pressure
|
||||
const dP_pump = p0 - p_tank
|
||||
// Power = flow-rate × pressure rise / density (simplified, single-stage)
|
||||
const P_turbine = (mdot_f / (rhoFuel || 800) + mdot_ox / (rhoOx || 1140)) * dP_pump
|
||||
return { type, p_tank, V_fuel, V_ox, V_prop, dP_pump, P_turbine }
|
||||
}
|
||||
318
src/engine/engineExportImport.js
Normal file
318
src/engine/engineExportImport.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import JSZip from 'jszip'
|
||||
import { downloadBlob } from './exportImport.js'
|
||||
import { formatValue } from './format.js'
|
||||
|
||||
export { downloadBlob }
|
||||
|
||||
/* ── JSON export / import ──────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Build an engine design JSON Blob for download.
|
||||
* Schema version 1: inputs section is re-importable; results is reference-only.
|
||||
*/
|
||||
export function exportEngineJSON({
|
||||
thermoInputs, chamber, nozzle, injector, cooling, feedSystem, burnTime,
|
||||
allThermo, chamberGeometry, nozzleGeometry, injectorGeometry, coolingResults, feedResults,
|
||||
}) {
|
||||
const payload = {
|
||||
version: 1,
|
||||
type: 'engine_design',
|
||||
exportedAt: new Date().toISOString(),
|
||||
inputs: {
|
||||
thermodynamics: thermoInputs,
|
||||
chamber,
|
||||
nozzle,
|
||||
injector,
|
||||
cooling,
|
||||
feedSystem,
|
||||
burnTime,
|
||||
},
|
||||
results: {
|
||||
thermodynamics: allThermo,
|
||||
chamberGeometry,
|
||||
nozzleGeometry,
|
||||
injectorGeometry,
|
||||
coolingResults,
|
||||
feedResults,
|
||||
},
|
||||
}
|
||||
return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an imported engine design JSON string and return the inputs section.
|
||||
* Throws a descriptive Error if the file is invalid.
|
||||
*/
|
||||
export function parseEngineImport(jsonString) {
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(jsonString)
|
||||
} catch {
|
||||
throw new Error('File is not valid JSON.')
|
||||
}
|
||||
|
||||
if (data.type !== 'engine_design') {
|
||||
throw new Error('This file is not an engine design export.')
|
||||
}
|
||||
if (data.version !== 1) {
|
||||
throw new Error(`Unsupported export version: ${data.version}`)
|
||||
}
|
||||
|
||||
return data.inputs ?? {}
|
||||
}
|
||||
|
||||
/* ── ODT XML helpers ────────────────────────────────────────────────── */
|
||||
|
||||
function xmlEscape(str) {
|
||||
return String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function cell(text) {
|
||||
return `<table:table-cell table:style-name="TableCell" office:value-type="string">` +
|
||||
`<text:p text:style-name="TableContents">${xmlEscape(text)}</text:p>` +
|
||||
`</table:table-cell>`
|
||||
}
|
||||
|
||||
function headerCell(text) {
|
||||
return `<table:table-cell table:style-name="TableHeaderCell" office:value-type="string">` +
|
||||
`<text:p text:style-name="TableHeader">${xmlEscape(text)}</text:p>` +
|
||||
`</table:table-cell>`
|
||||
}
|
||||
|
||||
function row(...cells) {
|
||||
return `<table:table-row>${cells.join('')}</table:table-row>`
|
||||
}
|
||||
|
||||
function table(name, columnCount, rows) {
|
||||
const cols = Array(columnCount)
|
||||
.fill(`<table:table-column table:style-name="TableColumn"/>`)
|
||||
.join('')
|
||||
return `<table:table table:name="${name}" table:style-name="Table">${cols}${rows}</table:table>`
|
||||
}
|
||||
|
||||
function heading(text, level = 1) {
|
||||
return `<text:h text:style-name="Heading${level}" text:outline-level="${level}">${xmlEscape(text)}</text:h>`
|
||||
}
|
||||
|
||||
function para(text = '') {
|
||||
return `<text:p text:style-name="Standard">${xmlEscape(text)}</text:p>`
|
||||
}
|
||||
|
||||
function fv(val) {
|
||||
return val !== null && val !== undefined && isFinite(val) ? formatValue(val) : '—'
|
||||
}
|
||||
|
||||
/* ── ODT styles ─────────────────────────────────────────────────────── */
|
||||
|
||||
const STYLES_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-styles
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||||
office:version="1.3">
|
||||
<office:styles>
|
||||
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
|
||||
<style:style style:name="Heading1" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-size="16pt" fo:font-weight="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading2" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-size="13pt" fo:font-weight="bold"/>
|
||||
<style:paragraph-properties fo:margin-top="6pt" fo:margin-bottom="3pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableContents" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:padding="1mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableHeader" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-weight="bold"/>
|
||||
<style:paragraph-properties fo:padding="1mm"/>
|
||||
</style:style>
|
||||
</office:styles>
|
||||
</office:document-styles>`
|
||||
|
||||
const AUTO_STYLES = `
|
||||
<style:style style:name="Table" style:family="table">
|
||||
<style:table-properties style:width="16cm" fo:margin-bottom="6mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableColumn" style:family="table-column">
|
||||
<style:table-column-properties style:column-width="5.3cm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableCell" style:family="table-cell">
|
||||
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableHeaderCell" style:family="table-cell">
|
||||
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm" fo:background-color="#e8e8e8"/>
|
||||
</style:style>`
|
||||
|
||||
const MANIFEST_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest
|
||||
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
||||
manifest:version="1.3">
|
||||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
|
||||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||||
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
||||
</manifest:manifest>`
|
||||
|
||||
/* ── ODT content builder ────────────────────────────────────────────── */
|
||||
|
||||
function buildEngineContentXml({ allThermo, chamberGeometry: cg, nozzleGeometry: ng, injectorGeometry: ig, coolingResults: cr, feedResults: fr }) {
|
||||
const exportedAt = new Date().toISOString()
|
||||
|
||||
const thermoTable = table('Thermo', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
[
|
||||
['Chamber Pressure (p0)', fv(allThermo.p0), 'Pa'],
|
||||
['Chamber Temperature (T0)', fv(allThermo.T0), 'K'],
|
||||
['Ratio of Specific Heats (g)', fv(allThermo.gamma), '-'],
|
||||
['Specific Gas Constant (R)', fv(allThermo.R), 'J/(kg K)'],
|
||||
['Mass Flow Rate (mdot)', fv(allThermo.mdot), 'kg/s'],
|
||||
['Thrust (F)', fv(allThermo.F), 'N'],
|
||||
['Specific Impulse (Isp)', fv(allThermo.Isp), 's'],
|
||||
['Characteristic Velocity (c*)', fv(allThermo.cstar), 'm/s'],
|
||||
['Thrust Coefficient (CF)', fv(allThermo.CF), '-'],
|
||||
['Throat Area (At)', fv(allThermo.At), 'm2'],
|
||||
['Exit Area (Ae)', fv(allThermo.Ae), 'm2'],
|
||||
['Expansion Ratio (eps)', fv(allThermo.eps), '-'],
|
||||
['Exit Mach Number (Me)', fv(allThermo.Me), '-'],
|
||||
['Exit Temperature (Te)', fv(allThermo.Te), 'K'],
|
||||
['Exit Pressure (pe)', fv(allThermo.pe), 'Pa'],
|
||||
['Exhaust Velocity (Ve)', fv(allThermo.Ve), 'm/s'],
|
||||
['Fuel Flow Rate (mdot_f)', fv(allThermo.mdot_f), 'kg/s'],
|
||||
['Oxidiser Flow Rate (mdot_ox)', fv(allThermo.mdot_ox), 'kg/s'],
|
||||
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
|
||||
const chamberTable = cg
|
||||
? table('Chamber', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
[
|
||||
['Chamber Diameter (Dc)', fv(cg.Dc * 1000), 'mm'],
|
||||
['Throat Diameter (Dt)', fv(cg.Dt * 1000), 'mm'],
|
||||
['Contraction Ratio', fv(cg.contractionRatio), '-'],
|
||||
['Total Chamber Length (Lc)', fv(cg.Lc * 1000), 'mm'],
|
||||
['Cylindrical Section Length', fv(cg.L_cyl * 1000), 'mm'],
|
||||
['Convergent Section Length', fv(cg.L_conv * 1000), 'mm'],
|
||||
['Chamber Volume', fv(cg.Vc * 1e6), 'cm3'],
|
||||
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
: para('(insufficient inputs to calculate chamber geometry)')
|
||||
|
||||
const nozzleTable = ng
|
||||
? table('Nozzle', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
[
|
||||
['Type', ng.type, '-'],
|
||||
['Throat Diameter (Dt)', fv(ng.Dt * 1000), 'mm'],
|
||||
['Exit Diameter (De)', fv(ng.De * 1000), 'mm'],
|
||||
['Nozzle Length (Ln)', fv(ng.Ln * 1000), 'mm'],
|
||||
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
: para('(insufficient inputs to calculate nozzle geometry)')
|
||||
|
||||
const injectorTable = ig
|
||||
? table('Injector', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
[
|
||||
['Type', ig.type, '-'],
|
||||
['Number of Elements (N)', fv(ig.N), '-'],
|
||||
['Pressure Drop (dP)', fv(ig.deltaP), 'Pa'],
|
||||
['Fuel Jet Velocity', fv(ig.v_f), 'm/s'],
|
||||
['Oxidiser Jet Velocity', fv(ig.v_ox), 'm/s'],
|
||||
['Fuel Orifice Diameter', fv(ig.d_f * 1000), 'mm'],
|
||||
['Oxidiser Orifice Diameter', fv(ig.d_ox * 1000), 'mm'],
|
||||
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
: para('(insufficient inputs to calculate injector geometry)')
|
||||
|
||||
const coolingSection = cr
|
||||
? (() => {
|
||||
const rows = [['Method', cr.method, '-']]
|
||||
if (cr.q_est != null) rows.push(['Estimated Heat Flux', fv(cr.q_est), 'W/m2'])
|
||||
if (cr.q_total != null) rows.push(['Total Heat Load', fv(cr.q_total), 'W'])
|
||||
if (cr.channelCount != null) rows.push(['Channel Count', fv(cr.channelCount), '-'])
|
||||
if (cr.filmFraction != null) rows.push(['Film Mass Fraction', fv(cr.filmFraction), '-'])
|
||||
if (cr.mdot_film != null) rows.push(['Film Mass Flow', fv(cr.mdot_film), 'kg/s'])
|
||||
if (cr.ispPenalty != null) rows.push(['Est. Isp Penalty', fv(cr.ispPenalty), '%'])
|
||||
if (cr.note) rows.push(['Note', cr.note, ''])
|
||||
return table('Cooling', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
rows.map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
})()
|
||||
: para('(insufficient inputs)')
|
||||
|
||||
const feedTable = fr
|
||||
? table('FeedSystem', 3,
|
||||
row(headerCell('Parameter'), headerCell('Value'), headerCell('Unit')) +
|
||||
[
|
||||
['Type', fr.type, '-'],
|
||||
['Tank Pressure', fv(fr.p_tank), 'Pa'],
|
||||
['Fuel Volume', fv(fr.V_fuel * 1000), 'L'],
|
||||
['Oxidiser Volume', fv(fr.V_ox * 1000), 'L'],
|
||||
['Total Propellant Volume', fv(fr.V_prop * 1000), 'L'],
|
||||
...(fr.m_press != null ? [['Pressurant Mass', fv(fr.m_press), 'kg']] : []),
|
||||
...(fr.dP_pump != null ? [['Pump Delta-P', fv(fr.dP_pump), 'Pa']] : []),
|
||||
...(fr.P_turbine != null ? [['Est. Turbine Power', fv(fr.P_turbine / 1000), 'kW']] : []),
|
||||
].map(([n, v, u]) => row(cell(n), cell(v), cell(u))).join('')
|
||||
)
|
||||
: para('(insufficient inputs)')
|
||||
|
||||
const body = [
|
||||
heading('Engine Design Report'),
|
||||
para(`Exported: ${exportedAt}`),
|
||||
para(),
|
||||
heading('1. Thermodynamic Performance', 2),
|
||||
thermoTable,
|
||||
para(),
|
||||
heading('2. Combustion Chamber', 2),
|
||||
chamberTable,
|
||||
para(),
|
||||
heading('3. Nozzle', 2),
|
||||
nozzleTable,
|
||||
para(),
|
||||
heading('4. Injector', 2),
|
||||
injectorTable,
|
||||
para(),
|
||||
heading('5. Cooling', 2),
|
||||
coolingSection,
|
||||
para(),
|
||||
heading('6. Feed System', 2),
|
||||
feedTable,
|
||||
].join('\n')
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-content
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||||
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
|
||||
office:version="1.3">
|
||||
<office:automatic-styles>${AUTO_STYLES}</office:automatic-styles>
|
||||
<office:body>
|
||||
<office:text>
|
||||
${body}
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document-content>`
|
||||
}
|
||||
|
||||
/* ── Public API ─────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Build an ODT Blob from the current engine design state.
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
export async function exportEngineOdt(designState) {
|
||||
const zip = new JSZip()
|
||||
zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' })
|
||||
zip.file('META-INF/manifest.xml', MANIFEST_XML)
|
||||
zip.file('styles.xml', STYLES_XML)
|
||||
zip.file('content.xml', buildEngineContentXml(designState))
|
||||
return zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.oasis.opendocument.text' })
|
||||
}
|
||||
512
src/engine/engineFieldInfo.js
Normal file
512
src/engine/engineFieldInfo.js
Normal file
@@ -0,0 +1,512 @@
|
||||
// Per-field descriptions for the Engine Design page info popups.
|
||||
// Each entry: { name, description, higher, lower }
|
||||
|
||||
export const ENGINE_FIELD_INFO = {
|
||||
|
||||
/* ── Thermodynamic Inputs ─────────────────────────────────────── */
|
||||
|
||||
p0: {
|
||||
name: 'Chamber Pressure (p₀)',
|
||||
description: 'Total stagnation pressure in the combustion chamber. One of the most critical design parameters — sets the thermodynamic cycle for the entire engine.',
|
||||
higher: 'Higher Isp and CF, smaller throat for same flow — but heavier chamber walls and higher required feed pressure',
|
||||
lower: 'Lighter structure and lower feed pressure — at the cost of performance and a larger throat',
|
||||
},
|
||||
|
||||
T0: {
|
||||
name: 'Adiabatic Flame Temperature (T₀)',
|
||||
description: 'Total stagnation temperature of the combustion gases. Set primarily by propellant chemistry and O/F ratio; not a free design variable for real propellants.',
|
||||
higher: 'Higher c* and Isp — but greater thermal loads demanding better cooling and higher-temperature materials',
|
||||
lower: 'Reduced thermal stress and cooling requirements — at the cost of performance',
|
||||
},
|
||||
|
||||
gamma: {
|
||||
name: 'Ratio of Specific Heats (γ)',
|
||||
description: 'Ratio of Cp to Cv for the combustion gas mixture. Governs how efficiently the gas expands through the nozzle.',
|
||||
higher: 'Steeper isentropic expansion for a given ε — slightly higher CF',
|
||||
lower: 'Flatter expansion profile; combustion products typically fall in the range γ ≈ 1.15–1.30',
|
||||
},
|
||||
|
||||
R_gas: {
|
||||
name: 'Specific Gas Constant (R)',
|
||||
description: 'R = R_universal / M_molecular. Reflects the mean molecular weight of the combustion products. Linked to propellant chemistry.',
|
||||
higher: 'Lower molecular weight gas → higher c* and Isp (e.g. hydrogen propellants)',
|
||||
lower: 'Heavier exhaust gas → lower performance (e.g. storable propellants)',
|
||||
},
|
||||
|
||||
mdot: {
|
||||
name: 'Total Mass Flow Rate (ṁ)',
|
||||
description: 'Total propellant flow through the engine. Combined with exhaust velocity it determines thrust. Sets the size of all flow-path components.',
|
||||
higher: 'More thrust and a larger engine — greater propellant consumption',
|
||||
lower: 'Smaller, lighter engine with lower thrust',
|
||||
},
|
||||
|
||||
F_input: {
|
||||
name: 'Thrust (F) — input',
|
||||
description: 'Desired thrust level. Specify this instead of mass flow rate; the solver will derive ṁ from F, p₀, and nozzle conditions.',
|
||||
higher: 'Larger engine, higher propellant consumption, greater structural loads',
|
||||
lower: 'Smaller engine, lower propellant consumption',
|
||||
},
|
||||
|
||||
OF: {
|
||||
name: 'Oxidiser-to-Fuel Mass Ratio (O/F)',
|
||||
description: 'Mass of oxidiser consumed per unit mass of fuel. Affects flame temperature, gas properties, and specific impulse. Peak Isp often occurs slightly fuel-rich.',
|
||||
higher: 'More oxidiser-rich — can reduce peak temperature but may move away from optimum Isp',
|
||||
lower: 'More fuel-rich — often closer to peak Isp; excess fuel reduces combustion temperature',
|
||||
},
|
||||
|
||||
At_input: {
|
||||
name: 'Throat Area (Aₜ) — input',
|
||||
description: 'Cross-sectional area at the nozzle throat. You can specify it manually to override the solver, or leave it blank and let ṁ and p₀ determine it.',
|
||||
higher: 'Higher flow capacity — needed for greater thrust or lower chamber pressure',
|
||||
lower: 'Smaller throat — requires higher chamber pressure to sustain the same flow',
|
||||
},
|
||||
|
||||
eps_input: {
|
||||
name: 'Expansion Ratio (ε = Aₑ/Aₜ) — input',
|
||||
description: 'Ratio of exit area to throat area. Controls how far the exhaust gas expands. The optimum ε matches exit pressure to ambient pressure for maximum efficiency.',
|
||||
higher: 'More expansion, lower exit pressure, higher Isp — but a longer, heavier nozzle',
|
||||
lower: 'Less expansion, shorter nozzle — under-expanded at altitude',
|
||||
},
|
||||
|
||||
pa: {
|
||||
name: 'Ambient Pressure (pₐ)',
|
||||
description: 'Ambient back pressure at the nozzle exit plane. Used to compute the pressure thrust component and the optimum expansion ratio.',
|
||||
higher: 'Greater back pressure reduces pressure thrust — lower effective Isp',
|
||||
lower: 'Vacuum conditions maximise the pressure thrust term',
|
||||
},
|
||||
|
||||
/* ── Combustion Chamber ───────────────────────────────────────── */
|
||||
|
||||
Lstar: {
|
||||
name: 'Characteristic Chamber Length (L*)',
|
||||
description: 'L* = V_chamber / A_throat. A geometric proxy for propellant residence time. Must be long enough for complete combustion — propellant-dependent.',
|
||||
higher: 'Longer residence time, more complete combustion — but a heavier chamber',
|
||||
lower: 'Shorter, lighter chamber — risk of incomplete combustion and acoustic instability',
|
||||
},
|
||||
|
||||
contractionRatio: {
|
||||
name: 'Contraction Ratio (Ac / At)',
|
||||
description: 'Ratio of the chamber cross-section to the throat area. Determines chamber diameter for a given throat size.',
|
||||
higher: 'Larger chamber relative to the throat — better flow uniformity, but heavier',
|
||||
lower: 'More compact chamber — may cause flow maldistribution; typical range is 3–10',
|
||||
},
|
||||
|
||||
convAngleDeg: {
|
||||
name: 'Convergent Half-Angle',
|
||||
description: 'Half-angle of the nozzle convergent section. Affects the length and pressure recovery of the converging part of the chamber.',
|
||||
higher: 'Steeper convergence → shorter chamber — risk of flow separation or higher losses',
|
||||
lower: 'Gentler convergence → smoother flow transition, but longer and heavier',
|
||||
},
|
||||
|
||||
/* ── Nozzle ───────────────────────────────────────────────────── */
|
||||
|
||||
nozzleType: {
|
||||
name: 'Nozzle Contour Type',
|
||||
description: 'Conical nozzles are simple but have divergence losses. Bell (Rao) nozzles are contoured to straighten the flow, achieving ~99% efficiency at 80% of the equivalent conical length.',
|
||||
higher: null,
|
||||
lower: null,
|
||||
},
|
||||
|
||||
divAngleDeg: {
|
||||
name: 'Divergence Half-Angle (Conical)',
|
||||
description: 'Half-angle of the conical nozzle diverging section. A divergence loss factor λ ≈ (1 + cos α) / 2 applies. Typical range: 12–18°.',
|
||||
higher: 'Shorter nozzle — but higher divergence losses reduce effective CF',
|
||||
lower: 'Better axial momentum alignment, higher CF — but longer and heavier nozzle',
|
||||
},
|
||||
|
||||
/* ── Injector ─────────────────────────────────────────────────── */
|
||||
|
||||
injectorType: {
|
||||
name: 'Injector Element Type',
|
||||
description: 'Determines the mixing and atomisation mechanism. Impinging doublets/triplets use jet impingement; coaxial elements rely on shear; pintles use an annular gap.',
|
||||
higher: null,
|
||||
lower: null,
|
||||
},
|
||||
|
||||
injectorN: {
|
||||
name: 'Number of Injector Elements (N)',
|
||||
description: 'Total number of fuel/oxidiser element pairs on the injector face. More elements give finer atomisation and better mixing uniformity.',
|
||||
higher: 'Finer spray, better mixing, more stable combustion — but more complex to manufacture',
|
||||
lower: 'Simpler injector — risk of poor atomisation and combustion instability',
|
||||
},
|
||||
|
||||
dpFraction: {
|
||||
name: 'Pressure Drop Fraction (ΔP / p₀)',
|
||||
description: 'Injector pressure drop as a fraction of chamber pressure. Acts as the primary stability margin — higher ΔP makes the injector less sensitive to combustion pressure oscillations.',
|
||||
higher: 'Better atomisation and stability margin — requires higher tank/feed pressure',
|
||||
lower: 'Reduces feed-system pressure requirement — risk of combustion instability',
|
||||
},
|
||||
|
||||
Cd: {
|
||||
name: 'Discharge Coefficient (Cd)',
|
||||
description: 'Ratio of actual to ideal orifice mass flow. Accounts for vena contracta and flow path losses. Sharp-edged orifices typically have Cd ≈ 0.61; rounded inlets approach 0.85–0.95.',
|
||||
higher: 'More efficient orifice → smaller hole needed for the same mass flow',
|
||||
lower: 'More restricted flow → larger orifice required',
|
||||
},
|
||||
|
||||
rhoFuel_inj: {
|
||||
name: 'Fuel Density (ρ_f) — injector',
|
||||
description: 'Liquid fuel density at injection conditions. Used alongside mass flow and jet velocity to size the fuel orifice area.',
|
||||
higher: 'Denser fuel → smaller orifice diameter for the same mass flow',
|
||||
lower: 'Less dense fuel → larger orifice required',
|
||||
},
|
||||
|
||||
rhoOx_inj: {
|
||||
name: 'Oxidiser Density (ρ_ox) — injector',
|
||||
description: 'Liquid oxidiser density at injection conditions. Used to size the oxidiser orifice area.',
|
||||
higher: 'Denser oxidiser → smaller orifice diameter',
|
||||
lower: 'Less dense oxidiser → larger orifice',
|
||||
},
|
||||
|
||||
/* ── Cooling ──────────────────────────────────────────────────── */
|
||||
|
||||
coolingMethod: {
|
||||
name: 'Cooling Method',
|
||||
description: 'Regenerative cooling circulates propellant through channels to absorb heat. Film cooling injects a thin fuel layer along the wall. Ablative liners sacrifice material. Uncooled engines are for very short burns.',
|
||||
higher: null,
|
||||
lower: null,
|
||||
},
|
||||
|
||||
channelCount: {
|
||||
name: 'Cooling Channel Count',
|
||||
description: 'Number of regenerative cooling channels around the chamber and nozzle wall. More channels distribute heat pickup across more passages.',
|
||||
higher: 'Better heat distribution, smaller per-channel flow area — more complex manufacture',
|
||||
lower: 'Fewer, larger channels — simpler but each must handle more heat load',
|
||||
},
|
||||
|
||||
filmFraction: {
|
||||
name: 'Film Cooling Mass Fraction',
|
||||
description: 'Fraction of total propellant flow injected as a film along the chamber walls to protect them from peak heat flux.',
|
||||
higher: 'Better wall protection — but more unburned propellant reduces Isp',
|
||||
lower: 'Less Isp penalty — risk of wall overheating at high heat flux',
|
||||
},
|
||||
|
||||
/* ── Feed System ──────────────────────────────────────────────── */
|
||||
|
||||
feedType: {
|
||||
name: 'Feed System Type',
|
||||
description: 'Pressure-fed systems use pressurised tanks to push propellant to the chamber. Pump-fed systems use turbopumps for much higher chamber pressures at lower tank mass.',
|
||||
higher: null,
|
||||
lower: null,
|
||||
},
|
||||
|
||||
feedFactor: {
|
||||
name: 'Feed Pressure Factor',
|
||||
description: 'Tank pressure as a multiple of chamber pressure for pressure-fed systems. Must be >1 to overcome injector ΔP and line losses. Typical range: 1.3–1.6.',
|
||||
higher: 'More pressure margin — heavier tank walls and more pressurant required',
|
||||
lower: 'Lighter system — less margin against feed-line or injector flow interruption',
|
||||
},
|
||||
|
||||
rhoFuel_feed: {
|
||||
name: 'Fuel Density (ρ_f) — feed system',
|
||||
description: 'Bulk fuel density used to compute the fuel tank volume from the required fuel mass.',
|
||||
higher: 'Denser fuel → smaller tank volume for the same mass',
|
||||
lower: 'Less dense fuel → larger tank',
|
||||
},
|
||||
|
||||
rhoOx_feed: {
|
||||
name: 'Oxidiser Density (ρ_ox) — feed system',
|
||||
description: 'Bulk oxidiser density used to compute the oxidiser tank volume.',
|
||||
higher: 'Denser oxidiser → smaller tank volume',
|
||||
lower: 'Less dense oxidiser → larger tank',
|
||||
},
|
||||
|
||||
burnTime: {
|
||||
name: 'Burn Time',
|
||||
description: 'Total engine burn duration. Used to compute total propellant mass and volumes, and pressurant requirements.',
|
||||
higher: 'More propellant, larger tanks, potentially more pressurant',
|
||||
lower: 'Less propellant, smaller system — shorter mission or more stages',
|
||||
},
|
||||
|
||||
/* ── Thermodynamic Results ────────────────────────────────────── */
|
||||
|
||||
At_result: {
|
||||
name: 'Throat Area (Aₜ)',
|
||||
description: 'Choked-flow cross-section that sets the engine mass flow capacity. The most thermally critical point in the nozzle.',
|
||||
higher: 'Higher flow capacity for a given chamber pressure',
|
||||
lower: 'Smaller, lighter throat — requires higher p₀ for the same ṁ',
|
||||
},
|
||||
|
||||
Ae_result: {
|
||||
name: 'Exit Area (Aₑ)',
|
||||
description: 'Nozzle exit cross-section. Determined by ε × Aₜ. Sets nozzle bell diameter and integration envelope.',
|
||||
higher: 'Larger exit for higher expansion ratio — heavier and harder to package',
|
||||
lower: 'More compact nozzle exit — under-expanded at altitude',
|
||||
},
|
||||
|
||||
eps_result: {
|
||||
name: 'Expansion Ratio (ε)',
|
||||
description: 'Aₑ / Aₜ. The optimum ε matches exit pressure to ambient for maximum thrust coefficient.',
|
||||
higher: 'Better vacuum Isp — diminishing returns above ε ≈ 100',
|
||||
lower: 'Shorter, lighter nozzle — better suited to sea-level operation',
|
||||
},
|
||||
|
||||
Me_result: {
|
||||
name: 'Exit Mach Number (Mₑ)',
|
||||
description: 'Exhaust speed at the nozzle exit relative to the local sound speed. Indicates how fully the gas has expanded.',
|
||||
higher: 'More complete expansion → higher Isp',
|
||||
lower: 'Under-expanded — flow still has kinetic energy to give',
|
||||
},
|
||||
|
||||
Te_result: {
|
||||
name: 'Exit Temperature (Tₑ)',
|
||||
description: 'Static temperature of the exhaust at the nozzle exit plane. Indicates how much thermal energy has been converted to kinetic energy.',
|
||||
higher: 'More thermal energy remaining → less efficient conversion to velocity',
|
||||
lower: 'More energy converted to exhaust velocity → higher Isp',
|
||||
},
|
||||
|
||||
pe_result: {
|
||||
name: 'Exit Pressure (pₑ)',
|
||||
description: 'Static pressure at the nozzle exit. Ideally matches ambient pressure for maximum CF. Mismatch causes under- or over-expansion losses.',
|
||||
higher: 'Under-expanded — nozzle too short; pressure thrust wasted',
|
||||
lower: 'Over-expanded — risk of flow separation at sea level',
|
||||
},
|
||||
|
||||
Ve_result: {
|
||||
name: 'Exhaust Velocity (Vₑ)',
|
||||
description: 'Actual gas velocity at the nozzle exit. Primary contributor to specific impulse (Isp ≈ Vₑ / g₀ for perfectly expanded nozzle).',
|
||||
higher: 'Higher Isp and better propulsive efficiency',
|
||||
lower: 'Lower performance — more propellant needed for the same Δv',
|
||||
},
|
||||
|
||||
F_result: {
|
||||
name: 'Thrust (F)',
|
||||
description: 'Total thrust = momentum thrust + pressure thrust. F = ṁ·Vₑ + (pₑ − pₐ)·Aₑ.',
|
||||
higher: 'Faster vehicle acceleration or higher payload capability',
|
||||
lower: 'Lower structural loads, easier integration',
|
||||
},
|
||||
|
||||
Isp_result: {
|
||||
name: 'Specific Impulse (Isp)',
|
||||
description: 'Thrust per unit weight flow: Isp = F / (ṁ · g₀). The fundamental measure of propellant efficiency.',
|
||||
higher: 'More Δv per kg of propellant — less propellant mass fraction needed',
|
||||
lower: 'Less efficient — more propellant mass required for the same mission',
|
||||
},
|
||||
|
||||
cstar_result: {
|
||||
name: 'Characteristic Velocity (c*)',
|
||||
description: 'c* = p₀ · Aₜ / ṁ. Measures combustion chamber performance independently of nozzle shape.',
|
||||
higher: 'Better combustion efficiency or higher-energy propellants',
|
||||
lower: 'Combustion inefficiency, off-design mixture ratio, or low-energy propellants',
|
||||
},
|
||||
|
||||
CF_result: {
|
||||
name: 'Thrust Coefficient (CF)',
|
||||
description: 'Dimensionless nozzle efficiency factor: F = CF · p₀ · Aₜ. Combines expansion efficiency and pressure matching.',
|
||||
higher: 'Better nozzle performance — closer to the optimum expansion condition',
|
||||
lower: 'Under- or over-expansion losses, or flow separation in over-expanded nozzle',
|
||||
},
|
||||
|
||||
mdot_f_result: {
|
||||
name: 'Fuel Mass Flow Rate (ṁ_f)',
|
||||
description: 'Fuel propellant consumption rate, derived from total ṁ and O/F: ṁ_f = ṁ / (1 + O/F).',
|
||||
higher: 'More fuel consumption — larger fuel tank and feed system required',
|
||||
lower: 'Less fuel flow — smaller fuel system',
|
||||
},
|
||||
|
||||
mdot_ox_result: {
|
||||
name: 'Oxidiser Mass Flow Rate (ṁ_ox)',
|
||||
description: 'Oxidiser consumption rate: ṁ_ox = ṁ · O/F / (1 + O/F).',
|
||||
higher: 'More oxidiser consumption — larger oxidiser tank required',
|
||||
lower: 'Less oxidiser flow — smaller oxidiser system',
|
||||
},
|
||||
|
||||
/* ── Chamber Geometry Results ─────────────────────────────────── */
|
||||
|
||||
Dc_result: {
|
||||
name: 'Chamber Diameter (Dc)',
|
||||
description: 'Inner diameter of the cylindrical combustion chamber. Determined by Dt × √(contraction ratio).',
|
||||
higher: 'Larger chamber volume per unit length — can achieve the required L* in a shorter chamber',
|
||||
lower: 'More compact chamber — requires greater axial length to reach the target L*',
|
||||
},
|
||||
|
||||
Dt_result: {
|
||||
name: 'Throat Diameter (Dt)',
|
||||
description: 'Diameter of the nozzle throat. The most thermally and mechanically stressed location in the engine.',
|
||||
higher: 'Higher mass flow capacity for the given chamber pressure',
|
||||
lower: 'More concentrated heat flux at the throat — harder to cool',
|
||||
},
|
||||
|
||||
contractionRatio_result: {
|
||||
name: 'Contraction Ratio (Ac / At)',
|
||||
description: 'Ratio of chamber cross-section to throat area. Determines how much the flow contracts before the throat.',
|
||||
higher: 'Wider chamber relative to the throat — better flow uniformity',
|
||||
lower: 'Tighter contraction — risk of flow distortion entering the throat',
|
||||
},
|
||||
|
||||
Lc_result: {
|
||||
name: 'Total Chamber Length (Lc)',
|
||||
description: 'Combined axial length of the cylindrical and convergent chamber sections.',
|
||||
higher: 'Longer chamber — more complete combustion, but heavier',
|
||||
lower: 'Shorter, lighter chamber — may need a high-energy propellant to reach required L*',
|
||||
},
|
||||
|
||||
L_cyl_result: {
|
||||
name: 'Cylindrical Section Length',
|
||||
description: 'Length of the straight cylindrical portion where most combustion occurs.',
|
||||
higher: 'More mixing and dwell time in the cylindrical zone',
|
||||
lower: 'Shorter cylinder — the convergent section supplies additional volume',
|
||||
},
|
||||
|
||||
L_conv_result: {
|
||||
name: 'Convergent Section Length',
|
||||
description: 'Axial length of the nozzle convergent section from the cylindrical chamber to the throat.',
|
||||
higher: 'Gentler convergence angle or wider chamber upstream',
|
||||
lower: 'Steeper convergence — risk of flow separation or higher losses',
|
||||
},
|
||||
|
||||
Vc_result: {
|
||||
name: 'Chamber Volume (Vc)',
|
||||
description: 'Total internal volume of the combustion chamber. Combined with throat area gives L* = Vc / At.',
|
||||
higher: 'Greater residence time → more complete combustion',
|
||||
lower: 'Shorter, lighter chamber — needs high-energy propellant for complete combustion',
|
||||
},
|
||||
|
||||
/* ── Nozzle Geometry Results ──────────────────────────────────── */
|
||||
|
||||
De_result: {
|
||||
name: 'Exit Diameter (De)',
|
||||
description: 'Outer diameter of the nozzle exit plane. Sets the integration envelope for the nozzle.',
|
||||
higher: 'Larger exit for higher expansion — heavier and harder to package',
|
||||
lower: 'More compact nozzle — under-expanded at altitude, over-expanded at sea level',
|
||||
},
|
||||
|
||||
Ln_result: {
|
||||
name: 'Nozzle Length (Ln)',
|
||||
description: 'Axial length of the diverging nozzle section from throat to exit plane.',
|
||||
higher: 'More axial length needed for high expansion ratios — adds mass',
|
||||
lower: 'Shorter nozzle — less expansion, but lighter and easier to integrate',
|
||||
},
|
||||
|
||||
/* ── Injector Results ─────────────────────────────────────────── */
|
||||
|
||||
deltaP_result: {
|
||||
name: 'Injector Pressure Drop (ΔP)',
|
||||
description: 'Pressure difference across the injector face. A key stability parameter — typically 15–30% of chamber pressure.',
|
||||
higher: 'Better atomisation and stability margin — requires higher feed pressure',
|
||||
lower: 'Reduced feed pressure requirement — risk of combustion instability',
|
||||
},
|
||||
|
||||
v_f_result: {
|
||||
name: 'Fuel Jet Velocity',
|
||||
description: 'Velocity of the fuel stream as it exits the orifice. Higher velocity improves atomisation at impingement.',
|
||||
higher: 'Better atomisation and mixing — finer spray droplets',
|
||||
lower: 'Coarser spray — may need more injector elements to compensate',
|
||||
},
|
||||
|
||||
v_ox_result: {
|
||||
name: 'Oxidiser Jet Velocity',
|
||||
description: 'Velocity of the oxidiser stream exiting the orifice.',
|
||||
higher: 'Better atomisation — but higher pressure drop across the orifice',
|
||||
lower: 'Coarser spray — may impair mixing quality',
|
||||
},
|
||||
|
||||
d_f_result: {
|
||||
name: 'Fuel Orifice Diameter',
|
||||
description: 'Diameter of each individual fuel injector orifice. Sized from ṁ_f, Cd, ρ_f, and jet velocity.',
|
||||
higher: 'Larger, easier-to-manufacture orifice — coarser atomisation',
|
||||
lower: 'Finer spray — but more susceptible to clogging',
|
||||
},
|
||||
|
||||
d_ox_result: {
|
||||
name: 'Oxidiser Orifice Diameter',
|
||||
description: 'Diameter of each oxidiser orifice. Sized from ṁ_ox, Cd, ρ_ox, and jet velocity.',
|
||||
higher: 'Larger orifice — coarser oxidiser spray',
|
||||
lower: 'Finer spray — tighter manufacturing tolerances required',
|
||||
},
|
||||
|
||||
/* ── Cooling Results ──────────────────────────────────────────── */
|
||||
|
||||
q_est_result: {
|
||||
name: 'Estimated Heat Flux (q″)',
|
||||
description: 'Estimated peak heat flux at the throat region based on a simplified Bartz-style correlation.',
|
||||
higher: 'More aggressive thermal environment — demands greater coolant flow or better channel geometry',
|
||||
lower: 'Easier cooling problem — more design margin',
|
||||
},
|
||||
|
||||
q_total_result: {
|
||||
name: 'Total Heat Load',
|
||||
description: 'Total heat power absorbed by the coolant, integrated over the chamber and nozzle surface area.',
|
||||
higher: 'More coolant flow or a larger temperature rise in the coolant required',
|
||||
lower: 'Easier to manage with available coolant flow',
|
||||
},
|
||||
|
||||
channelCount_result: {
|
||||
name: 'Cooling Channel Count',
|
||||
description: 'Number of regenerative cooling channels (reflects the input setting).',
|
||||
higher: 'Distributed heat pickup — less flow per channel',
|
||||
lower: 'Fewer channels — each must carry more coolant',
|
||||
},
|
||||
|
||||
channelArea_result: {
|
||||
name: 'Cooling Channel Area (per channel)',
|
||||
description: 'Cross-sectional flow area of each cooling channel. Determines coolant velocity and pressure drop.',
|
||||
higher: 'Lower coolant velocity — lower pressure drop but less turbulent heat transfer',
|
||||
lower: 'Higher velocity — better heat transfer coefficient but larger pressure drop',
|
||||
},
|
||||
|
||||
mdot_film_result: {
|
||||
name: 'Film Coolant Mass Flow',
|
||||
description: 'Mass flow rate of propellant injected as a film layer along the chamber walls.',
|
||||
higher: 'Better wall protection — more effective at high heat flux',
|
||||
lower: 'Less Isp penalty from film dilution',
|
||||
},
|
||||
|
||||
ispPenalty_result: {
|
||||
name: 'Isp Penalty (Film Cooling)',
|
||||
description: 'Estimated percentage reduction in specific impulse due to film coolant that does not participate fully in combustion.',
|
||||
higher: 'Significant Isp loss — consider reducing film fraction if thermally feasible',
|
||||
lower: 'Minimal performance impact — film cooling is efficient for this design',
|
||||
},
|
||||
|
||||
/* ── Feed System Results ──────────────────────────────────────── */
|
||||
|
||||
p_tank_result: {
|
||||
name: 'Tank Pressure',
|
||||
description: 'Required propellant tank pressure to sustain the desired chamber pressure through the injector and feed lines.',
|
||||
higher: 'Heavier tank walls and more pressurant — may require a composite tank',
|
||||
lower: 'Lighter structure — check that there is sufficient margin over chamber pressure',
|
||||
},
|
||||
|
||||
V_fuel_result: {
|
||||
name: 'Fuel Volume',
|
||||
description: 'Required fuel tank volume for the specified burn time and fuel density.',
|
||||
higher: 'Larger, heavier fuel tank — may need to be distributed or jettisoned',
|
||||
lower: 'Compact fuel system — allows a smaller vehicle',
|
||||
},
|
||||
|
||||
V_ox_result: {
|
||||
name: 'Oxidiser Volume',
|
||||
description: 'Required oxidiser tank volume for the specified burn time and oxidiser density.',
|
||||
higher: 'Larger oxidiser tank — often the dominant volume for high-O/F propellants',
|
||||
lower: 'Compact oxidiser system',
|
||||
},
|
||||
|
||||
V_prop_result: {
|
||||
name: 'Total Propellant Volume',
|
||||
description: 'Combined fuel + oxidiser volume. Determines the overall propellant tankage size.',
|
||||
higher: 'More propellant for longer burns or higher thrust',
|
||||
lower: 'Smaller, lighter vehicle',
|
||||
},
|
||||
|
||||
m_press_result: {
|
||||
name: 'Pressurant Mass',
|
||||
description: 'Mass of pressurising gas (typically helium or nitrogen) needed to maintain tank pressure throughout the burn. Estimated assuming isothermal blowdown.',
|
||||
higher: 'Heavier pressurant load — consider a regulated or blowdown feed system',
|
||||
lower: 'Lightweight pressurant — efficient feed system design',
|
||||
},
|
||||
|
||||
dP_pump_result: {
|
||||
name: 'Pump ΔP',
|
||||
description: 'Required pump pressure rise across the propellant pump(s) in a pump-fed system.',
|
||||
higher: 'More pump work — larger, heavier turbopump needed',
|
||||
lower: 'Lighter pump — may indicate lower chamber pressure or very efficient feed lines',
|
||||
},
|
||||
|
||||
P_turbine_result: {
|
||||
name: 'Est. Turbine Power',
|
||||
description: 'Estimated turbopump turbine shaft power required to drive the propellant pumps.',
|
||||
higher: 'Larger turbine and more turbine propellant bleed — heavier turbopump assembly',
|
||||
lower: 'Smaller turbine — more efficient or lower chamber pressure design',
|
||||
},
|
||||
}
|
||||
519
src/engine/equations.js
Normal file
519
src/engine/equations.js
Normal 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'] },
|
||||
]
|
||||
106
src/engine/exportImport.js
Normal file
106
src/engine/exportImport.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { VARIABLES } from './variables.js'
|
||||
import { formatValue } from './format.js'
|
||||
|
||||
/**
|
||||
* Assemble a plain object describing the current workspace state,
|
||||
* used for both ODT generation and JSON export.
|
||||
*/
|
||||
export function buildExportData(workspaceVarIds, userValues, solved, unitSelections, getUnit, sciNotation) {
|
||||
const given = Object.entries(userValues).map(([varId, siVal]) => {
|
||||
const v = VARIABLES[varId]
|
||||
const unit = getUnit(varId)
|
||||
return {
|
||||
symbol: v?.symbol ?? varId,
|
||||
name: v?.name ?? varId,
|
||||
value: formatValue(unit.fromSI(siVal), sciNotation),
|
||||
unit: unit.label,
|
||||
}
|
||||
})
|
||||
|
||||
const solvedEntries = Object.entries(solved).map(([varId, info]) => {
|
||||
const v = VARIABLES[varId]
|
||||
const unit = getUnit(varId)
|
||||
return {
|
||||
symbol: v?.symbol ?? varId,
|
||||
name: v?.name ?? varId,
|
||||
value: formatValue(unit.fromSI(info.value), sciNotation),
|
||||
unit: unit.label,
|
||||
via: info.equationName ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
const unsolved = workspaceVarIds
|
||||
.filter(id => !(id in userValues) && !(id in solved))
|
||||
.map(id => {
|
||||
const v = VARIABLES[id]
|
||||
return { symbol: v?.symbol ?? id, name: v?.name ?? id }
|
||||
})
|
||||
|
||||
return { given, solved: solvedEntries, unsolved }
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a JSON Blob for download.
|
||||
* Schema version 1: workspace section is re-imported; results section is reference-only.
|
||||
*/
|
||||
export function exportJSON(exportData, workspaceVarIds, userValues, unitSelections) {
|
||||
const payload = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
workspace: {
|
||||
variableIds: workspaceVarIds,
|
||||
userValues,
|
||||
unitSelections,
|
||||
},
|
||||
results: {
|
||||
given: exportData.given,
|
||||
solved: exportData.solved,
|
||||
unsolved: exportData.unsolved,
|
||||
},
|
||||
}
|
||||
return new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an imported JSON string and return only the workspace section.
|
||||
* Throws a descriptive Error if the file is invalid.
|
||||
*/
|
||||
export function parseImport(jsonString) {
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(jsonString)
|
||||
} catch {
|
||||
throw new Error('File is not valid JSON.')
|
||||
}
|
||||
|
||||
if (data.version !== 1) {
|
||||
throw new Error(`Unsupported export version: ${data.version}`)
|
||||
}
|
||||
|
||||
const { variableIds, userValues, unitSelections } = data.workspace ?? {}
|
||||
|
||||
if (!Array.isArray(variableIds)) {
|
||||
throw new Error('Import file is missing workspace.variableIds.')
|
||||
}
|
||||
if (typeof userValues !== 'object' || userValues === null) {
|
||||
throw new Error('Import file is missing workspace.userValues.')
|
||||
}
|
||||
|
||||
return {
|
||||
variableIds,
|
||||
userValues,
|
||||
unitSelections: unitSelections ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a browser file download.
|
||||
*/
|
||||
export function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
168
src/engine/exportOdt.js
Normal file
168
src/engine/exportOdt.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import JSZip from 'jszip'
|
||||
|
||||
/* ── ODT XML helpers ────────────────────────────────────────────────── */
|
||||
|
||||
function xmlEscape(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function cell(text, styleName = 'TableCell') {
|
||||
return `<table:table-cell table:style-name="${styleName}" office:value-type="string">` +
|
||||
`<text:p text:style-name="TableContents">${xmlEscape(text)}</text:p>` +
|
||||
`</table:table-cell>`
|
||||
}
|
||||
|
||||
function headerCell(text) {
|
||||
return `<table:table-cell table:style-name="TableHeaderCell" office:value-type="string">` +
|
||||
`<text:p text:style-name="TableHeader">${xmlEscape(text)}</text:p>` +
|
||||
`</table:table-cell>`
|
||||
}
|
||||
|
||||
function row(...cells) {
|
||||
return `<table:table-row>${cells.join('')}</table:table-row>`
|
||||
}
|
||||
|
||||
function table(name, columnCount, rows) {
|
||||
const cols = Array(columnCount).fill(
|
||||
`<table:table-column table:style-name="TableColumn"/>`
|
||||
).join('')
|
||||
return `<table:table table:name="${name}" table:style-name="Table">${cols}${rows}</table:table>`
|
||||
}
|
||||
|
||||
function heading(text, level = 1) {
|
||||
return `<text:h text:style-name="Heading${level}" text:outline-level="${level}">${xmlEscape(text)}</text:h>`
|
||||
}
|
||||
|
||||
function para(text = '') {
|
||||
return `<text:p text:style-name="Standard">${xmlEscape(text)}</text:p>`
|
||||
}
|
||||
|
||||
/* ── Styles XML ─────────────────────────────────────────────────────── */
|
||||
|
||||
const STYLES_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-styles
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||||
office:version="1.3">
|
||||
<office:styles>
|
||||
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
|
||||
<style:style style:name="Heading1" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-size="16pt" fo:font-weight="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="Heading2" style:family="paragraph" style:class="text" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-size="13pt" fo:font-weight="bold"/>
|
||||
<style:paragraph-properties fo:margin-top="6pt" fo:margin-bottom="3pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableContents" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:padding="1mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableHeader" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:text-properties fo:font-weight="bold"/>
|
||||
<style:paragraph-properties fo:padding="1mm"/>
|
||||
</style:style>
|
||||
</office:styles>
|
||||
</office:document-styles>`
|
||||
|
||||
/* ── Automatic styles for content.xml ───────────────────────────────── */
|
||||
|
||||
const AUTO_STYLES = `
|
||||
<style:style style:name="Table" style:family="table">
|
||||
<style:table-properties style:width="16cm" fo:margin-bottom="6mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableColumn" style:family="table-column">
|
||||
<style:table-column-properties style:column-width="4cm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableCell" style:family="table-cell">
|
||||
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm"/>
|
||||
</style:style>
|
||||
<style:style style:name="TableHeaderCell" style:family="table-cell">
|
||||
<style:table-cell-properties fo:border="0.05pt solid #888888" fo:padding="1.5mm" fo:background-color="#e8e8e8"/>
|
||||
</style:style>`
|
||||
|
||||
/* ── content.xml builder ────────────────────────────────────────────── */
|
||||
|
||||
function buildContentXml(exportData, exportedAt) {
|
||||
const { given, solved, unsolved } = exportData
|
||||
|
||||
const givenTable = table('GivenValues', 4,
|
||||
row(headerCell('Symbol'), headerCell('Name'), headerCell('Value'), headerCell('Unit')) +
|
||||
given.map(r => row(cell(r.symbol), cell(r.name), cell(r.value), cell(r.unit))).join('')
|
||||
)
|
||||
|
||||
const solvedTable = table('SolvedValues', 5,
|
||||
row(headerCell('Symbol'), headerCell('Name'), headerCell('Value'), headerCell('Unit'), headerCell('Solved via')) +
|
||||
solved.map(r => row(cell(r.symbol), cell(r.name), cell(r.value), cell(r.unit), cell(r.via))).join('')
|
||||
)
|
||||
|
||||
const unsolvedSection = unsolved.length === 0 ? '' :
|
||||
heading('Unsolved Variables', 2) +
|
||||
table('UnsolvedVars', 2,
|
||||
row(headerCell('Symbol'), headerCell('Name')) +
|
||||
unsolved.map(r => row(cell(r.symbol), cell(r.name))).join('')
|
||||
)
|
||||
|
||||
const body = [
|
||||
heading('Rocketry Workspace Export'),
|
||||
para(`Exported: ${exportedAt}`),
|
||||
para(),
|
||||
heading('Given Values', 2),
|
||||
given.length > 0 ? givenTable : para('(none)'),
|
||||
para(),
|
||||
heading('Solved Values', 2),
|
||||
solved.length > 0 ? solvedTable : para('(none)'),
|
||||
para(),
|
||||
unsolvedSection,
|
||||
].join('\n')
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document-content
|
||||
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||||
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
|
||||
office:version="1.3">
|
||||
<office:automatic-styles>${AUTO_STYLES}</office:automatic-styles>
|
||||
<office:body>
|
||||
<office:text>
|
||||
${body}
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document-content>`
|
||||
}
|
||||
|
||||
const MANIFEST_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest:manifest
|
||||
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
||||
manifest:version="1.3">
|
||||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
|
||||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||||
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
||||
</manifest:manifest>`
|
||||
|
||||
/* ── Public API ─────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Build an ODT Blob from the assembled export data.
|
||||
* @param {object} exportData - output of buildExportData()
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
export async function generateOdt(exportData) {
|
||||
const exportedAt = new Date().toISOString()
|
||||
const zip = new JSZip()
|
||||
|
||||
// mimetype MUST be the first file and stored uncompressed
|
||||
zip.file('mimetype', 'application/vnd.oasis.opendocument.text', { compression: 'STORE' })
|
||||
zip.file('META-INF/manifest.xml', MANIFEST_XML)
|
||||
zip.file('styles.xml', STYLES_XML)
|
||||
zip.file('content.xml', buildContentXml(exportData, exportedAt))
|
||||
|
||||
return zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.oasis.opendocument.text' })
|
||||
}
|
||||
10
src/engine/format.js
Normal file
10
src/engine/format.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export function formatValue(v, sciNotation = false) {
|
||||
if (v === undefined || v === null) return '—'
|
||||
if (!isFinite(v)) return '∞'
|
||||
if (v === 0) return '0'
|
||||
if (sciNotation) return v.toExponential(4)
|
||||
if (Math.abs(v) >= 1e6 || (Math.abs(v) < 0.001 && v !== 0)) {
|
||||
return v.toExponential(4)
|
||||
}
|
||||
return parseFloat(v.toPrecision(6)).toString()
|
||||
}
|
||||
754
src/engine/knowledgebaseData.js
Normal file
754
src/engine/knowledgebaseData.js
Normal file
@@ -0,0 +1,754 @@
|
||||
// Knowledgebase: individual substances (fuels, oxidisers, monopropellants)
|
||||
// and bipropellant combination performance data.
|
||||
//
|
||||
// COMBINATIONS also serve as the single source of truth for the solver's
|
||||
// PropellantModal — PROPELLANTS and PROPELLANT_TYPES are derived below.
|
||||
|
||||
export const SUBSTANCES = [
|
||||
// ── Fuels ───────────────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'lh2',
|
||||
name: 'Liquid Hydrogen',
|
||||
symbol: 'LH₂',
|
||||
formula: 'H₂',
|
||||
role: 'fuel',
|
||||
subcategory: 'Cryogenic',
|
||||
description:
|
||||
'Liquid hydrogen is the highest-performance liquid rocket fuel, prized for its exceptionally low molecular weight combustion products. It must be stored at −253 °C (just 20 K above absolute zero), imposing strict insulation and boil-off management requirements. Despite its low density, its combination with LOX delivers the highest specific impulse of any practical bipropellant.',
|
||||
density: 71,
|
||||
densityNote: 'at −253 °C, 1 atm',
|
||||
boilingPoint: -253,
|
||||
meltingPoint: -259,
|
||||
storagePressure: '1 atm (cryogenic dewar)',
|
||||
molecularWeight: 2.016,
|
||||
autoignitionTemp: 500,
|
||||
flammabilityRange: '4–75 vol% in air',
|
||||
hazards: ['Extreme cryogen (−253 °C)', 'Highly flammable', 'Wide flammability range', 'Asphyxiant in confined spaces'],
|
||||
toxicity: 'Non-toxic',
|
||||
engineExamples: ['RL-10', 'J-2', 'RS-68', 'Vulcain 2', 'LE-7A', 'HM7B'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['lox'],
|
||||
},
|
||||
{
|
||||
id: 'rp1',
|
||||
name: 'RP-1 (Kerosene)',
|
||||
symbol: 'RP-1',
|
||||
formula: 'C₁₂H₂₄ (avg)',
|
||||
role: 'fuel',
|
||||
subcategory: 'Storable',
|
||||
description:
|
||||
'Rocket Propellant-1 is a highly refined kerosene used as a rocket fuel. It is a complex mixture of hydrocarbons with good energy density, low toxicity relative to hydrazine fuels, and ambient storage conditions. Its high density makes it attractive for volume-constrained vehicles. It is one of the most widely used liquid rocket fuels in history.',
|
||||
density: 820,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 175,
|
||||
meltingPoint: -40,
|
||||
storagePressure: 'Ambient',
|
||||
molecularWeight: 170,
|
||||
autoignitionTemp: 220,
|
||||
flammabilityRange: '0.6–4.7 vol% in air',
|
||||
hazards: ['Flammable liquid', 'Aspiration hazard', 'Skin irritant'],
|
||||
toxicity: 'Low acute toxicity; mildly irritating',
|
||||
engineExamples: ['Merlin', 'RD-180', 'F-1', 'NK-33', 'Rutherford', 'Gamma'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['lox', 'htp'],
|
||||
},
|
||||
{
|
||||
id: 'lch4',
|
||||
name: 'Liquid Methane',
|
||||
symbol: 'LCH₄',
|
||||
formula: 'CH₄',
|
||||
role: 'fuel',
|
||||
subcategory: 'Cryogenic',
|
||||
description:
|
||||
'Liquid methane is a cryogenic fuel that has gained prominence for reusable rocket applications. It offers a better Isp than RP-1, cleaner combustion (less coking), and is compatible with deep-throttling engines. It can in principle be produced on Mars via the Sabatier reaction, making it attractive for interplanetary missions.',
|
||||
density: 424,
|
||||
densityNote: 'at −162 °C, 1 atm',
|
||||
boilingPoint: -162,
|
||||
meltingPoint: -182,
|
||||
storagePressure: '1 atm (cryogenic) or ~3 bar (semi-cryo)',
|
||||
molecularWeight: 16.04,
|
||||
autoignitionTemp: 537,
|
||||
flammabilityRange: '5–15 vol% in air',
|
||||
hazards: ['Cryogen (−162 °C)', 'Highly flammable', 'Asphyxiant'],
|
||||
toxicity: 'Non-toxic; simple asphyxiant',
|
||||
engineExamples: ['Raptor', 'BE-4', 'Prometheus', 'Zephyr'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['lox'],
|
||||
},
|
||||
{
|
||||
id: 'lc2h6',
|
||||
name: 'Liquid Ethane',
|
||||
symbol: 'LC₂H₆',
|
||||
formula: 'C₂H₆',
|
||||
role: 'fuel',
|
||||
subcategory: 'Cryogenic',
|
||||
description:
|
||||
'Liquid ethane is a cryogenic hydrocarbon fuel investigated as an alternative to methane. Its slightly higher density and carbon content give marginally better volumetric performance. It has been researched for reusable applications and has similar handling requirements to LCH₄.',
|
||||
density: 544,
|
||||
densityNote: 'at −89 °C, 1 atm',
|
||||
boilingPoint: -89,
|
||||
meltingPoint: -183,
|
||||
storagePressure: '1 atm (cryogenic)',
|
||||
molecularWeight: 30.07,
|
||||
autoignitionTemp: 472,
|
||||
flammabilityRange: '3–12.5 vol% in air',
|
||||
hazards: ['Cryogen (−89 °C)', 'Flammable', 'Asphyxiant'],
|
||||
toxicity: 'Non-toxic; simple asphyxiant',
|
||||
engineExamples: ['Research engines (Korolev cross-feed studies)'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['lox'],
|
||||
},
|
||||
{
|
||||
id: 'ethanol',
|
||||
name: 'Ethanol',
|
||||
symbol: 'EtOH',
|
||||
formula: 'C₂H₅OH',
|
||||
role: 'fuel',
|
||||
subcategory: 'Storable',
|
||||
description:
|
||||
'Ethanol (typically used as 75% aqueous solution) was one of the earliest liquid rocket fuels, used in the German V-2. Today it is popular in amateur and experimental rocketry, especially with nitrous oxide as the oxidiser. The water content helps cool the combustion chamber and reduces flame temperature.',
|
||||
density: 789,
|
||||
densityNote: 'at 20 °C (anhydrous); 75% solution: ~870 kg/m³',
|
||||
boilingPoint: 78,
|
||||
meltingPoint: -114,
|
||||
storagePressure: 'Ambient',
|
||||
molecularWeight: 46.07,
|
||||
autoignitionTemp: 365,
|
||||
flammabilityRange: '3.3–19 vol% in air',
|
||||
hazards: ['Flammable liquid', 'Vapour heavier than air'],
|
||||
toxicity: 'Low acute toxicity; CNS depressant at high concentrations',
|
||||
engineExamples: ['A-4 (V-2)', 'Amateur N₂O motors'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['n2o'],
|
||||
},
|
||||
{
|
||||
id: 'udmh',
|
||||
name: 'UDMH',
|
||||
symbol: 'UDMH',
|
||||
formula: '(CH₃)₂N₂H₂',
|
||||
role: 'fuel',
|
||||
subcategory: 'Hypergolic',
|
||||
description:
|
||||
'Unsymmetrical dimethylhydrazine is a storable, hypergolic fuel that ignites on contact with nitrogen tetroxide. Its storability (liquid at ambient temperatures) and instant ignition make it ideal for spacecraft and missile applications requiring reliable restart. It is highly toxic and carcinogenic.',
|
||||
density: 791,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 63,
|
||||
meltingPoint: -57,
|
||||
storagePressure: 'Ambient (sealed)',
|
||||
molecularWeight: 60.10,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: '2–95 vol% in air',
|
||||
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable', 'Corrosive vapour'],
|
||||
toxicity: 'Highly toxic; probable human carcinogen (IARC Group 2A)',
|
||||
engineExamples: ['RD-253 (Proton)', 'YF-20 (Long March)', 'Viking (Ariane 1–4)'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['n2o4'],
|
||||
},
|
||||
{
|
||||
id: 'mmh',
|
||||
name: 'MMH',
|
||||
symbol: 'MMH',
|
||||
formula: 'CH₃N₂H₃',
|
||||
role: 'fuel',
|
||||
subcategory: 'Hypergolic',
|
||||
description:
|
||||
'Monomethylhydrazine is the premier storable spacecraft propellant when used with nitrogen tetroxide. It has been the workhorse of spacecraft propulsion systems for decades, prized for its wide liquid range, reliable hypergolic ignition, and good performance. Like all hydrazines, it is highly toxic.',
|
||||
density: 874,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 87,
|
||||
meltingPoint: -52,
|
||||
storagePressure: 'Ambient (sealed)',
|
||||
molecularWeight: 46.07,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: '2.5–98 vol% in air',
|
||||
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable'],
|
||||
toxicity: 'Highly toxic; probable human carcinogen',
|
||||
engineExamples: ['Shuttle OMS/RCS', 'Orion SM', 'AJ10', 'R-4D', 'Aestus'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['n2o4'],
|
||||
},
|
||||
{
|
||||
id: 'aerozine50',
|
||||
name: 'Aerozine-50',
|
||||
symbol: 'A-50',
|
||||
formula: '50% UDMH + 50% N₂H₄',
|
||||
role: 'fuel',
|
||||
subcategory: 'Hypergolic',
|
||||
description:
|
||||
'Aerozine-50 is a 50/50 blend of UDMH and hydrazine developed for the Titan II missile. Compared to pure UDMH it has higher energy content; compared to pure hydrazine it has better thermal stability and lower freezing point. It ignites hypergolically with N₂O₄ and was used in the Apollo Lunar Module descent engine.',
|
||||
density: 900,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 70,
|
||||
meltingPoint: -57,
|
||||
storagePressure: 'Ambient (sealed)',
|
||||
molecularWeight: 53,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: '2–99 vol% in air',
|
||||
hazards: ['Highly toxic', 'Carcinogenic', 'Hypergolic with N₂O₄', 'Flammable'],
|
||||
toxicity: 'Highly toxic; carcinogenic',
|
||||
engineExamples: ['Titan II/III/IV', 'Lunar Module DPS', 'Delta II (second stage)', 'TR-201'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['n2o4'],
|
||||
},
|
||||
{
|
||||
id: 'htpb',
|
||||
name: 'HTPB',
|
||||
symbol: 'HTPB',
|
||||
formula: '(C₄H₆)ₙ (polymer)',
|
||||
role: 'fuel',
|
||||
subcategory: 'Hybrid',
|
||||
description:
|
||||
'Hydroxyl-terminated polybutadiene is a solid rubber binder used as both structural matrix and fuel in hybrid and solid rocket motors. In hybrid engines the solid grain regresses as oxidiser flows over it. HTPB has good mechanical properties, low toxicity during handling (before combustion), and is highly versatile.',
|
||||
density: 920,
|
||||
densityNote: 'cured solid at 20 °C',
|
||||
boilingPoint: null,
|
||||
meltingPoint: null,
|
||||
storagePressure: 'N/A (solid)',
|
||||
molecularWeight: null,
|
||||
autoignitionTemp: 320,
|
||||
flammabilityRange: 'N/A (solid)',
|
||||
hazards: ['Combustible solid', 'Combustion products may be toxic'],
|
||||
toxicity: 'Low toxicity as-cast; combustion gases are hazardous',
|
||||
engineExamples: ['SpaceShipOne/Two (N₂O hybrid)', 'SRM binders (Space Shuttle SRB)', 'NAMMO hybrid'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['n2o', 'lox', 'ap'],
|
||||
},
|
||||
{
|
||||
id: 'sucrose',
|
||||
name: 'Sucrose',
|
||||
symbol: 'Sucrose',
|
||||
formula: 'C₁₂H₂₂O₁₁',
|
||||
role: 'fuel',
|
||||
subcategory: 'Solid',
|
||||
description:
|
||||
'Sucrose (table sugar) combined with potassium nitrate forms KNSU, a popular amateur "candy" propellant. The mixture is cast or pressed and burns to produce a vigorous thrust with modest specific impulse. It is widely used in educational and amateur rocketry due to its low cost and ease of manufacture.',
|
||||
density: 1590,
|
||||
densityNote: 'at 20 °C (crystal)',
|
||||
boilingPoint: null,
|
||||
meltingPoint: 186,
|
||||
storagePressure: 'N/A (solid)',
|
||||
molecularWeight: 342.30,
|
||||
autoignitionTemp: 370,
|
||||
flammabilityRange: 'N/A (combustible solid)',
|
||||
hazards: ['Combustible solid when mixed with oxidiser', 'Dust explosion risk'],
|
||||
toxicity: 'Non-toxic',
|
||||
engineExamples: ['Amateur KNSU motors', 'Educational rocket motors'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['kno3'],
|
||||
},
|
||||
{
|
||||
id: 'dextrose',
|
||||
name: 'Dextrose',
|
||||
symbol: 'Dextrose',
|
||||
formula: 'C₆H₁₂O₆',
|
||||
role: 'fuel',
|
||||
subcategory: 'Solid',
|
||||
description:
|
||||
'Dextrose (glucose) is an alternative to sucrose in candy propellants, forming KNDX with potassium nitrate. It produces a slightly lower flame temperature than KNSU and is similarly accessible. KNDX is popular among amateur rocketeers seeking an alternative binder chemistry.',
|
||||
density: 1540,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: null,
|
||||
meltingPoint: 146,
|
||||
storagePressure: 'N/A (solid)',
|
||||
molecularWeight: 180.16,
|
||||
autoignitionTemp: 390,
|
||||
flammabilityRange: 'N/A (combustible solid)',
|
||||
hazards: ['Combustible solid when mixed with oxidiser', 'Dust explosion risk'],
|
||||
toxicity: 'Non-toxic',
|
||||
engineExamples: ['Amateur KNDX motors'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['kno3'],
|
||||
},
|
||||
|
||||
// ── Oxidisers ───────────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'lox',
|
||||
name: 'Liquid Oxygen',
|
||||
symbol: 'LOX',
|
||||
formula: 'O₂',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Cryogenic',
|
||||
description:
|
||||
'Liquid oxygen is the most commonly used rocket oxidiser, offering high performance, non-toxicity, and relatively affordable cost. Its cryogenic nature (−183 °C) requires insulated tanks and introduces boil-off losses. LOX is compatible with nearly all hydrocarbon fuels and hydrogen, making it the foundation of most high-performance launch vehicles.',
|
||||
density: 1141,
|
||||
densityNote: 'at −183 °C, 1 atm',
|
||||
boilingPoint: -183,
|
||||
meltingPoint: -219,
|
||||
storagePressure: '1 atm (cryogenic dewar)',
|
||||
molecularWeight: 32.00,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (oxidiser; powerfully supports combustion)',
|
||||
hazards: ['Cryogen (−183 °C)', 'Powerful oxidiser', 'Explosive with hydrocarbons under pressure', 'Causes fire on contact with organic materials'],
|
||||
toxicity: 'Non-toxic; high concentrations are physiologically dangerous',
|
||||
engineExamples: ['Merlin', 'Raptor', 'RL-10', 'RD-180', 'BE-4', 'RS-68', 'J-2'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['lh2', 'rp1', 'lch4', 'lc2h6', 'htpb'],
|
||||
},
|
||||
{
|
||||
id: 'n2o4',
|
||||
name: 'Nitrogen Tetroxide',
|
||||
symbol: 'N₂O₄',
|
||||
formula: 'N₂O₄',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Hypergolic',
|
||||
description:
|
||||
'Nitrogen tetroxide is the standard storable oxidiser for military and spacecraft propulsion. It ignites hypergolically with hydrazine-based fuels, enabling reliable engine restarts without an ignition system. NTO is stored as a liquid at ambient temperature but is highly corrosive, toxic, and a strong oxidiser requiring careful materials compatibility.',
|
||||
density: 1440,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 21,
|
||||
meltingPoint: -11,
|
||||
storagePressure: 'Ambient (sealed, slight pressure)',
|
||||
molecularWeight: 92.01,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (oxidiser)',
|
||||
hazards: ['Highly toxic (IDLH 20 ppm)', 'Hypergolic with hydrazines', 'Strong oxidiser', 'Corrosive', 'Reddish-brown toxic fumes (NO₂)'],
|
||||
toxicity: 'Highly toxic; IDLH 20 ppm; causes severe pulmonary oedema',
|
||||
engineExamples: ['Shuttle OMS/RCS', 'Titan II/IV', 'Proton (second/third stage)', 'AJ10', 'Viking'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['udmh', 'mmh', 'aerozine50'],
|
||||
},
|
||||
{
|
||||
id: 'htp',
|
||||
name: 'High-Test Peroxide (HTP)',
|
||||
symbol: 'HTP',
|
||||
formula: 'H₂O₂ (≥90%)',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Green',
|
||||
description:
|
||||
'High-test peroxide is concentrated hydrogen peroxide (≥90%) used as a relatively "green" oxidiser. It decomposes exothermically over a silver or platinum catalyst to produce steam and oxygen, which then supports combustion of a fuel. HTP is significantly less toxic than NTO and has heritage in early British rocketry (Gamma engine) and amateur systems.',
|
||||
density: 1390,
|
||||
densityNote: 'at 20 °C (90%)',
|
||||
boilingPoint: 114,
|
||||
meltingPoint: -12,
|
||||
storagePressure: 'Ambient (vented)',
|
||||
molecularWeight: 34.01,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (oxidiser)',
|
||||
hazards: ['Strong oxidiser', 'Corrosive to skin/eyes', 'Decomposes explosively if contaminated', 'Fire risk with organics'],
|
||||
toxicity: 'Moderate; concentrated solutions cause severe burns',
|
||||
engineExamples: ['Gamma (Black Arrow)', 'Spectre', 'Amateur HTP/RP-1 engines', 'Bloodhound LSR'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['rp1'],
|
||||
},
|
||||
{
|
||||
id: 'n2o',
|
||||
name: 'Nitrous Oxide',
|
||||
symbol: 'N₂O',
|
||||
formula: 'N₂O',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Green',
|
||||
description:
|
||||
'Nitrous oxide is a self-pressurising liquid oxidiser popular in amateur and hybrid rocketry. Its vapour pressure (~5 MPa at 20 °C) eliminates the need for separate pressurisation systems. N₂O is relatively non-toxic and easy to handle compared to NTO or HTP, though it is a potent greenhouse gas and can detonate under specific conditions.',
|
||||
density: 770,
|
||||
densityNote: 'liquid at 20 °C, ~5 MPa',
|
||||
boilingPoint: -88,
|
||||
meltingPoint: -91,
|
||||
storagePressure: '~5 MPa at 20 °C (self-pressurising)',
|
||||
molecularWeight: 44.01,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (oxidiser; non-flammable)',
|
||||
hazards: ['Strong oxidiser', 'Detonation risk at high pressure/temperature', 'Asphyxiant', 'Greenhouse gas'],
|
||||
toxicity: 'Low acute toxicity; can cause hypoxia in high concentrations',
|
||||
engineExamples: ['SpaceShipOne/Two', 'Amateur hybrid motors', 'N₂O/ethanol experimental engines'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['ethanol', 'htpb'],
|
||||
},
|
||||
{
|
||||
id: 'ap',
|
||||
name: 'Ammonium Perchlorate',
|
||||
symbol: 'AP',
|
||||
formula: 'NH₄ClO₄',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Solid',
|
||||
description:
|
||||
'Ammonium perchlorate is the primary oxidiser in composite solid propellants (APCP). It is mixed with HTPB binder and aluminium powder to form the propellant grain. AP is an energetic solid that detonates under severe shock but is stable under normal processing conditions. It is the oxidiser in virtually all modern high-performance solid rocket motors.',
|
||||
density: 1950,
|
||||
densityNote: 'crystal at 20 °C',
|
||||
boilingPoint: null,
|
||||
meltingPoint: 240,
|
||||
storagePressure: 'N/A (solid)',
|
||||
molecularWeight: 117.49,
|
||||
autoignitionTemp: 420,
|
||||
flammabilityRange: 'N/A (solid oxidiser)',
|
||||
hazards: ['Explosive oxidiser', 'Toxic chlorine-containing combustion products (HCl)', 'Skin/eye irritant', 'Environmental: thyroid disruptor'],
|
||||
toxicity: 'Moderate; thyroid disruption with chronic exposure; combustion products (HCl) are toxic',
|
||||
engineExamples: ['Space Shuttle SRB', 'Ariane 5 P230', 'Hobby/high-power APCP motors', 'Ares I/V SRBs'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['htpb'],
|
||||
},
|
||||
{
|
||||
id: 'kno3',
|
||||
name: 'Potassium Nitrate',
|
||||
symbol: 'KNO₃',
|
||||
formula: 'KNO₃',
|
||||
role: 'oxidiser',
|
||||
subcategory: 'Solid',
|
||||
description:
|
||||
'Potassium nitrate (saltpetre) is one of the oldest known oxidisers, the basis of black powder and candy propellants. In KNSU and KNDX it is combined with sucrose or dextrose to make an accessible, low-cost propellant for educational and amateur rocketry. Performance is modest but manufacturing complexity is low.',
|
||||
density: 2109,
|
||||
densityNote: 'crystal at 20 °C',
|
||||
boilingPoint: 400,
|
||||
meltingPoint: 334,
|
||||
storagePressure: 'N/A (solid)',
|
||||
molecularWeight: 101.10,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (solid oxidiser)',
|
||||
hazards: ['Oxidiser; accelerates combustion', 'Explosive when mixed with fuels and subjected to shock/heat', 'Irritant'],
|
||||
toxicity: 'Low acute toxicity; irritant at high concentrations',
|
||||
engineExamples: ['Amateur KNSU/KNDX candy motors', 'Black powder igniters'],
|
||||
catalyticIsp: null,
|
||||
compatibleWith: ['sucrose', 'dextrose'],
|
||||
},
|
||||
|
||||
// ── Monopropellants ─────────────────────────────────────────────────────
|
||||
{
|
||||
id: 'n2h4_mono',
|
||||
name: 'Hydrazine',
|
||||
symbol: 'N₂H₄',
|
||||
formula: 'N₂H₄',
|
||||
role: 'monopropellant',
|
||||
subcategory: 'Storable',
|
||||
description:
|
||||
'Hydrazine is the most widely used monopropellant in spacecraft propulsion. It decomposes exothermically over an iridium/alumina catalyst (Shell 405) to produce hot nitrogen, hydrogen, and ammonia gases. It offers high heritage, good Isp for a monopropellant, and reliable restart capability. Its high toxicity is its primary drawback, driving the search for green replacements.',
|
||||
density: 1004,
|
||||
densityNote: 'at 20 °C',
|
||||
boilingPoint: 114,
|
||||
meltingPoint: 2,
|
||||
storagePressure: 'Ambient (sealed, slight positive pressure)',
|
||||
molecularWeight: 32.05,
|
||||
autoignitionTemp: 270,
|
||||
flammabilityRange: '4.7–100 vol% in air',
|
||||
hazards: ['Highly toxic (IDLH 50 ppm)', 'Carcinogenic', 'Flammable', 'Reactive with many metals', 'Hypergolic with some oxidisers'],
|
||||
toxicity: 'Highly toxic and probable human carcinogen (IARC Group 2A)',
|
||||
engineExamples: ['MR-103 (many NASA/ESA spacecraft)', 'MONARC', 'Aerojet MR-80'],
|
||||
catalyticIsp: 230,
|
||||
// Thermodynamic data for solver (catalytic decomposition)
|
||||
gamma: 1.27,
|
||||
R: 376,
|
||||
T0: 1000,
|
||||
compatibleWith: [],
|
||||
},
|
||||
{
|
||||
id: 'htp_mono',
|
||||
name: 'HTP Monopropellant',
|
||||
symbol: 'HTP',
|
||||
formula: 'H₂O₂ (≥90%)',
|
||||
role: 'monopropellant',
|
||||
subcategory: 'Green',
|
||||
description:
|
||||
'High-test peroxide can be used as a monopropellant by catalytic decomposition over silver or platinum, producing steam and oxygen. Its Isp is lower than hydrazine but it is far less toxic, making it an attractive green alternative for attitude control systems and small thrusters in CubeSats and small satellites.',
|
||||
density: 1390,
|
||||
densityNote: 'at 20 °C (90%)',
|
||||
boilingPoint: 114,
|
||||
meltingPoint: -12,
|
||||
storagePressure: 'Ambient (vented)',
|
||||
molecularWeight: 34.01,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (oxidiser/monoprop)',
|
||||
hazards: ['Strong oxidiser', 'Corrosive', 'Decomposition can be violent if contaminated'],
|
||||
toxicity: 'Moderate; corrosive to skin and eyes at high concentration',
|
||||
engineExamples: ['Rutherford (gas gen.)', 'CubeSat monoprop thrusters', 'Bloodhound LSR'],
|
||||
catalyticIsp: 185,
|
||||
gamma: 1.30,
|
||||
R: 350,
|
||||
T0: 1030,
|
||||
compatibleWith: [],
|
||||
},
|
||||
{
|
||||
id: 'han',
|
||||
name: 'HAN-based (AF-M315E / LMP-103S)',
|
||||
symbol: 'HAN',
|
||||
formula: 'NH₃OHNO₃ blend',
|
||||
role: 'monopropellant',
|
||||
subcategory: 'Green',
|
||||
description:
|
||||
'Hydroxylammonium nitrate (HAN) based ionic liquid monopropellants are the leading green replacements for hydrazine. AF-M315E (ASCENT) and LMP-103S (HPGP) deliver Isp values significantly higher than hydrazine while being far less toxic. They require higher catalyst pre-heat temperatures (~300 °C) than hydrazine but offer reduced handling hazards and regulatory burden.',
|
||||
density: 1460,
|
||||
densityNote: 'at 20 °C (AF-M315E)',
|
||||
boilingPoint: null,
|
||||
meltingPoint: -80,
|
||||
storagePressure: 'Ambient (sealed)',
|
||||
molecularWeight: null,
|
||||
autoignitionTemp: null,
|
||||
flammabilityRange: 'N/A (ionic liquid)',
|
||||
hazards: ['Moderately toxic', 'Energetic; avoid contamination', 'Skin/eye irritant'],
|
||||
toxicity: 'Moderately toxic; significantly safer than hydrazine',
|
||||
engineExamples: ['GPIM (NASA)', 'Lunar Flashlight', 'Many commercial CubeSat thrusters'],
|
||||
catalyticIsp: 250,
|
||||
gamma: 1.25,
|
||||
R: 340,
|
||||
T0: 1930,
|
||||
compatibleWith: [],
|
||||
},
|
||||
]
|
||||
|
||||
// ── Bipropellant combination performance data ────────────────────────────────
|
||||
// Values at ~6.9 MPa chamber pressure unless noted.
|
||||
// gamma and R feed directly into the solver (variable IDs match).
|
||||
// T0 = flameTemp, OF = optimalOF for solver use.
|
||||
|
||||
export const COMBINATIONS = [
|
||||
{
|
||||
fuelId: 'lh2',
|
||||
oxidiserId: 'lox',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 455,
|
||||
flameTemp: 3250,
|
||||
optimalOF: 6.0,
|
||||
gamma: 1.21,
|
||||
R: 934,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Cryogenic Bipropellant',
|
||||
notes: 'Highest Isp liquid bipropellant. Extremely demanding cryogenic handling for both propellants.',
|
||||
engines: ['RL-10', 'J-2', 'RS-68', 'Vulcain 2', 'LE-7A', 'HM7B'],
|
||||
},
|
||||
{
|
||||
fuelId: 'rp1',
|
||||
oxidiserId: 'lox',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 363,
|
||||
flameTemp: 3570,
|
||||
optimalOF: 2.6,
|
||||
gamma: 1.24,
|
||||
R: 361,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Cryogenic Bipropellant',
|
||||
notes: 'High density, high performance. Proven across dozens of vehicles and engines over 60+ years.',
|
||||
engines: ['Merlin', 'RD-180', 'F-1', 'NK-33', 'Rutherford'],
|
||||
},
|
||||
{
|
||||
fuelId: 'lch4',
|
||||
oxidiserId: 'lox',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 380,
|
||||
flameTemp: 3500,
|
||||
optimalOF: 3.5,
|
||||
gamma: 1.20,
|
||||
R: 416,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Cryogenic Bipropellant',
|
||||
notes: 'Reusable-friendly; clean combustion reduces coking. Potential ISRU on Mars.',
|
||||
engines: ['Raptor', 'BE-4', 'Prometheus'],
|
||||
},
|
||||
{
|
||||
fuelId: 'lc2h6',
|
||||
oxidiserId: 'lox',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 375,
|
||||
flameTemp: 3480,
|
||||
optimalOF: 3.2,
|
||||
gamma: 1.21,
|
||||
R: 390,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Cryogenic Bipropellant',
|
||||
notes: 'Higher density than methane with comparable performance. Primarily in research.',
|
||||
engines: ['Research engines'],
|
||||
},
|
||||
{
|
||||
fuelId: 'udmh',
|
||||
oxidiserId: 'n2o4',
|
||||
isHypergolic: true,
|
||||
vacuumIsp: 340,
|
||||
flameTemp: 3040,
|
||||
optimalOF: 2.6,
|
||||
gamma: 1.25,
|
||||
R: 320,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Storable Bipropellant',
|
||||
notes: 'Hypergolic storable combination. Highly toxic but reliable; widely used in Russian and Chinese vehicles.',
|
||||
engines: ['RD-253 (Proton)', 'YF-20', 'Viking (Ariane 1–4)'],
|
||||
},
|
||||
{
|
||||
fuelId: 'mmh',
|
||||
oxidiserId: 'n2o4',
|
||||
isHypergolic: true,
|
||||
vacuumIsp: 341,
|
||||
flameTemp: 3000,
|
||||
optimalOF: 1.65,
|
||||
gamma: 1.25,
|
||||
R: 318,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Storable Bipropellant',
|
||||
notes: 'Primary spacecraft upper-stage and RCS propellant. Decades of heritage.',
|
||||
engines: ['Shuttle OMS/RCS', 'Orion SM', 'AJ10', 'R-4D', 'Aestus'],
|
||||
},
|
||||
{
|
||||
fuelId: 'aerozine50',
|
||||
oxidiserId: 'n2o4',
|
||||
isHypergolic: true,
|
||||
vacuumIsp: 342,
|
||||
flameTemp: 3070,
|
||||
optimalOF: 1.9,
|
||||
gamma: 1.25,
|
||||
R: 316,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Storable Bipropellant',
|
||||
notes: 'Better stability than pure hydrazine. Used in Titan launch vehicles and the Apollo Lunar Module.',
|
||||
engines: ['Titan II/III/IV', 'Lunar Module DPS', 'TR-201'],
|
||||
},
|
||||
{
|
||||
fuelId: 'rp1',
|
||||
oxidiserId: 'htp',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 328,
|
||||
flameTemp: 2900,
|
||||
optimalOF: 7.0,
|
||||
gamma: 1.26,
|
||||
R: 285,
|
||||
chamberPressureRef: '2 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Pressure-fed Bipropellant',
|
||||
notes: 'Relatively non-toxic combination. HTP decomposes catalytically before combustion.',
|
||||
engines: ['Gamma (Black Arrow)', 'Spectre', 'Bloodhound LSR powerplant'],
|
||||
},
|
||||
{
|
||||
fuelId: 'ethanol',
|
||||
oxidiserId: 'n2o',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 295,
|
||||
flameTemp: 2700,
|
||||
optimalOF: 4.5,
|
||||
gamma: 1.24,
|
||||
R: 310,
|
||||
chamberPressureRef: '~3 MPa',
|
||||
expansionRatioRef: 10,
|
||||
energeticCategory: 'Pressure-fed Bipropellant',
|
||||
notes: 'Popular amateur/experimental combination; N₂O self-pressurises. Much safer than hypergolics.',
|
||||
engines: ['Amateur N₂O/ethanol motors'],
|
||||
},
|
||||
{
|
||||
fuelId: 'htpb',
|
||||
oxidiserId: 'n2o',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 280,
|
||||
flameTemp: 2700,
|
||||
optimalOF: 7.0,
|
||||
gamma: 1.23,
|
||||
R: 300,
|
||||
chamberPressureRef: '~3.5 MPa',
|
||||
expansionRatioRef: 10,
|
||||
energeticCategory: 'Hybrid',
|
||||
notes: 'Simple and safe hybrid combination. O/F varies with regression rate and geometry.',
|
||||
engines: ['SpaceShipOne/Two', 'NAMMO hybrid', 'Amateur hybrids'],
|
||||
},
|
||||
{
|
||||
fuelId: 'htpb',
|
||||
oxidiserId: 'lox',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 350,
|
||||
flameTemp: 3200,
|
||||
optimalOF: 2.3,
|
||||
gamma: 1.22,
|
||||
R: 360,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 40,
|
||||
energeticCategory: 'Hybrid',
|
||||
notes: 'Higher performance hybrid. Requires cryogenic LOX handling alongside solid grain.',
|
||||
engines: ['Research vehicles', 'Peregrine (small launcher studies)'],
|
||||
},
|
||||
{
|
||||
fuelId: 'htpb',
|
||||
oxidiserId: 'ap',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 275,
|
||||
flameTemp: 3300,
|
||||
optimalOF: null,
|
||||
gamma: 1.24,
|
||||
R: 320,
|
||||
chamberPressureRef: '6.9 MPa',
|
||||
expansionRatioRef: 10,
|
||||
energeticCategory: 'Solid',
|
||||
notes: 'Standard APCP composite solid propellant. Al powder added for density/performance. O/F is premixed; no separate ratio.',
|
||||
engines: ['Space Shuttle SRB', 'Ariane 5 P230', 'Hobby APCP motors'],
|
||||
},
|
||||
{
|
||||
fuelId: 'sucrose',
|
||||
oxidiserId: 'kno3',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 164,
|
||||
flameTemp: 1720,
|
||||
optimalOF: 0.65,
|
||||
gamma: 1.30,
|
||||
R: 290,
|
||||
chamberPressureRef: '~7 MPa',
|
||||
expansionRatioRef: 10,
|
||||
energeticCategory: 'Solid',
|
||||
notes: 'KNSU: low cost, low performance candy propellant. Ideal for educational motors.',
|
||||
engines: ['Amateur KNSU motors'],
|
||||
},
|
||||
{
|
||||
fuelId: 'dextrose',
|
||||
oxidiserId: 'kno3',
|
||||
isHypergolic: false,
|
||||
vacuumIsp: 160,
|
||||
flameTemp: 1700,
|
||||
optimalOF: 0.63,
|
||||
gamma: 1.31,
|
||||
R: 288,
|
||||
chamberPressureRef: '~7 MPa',
|
||||
expansionRatioRef: 10,
|
||||
energeticCategory: 'Solid',
|
||||
notes: 'KNDX: slightly lower flame temperature than KNSU. Comparable accessibility.',
|
||||
engines: ['Amateur KNDX motors'],
|
||||
},
|
||||
]
|
||||
|
||||
// ── Derived solver data ──────────────────────────────────────────────────────
|
||||
// PROPELLANTS and PROPELLANT_TYPES replace propellants.js as the single source
|
||||
// of truth consumed by PropellantModal / useSolver.
|
||||
|
||||
const substanceMap = Object.fromEntries(SUBSTANCES.map(s => [s.id, s]))
|
||||
|
||||
export const PROPELLANT_TYPES = [
|
||||
'All',
|
||||
'Cryogenic Bipropellant',
|
||||
'Storable Bipropellant',
|
||||
'Pressure-fed Bipropellant',
|
||||
'Hybrid',
|
||||
'Solid',
|
||||
'Monopropellant',
|
||||
]
|
||||
|
||||
export const PROPELLANTS = [
|
||||
// Bipropellant combinations
|
||||
...COMBINATIONS.map(c => {
|
||||
const fuel = substanceMap[c.fuelId]
|
||||
const ox = substanceMap[c.oxidiserId]
|
||||
const values = { gamma: c.gamma, R: c.R, T0: c.flameTemp }
|
||||
if (c.optimalOF != null) values.OF = c.optimalOF
|
||||
if (fuel?.density != null) values.rhoFuel = fuel.density
|
||||
if (ox?.density != null) values.rhoOx = ox.density
|
||||
return {
|
||||
id: `${c.oxidiserId}_${c.fuelId}`,
|
||||
name: `${ox?.symbol ?? c.oxidiserId} / ${fuel?.symbol ?? c.fuelId}`,
|
||||
oxidizer: ox?.name ?? c.oxidiserId,
|
||||
fuel: fuel?.name ?? c.fuelId,
|
||||
type: c.energeticCategory,
|
||||
description: c.notes,
|
||||
vacuumIsp: c.vacuumIsp,
|
||||
values,
|
||||
notes: `Theoretical vacuum Isp at ε = ${c.expansionRatioRef}:1, pc = ${c.chamberPressureRef}`,
|
||||
}
|
||||
}),
|
||||
// Monopropellants
|
||||
...SUBSTANCES
|
||||
.filter(s => s.role === 'monopropellant')
|
||||
.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
oxidizer: null,
|
||||
fuel: s.name,
|
||||
type: 'Monopropellant',
|
||||
description: s.description,
|
||||
vacuumIsp: s.catalyticIsp,
|
||||
values: { gamma: s.gamma, R: s.R, T0: s.T0 },
|
||||
notes: 'Catalytic decomposition; vacuum Isp',
|
||||
})),
|
||||
]
|
||||
42
src/engine/numerics.js
Normal file
42
src/engine/numerics.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Numerical methods for transcendental equations
|
||||
|
||||
/**
|
||||
* Bisection method — finds root of f in [lo, hi] within tolerance.
|
||||
* Returns null if no sign change is found.
|
||||
*/
|
||||
export function bisect(f, lo, hi, tol = 1e-10, maxIter = 200) {
|
||||
let flo = f(lo)
|
||||
let fhi = f(hi)
|
||||
if (!isFinite(flo) || !isFinite(fhi)) return null
|
||||
if (flo * fhi > 0) return null // no sign change
|
||||
for (let i = 0; i < maxIter; i++) {
|
||||
const mid = (lo + hi) / 2
|
||||
if ((hi - lo) / 2 < tol) return mid
|
||||
const fmid = f(mid)
|
||||
if (fmid === 0) return mid
|
||||
if (flo * fmid < 0) { hi = mid; fhi = fmid }
|
||||
else { lo = mid; flo = fmid }
|
||||
}
|
||||
return (lo + hi) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Isentropic area-ratio function A/A* as a function of Mach M and gamma.
|
||||
* A/A* = (1/M) * [(2/(γ+1)) * (1 + (γ-1)/2 * M²)]^((γ+1)/(2(γ-1)))
|
||||
*/
|
||||
export function areaRatioFromMach(M, gamma) {
|
||||
const exp = (gamma + 1) / (2 * (gamma - 1))
|
||||
const base = (2 / (gamma + 1)) * (1 + (gamma - 1) / 2 * M * M)
|
||||
return (1 / M) * Math.pow(base, exp)
|
||||
}
|
||||
|
||||
/**
|
||||
* Solve Mach number from area ratio and gamma using bisection.
|
||||
* supersonic=true searches M > 1, false searches M < 1.
|
||||
* Returns null if unsolvable.
|
||||
*/
|
||||
export function machFromAreaRatio(eps, gamma, supersonic = true) {
|
||||
const f = (M) => areaRatioFromMach(M, gamma) - eps
|
||||
if (supersonic) return bisect(f, 1.0001, 200, 1e-9)
|
||||
else return bisect(f, 0.0001, 0.9999, 1e-9)
|
||||
}
|
||||
234
src/engine/rocketDesignCalcs.js
Normal file
234
src/engine/rocketDesignCalcs.js
Normal file
@@ -0,0 +1,234 @@
|
||||
// Pure calculation functions for rocket vehicle design (no React)
|
||||
|
||||
const G0 = 9.80665 // m/s² standard gravity
|
||||
|
||||
/**
|
||||
* 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
|
||||
* structMassFraction, // 0–1 (dry mass fraction of propellant mass)
|
||||
* }
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function calcRocketGeometry(engineData, rocketInputs) {
|
||||
if (!engineData || !rocketInputs) return null
|
||||
|
||||
const {
|
||||
outerRadius,
|
||||
burnTime,
|
||||
arrangement,
|
||||
innerPropellant,
|
||||
rhoFuel,
|
||||
rhoOx,
|
||||
payloadMass,
|
||||
payloadBayLength,
|
||||
structMassFraction,
|
||||
} = rocketInputs
|
||||
|
||||
const Isp = engineData?.allThermo?.Isp
|
||||
const F = engineData?.allThermo?.F
|
||||
const mdot = engineData?.allThermo?.mdot
|
||||
const OF = engineData?.allThermo?.OF
|
||||
|
||||
// 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 (ogive approximation: height = 2R) ────────────────────
|
||||
const L_nose = 2 * R
|
||||
|
||||
// ── 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 ───────────────────────────────────────────────────
|
||||
let L_tank_fuel, L_tank_ox, L_tank, r_inner
|
||||
|
||||
if (arrangement === 'coaxial') {
|
||||
// Both tanks share the same axial section; total volume in annulus + inner cylinder
|
||||
// Inner tank radius from the smaller propellant volume
|
||||
// Outer tank occupies the remaining annular area
|
||||
const V_inner = innerPropellant === 'fuel' ? V_fuel : V_ox
|
||||
const V_outer = innerPropellant === 'fuel' ? V_ox : V_fuel
|
||||
|
||||
// Solve for tank length using outer volume first (conservative — outer is usually larger)
|
||||
// V_outer = π (R² - r_inner²) L_tank and V_inner = π r_inner² L_tank
|
||||
// From V_inner / (V_inner + V_outer) = r_inner² / R²
|
||||
// → r_inner = R √(V_inner / V_prop_total)
|
||||
r_inner = R * Math.sqrt(V_inner / V_prop)
|
||||
// Guard: inner radius must be smaller than outer
|
||||
if (r_inner >= R) r_inner = R * 0.7
|
||||
|
||||
L_tank = V_prop / (Math.PI * R * R)
|
||||
L_tank_fuel = arrangement === 'coaxial' ? L_tank : null
|
||||
L_tank_ox = arrangement === 'coaxial' ? L_tank : null
|
||||
} else {
|
||||
// Tandem: stacked cylinders
|
||||
L_tank_fuel = V_fuel / (Math.PI * R * R)
|
||||
L_tank_ox = V_ox / (Math.PI * R * R)
|
||||
L_tank = L_tank_fuel + L_tank_ox
|
||||
r_inner = null
|
||||
}
|
||||
|
||||
// ── Total vehicle length ────────────────────────────────────────────
|
||||
const totalLength = L_nose + L_payload + L_tank + L_engine
|
||||
|
||||
// ── Mass budget ─────────────────────────────────────────────────────
|
||||
const m_prop = m_fuel + m_ox
|
||||
const m_struct = m_prop * (structMassFraction ?? 0.10)
|
||||
const m_payload = payloadMass ?? 0
|
||||
const m_dry = m_struct + m_payload
|
||||
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_engine,
|
||||
totalLength,
|
||||
r_inner,
|
||||
// Mass
|
||||
m_fuel,
|
||||
m_ox,
|
||||
V_fuel,
|
||||
V_ox,
|
||||
m_prop,
|
||||
m_struct,
|
||||
m_payload,
|
||||
m_dry,
|
||||
m_wet,
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
56
src/engine/rocketExportImport.js
Normal file
56
src/engine/rocketExportImport.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { downloadBlob } from './exportImport.js'
|
||||
|
||||
export { downloadBlob }
|
||||
|
||||
/**
|
||||
* Build a rocket design JSON Blob for download.
|
||||
* Schema version 1: inputs section is re-importable; results is reference-only.
|
||||
*/
|
||||
export function exportRocketJSON({ outerRadius, tankConfig, propDensities, payload, structure, engineData, geometry }) {
|
||||
const payload_ = {
|
||||
version: 1,
|
||||
type: 'rocket_design',
|
||||
exportedAt: new Date().toISOString(),
|
||||
inputs: {
|
||||
outerRadius,
|
||||
tankConfig,
|
||||
propDensities,
|
||||
payload,
|
||||
structure,
|
||||
},
|
||||
engineData: engineData ?? null,
|
||||
results: geometry ?? null,
|
||||
}
|
||||
return new Blob([JSON.stringify(payload_, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an imported rocket design JSON string and return the inputs section.
|
||||
* Throws a descriptive Error if the file is invalid.
|
||||
*/
|
||||
export function parseRocketImport(jsonString) {
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(jsonString)
|
||||
} catch {
|
||||
throw new Error('File is not valid JSON.')
|
||||
}
|
||||
|
||||
if (data.type !== 'rocket_design') {
|
||||
throw new Error('This file is not a rocket design export.')
|
||||
}
|
||||
if (data.version !== 1) {
|
||||
throw new Error(`Unsupported export version: ${data.version}`)
|
||||
}
|
||||
|
||||
const { outerRadius, tankConfig, propDensities, payload, structure } = data.inputs ?? {}
|
||||
|
||||
return {
|
||||
outerRadius: outerRadius ?? null,
|
||||
tankConfig: tankConfig ?? null,
|
||||
propDensities: propDensities ?? null,
|
||||
payload: payload ?? null,
|
||||
structure: structure ?? null,
|
||||
engineData: data.engineData ?? null,
|
||||
}
|
||||
}
|
||||
127
src/engine/solver.js
Normal file
127
src/engine/solver.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { EQUATIONS } from './equations.js'
|
||||
import { VARIABLES } from './variables.js'
|
||||
|
||||
/**
|
||||
* Run the constraint-propagation solver.
|
||||
*
|
||||
* @param {Record<string, number>} knownValues – user-entered { varId: value }
|
||||
* @returns {{
|
||||
* solved: Record<string, { value: number, equationId: string, equationName: string }>,
|
||||
* missing: Record<string, string[][]> – varId → list of variable-sets that would unlock it
|
||||
* }}
|
||||
*/
|
||||
export function solve(knownValues) {
|
||||
const known = { ...knownValues }
|
||||
const solved = {}
|
||||
|
||||
let progress = true
|
||||
while (progress) {
|
||||
progress = false
|
||||
for (const eq of EQUATIONS) {
|
||||
for (const target of eq.variables) {
|
||||
if (target in known) continue
|
||||
if (!eq.solvers[target]) continue
|
||||
|
||||
const solver = eq.solvers[target]
|
||||
const others = solver.requires
|
||||
? solver.requires(known)
|
||||
: eq.variables.filter(v => v !== target)
|
||||
if (others.every(v => v in known)) {
|
||||
try {
|
||||
const val = solver(known)
|
||||
if (isFinite(val) && !isNaN(val)) {
|
||||
known[target] = val
|
||||
solved[target] = { value: val, equationId: eq.id, equationName: eq.name }
|
||||
progress = true
|
||||
break // restart inner loop after new solve
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out what's missing: for each unsolved variable in the workspace,
|
||||
// find all equations that could solve it and report what each needs.
|
||||
const missing = {}
|
||||
const allWorkspaceVars = Object.keys(knownValues)
|
||||
|
||||
for (const varId of allWorkspaceVars) {
|
||||
if (varId in solved || varId in knownValues) continue
|
||||
// This shouldn't happen (workspace vars that are neither known nor solved)
|
||||
// but keep for symmetry.
|
||||
}
|
||||
|
||||
// For all variables NOT in known at all, find what each equation needs
|
||||
for (const eq of EQUATIONS) {
|
||||
for (const target of eq.variables) {
|
||||
if (target in known) continue
|
||||
if (!eq.solvers[target]) continue
|
||||
|
||||
const allRequired = eq.solvers[target]?.requires
|
||||
? eq.solvers[target].requires(known)
|
||||
: eq.variables.filter(v => v !== target)
|
||||
const needed = allRequired.filter(v => !(v in known))
|
||||
if (needed.length > 0) {
|
||||
if (!missing[target]) missing[target] = []
|
||||
// Avoid duplicate entries
|
||||
const key = needed.sort().join(',')
|
||||
if (!missing[target].some(arr => arr.slice().sort().join(',') === key)) {
|
||||
missing[target].push(needed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { solved, missing }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current known+solved values, return a summary of
|
||||
* what each unsolvable workspace variable still needs.
|
||||
*
|
||||
* @param {string[]} workspaceVarIds – ids of vars on the workspace
|
||||
* @param {Record<string, number>} knownValues
|
||||
* @param {Record<string, any>} solvedValues
|
||||
* @returns {Array<{ varId, name, symbol, options: string[][] }>}
|
||||
*/
|
||||
export function getMissingReport(workspaceVarIds, knownValues, solvedValues) {
|
||||
const allKnown = {
|
||||
...knownValues,
|
||||
...Object.fromEntries(Object.entries(solvedValues).map(([k, v]) => [k, v.value])),
|
||||
}
|
||||
|
||||
const report = []
|
||||
for (const varId of workspaceVarIds) {
|
||||
if (varId in allKnown) continue // it's known or solved, skip
|
||||
|
||||
const options = []
|
||||
for (const eq of EQUATIONS) {
|
||||
if (!eq.variables.includes(varId)) continue
|
||||
if (!eq.solvers[varId]) continue
|
||||
|
||||
const allRequired = eq.solvers[varId]?.requires
|
||||
? eq.solvers[varId].requires(allKnown)
|
||||
: eq.variables.filter(v => v !== varId)
|
||||
const needed = allRequired.filter(v => !(v in allKnown))
|
||||
if (needed.length > 0) {
|
||||
const key = needed.slice().sort().join(',')
|
||||
if (!options.some(o => o.slice().sort().join(',') === key)) {
|
||||
options.push(needed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
const varDef = VARIABLES[varId]
|
||||
report.push({
|
||||
varId,
|
||||
name: varDef?.name ?? varId,
|
||||
symbol: varDef?.symbol ?? varId,
|
||||
options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
122
src/engine/units.js
Normal file
122
src/engine/units.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// Unit families for display/input conversion.
|
||||
// The solver always works in SI internally.
|
||||
// Each unit: { id, label, toSI(v), fromSI(v) }
|
||||
|
||||
export const UNIT_FAMILIES = {
|
||||
force: {
|
||||
units: [
|
||||
{ id: 'N', label: 'N', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'kN', label: 'kN', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
|
||||
{ id: 'MN', label: 'MN', toSI: v => v * 1e6, fromSI: v => v / 1e6 },
|
||||
{ id: 'lbf', label: 'lbf', toSI: v => v * 4.44822, fromSI: v => v / 4.44822 },
|
||||
],
|
||||
},
|
||||
pressure: {
|
||||
units: [
|
||||
{ id: 'Pa', label: 'Pa', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'kPa', label: 'kPa', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
|
||||
{ id: 'MPa', label: 'MPa', toSI: v => v * 1e6, fromSI: v => v / 1e6 },
|
||||
{ id: 'bar', label: 'bar', toSI: v => v * 1e5, fromSI: v => v / 1e5 },
|
||||
{ id: 'psi', label: 'psi', toSI: v => v * 6894.76, fromSI: v => v / 6894.76 },
|
||||
{ id: 'atm', label: 'atm', toSI: v => v * 101325, fromSI: v => v / 101325 },
|
||||
],
|
||||
},
|
||||
mass: {
|
||||
units: [
|
||||
{ id: 'kg', label: 'kg', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'g', label: 'g', toSI: v => v * 0.001, fromSI: v => v * 1000 },
|
||||
{ id: 't', label: 't', toSI: v => v * 1000, fromSI: v => v * 0.001 },
|
||||
{ id: 'lb', label: 'lb', toSI: v => v * 0.453592, fromSI: v => v / 0.453592 },
|
||||
{ id: 'slug', label: 'slug', toSI: v => v * 14.5939, fromSI: v => v / 14.5939 },
|
||||
],
|
||||
},
|
||||
massflow: {
|
||||
units: [
|
||||
{ id: 'kg/s', label: 'kg/s', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'g/s', label: 'g/s', toSI: v => v * 0.001, fromSI: v => v * 1000 },
|
||||
{ id: 'lb/s', label: 'lb/s', toSI: v => v * 0.453592, fromSI: v => v / 0.453592 },
|
||||
{ id: 'lb/min', label: 'lb/min', toSI: v => v * 0.453592 / 60, fromSI: v => v / 0.453592 * 60 },
|
||||
],
|
||||
},
|
||||
velocity: {
|
||||
units: [
|
||||
{ id: 'm/s', label: 'm/s', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'km/s', label: 'km/s', toSI: v => v * 1000, fromSI: v => v / 1000 },
|
||||
{ id: 'ft/s', label: 'ft/s', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
|
||||
],
|
||||
},
|
||||
acceleration: {
|
||||
units: [
|
||||
{ id: 'm/s²', label: 'm/s²', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'ft/s²', label: 'ft/s²', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
|
||||
],
|
||||
},
|
||||
temperature: {
|
||||
units: [
|
||||
{ id: 'K', label: 'K', toSI: v => v, fromSI: v => v },
|
||||
{ id: '°C', label: '°C', toSI: v => v + 273.15, fromSI: v => v - 273.15 },
|
||||
{ id: '°F', label: '°F', toSI: v => (v - 32) * 5 / 9 + 273.15, fromSI: v => (v - 273.15) * 9 / 5 + 32 },
|
||||
{ id: '°R', label: '°R', toSI: v => v * 5 / 9, fromSI: v => v * 9 / 5 },
|
||||
],
|
||||
},
|
||||
area: {
|
||||
units: [
|
||||
{ id: 'm²', label: 'm²', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'cm²', label: 'cm²', toSI: v => v * 1e-4, fromSI: v => v * 1e4 },
|
||||
{ id: 'mm²', label: 'mm²', toSI: v => v * 1e-6, fromSI: v => v * 1e6 },
|
||||
{ id: 'in²', label: 'in²', toSI: v => v * 6.4516e-4, fromSI: v => v / 6.4516e-4 },
|
||||
],
|
||||
},
|
||||
time: {
|
||||
units: [
|
||||
{ id: 's', label: 's', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'ms', label: 'ms', toSI: v => v * 0.001, fromSI: v => v * 1000 },
|
||||
{ id: 'min', label: 'min', toSI: v => v * 60, fromSI: v => v / 60 },
|
||||
],
|
||||
},
|
||||
impulse: {
|
||||
units: [
|
||||
{ id: 'N·s', label: 'N·s', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'kN·s', label: 'kN·s', toSI: v => v * 1e3, fromSI: v => v / 1e3 },
|
||||
{ id: 'lbf·s', label: 'lbf·s', toSI: v => v * 4.44822, fromSI: v => v / 4.44822 },
|
||||
],
|
||||
},
|
||||
density: {
|
||||
units: [
|
||||
{ id: 'kg/m³', label: 'kg/m³', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'g/cm³', label: 'g/cm³', toSI: v => v * 1000, fromSI: v => v / 1000 },
|
||||
{ id: 'lb/ft³', label: 'lb/ft³', toSI: v => v * 16.0185, fromSI: v => v / 16.0185 },
|
||||
],
|
||||
},
|
||||
spec_gas: {
|
||||
units: [
|
||||
{ id: 'J/(kg·K)', label: 'J/(kg·K)', toSI: v => v, fromSI: v => v },
|
||||
],
|
||||
},
|
||||
length: {
|
||||
units: [
|
||||
{ id: 'm', label: 'm', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'mm', label: 'mm', toSI: v => v * 0.001, fromSI: v => v * 1000 },
|
||||
{ id: 'cm', label: 'cm', toSI: v => v * 0.01, fromSI: v => v * 100 },
|
||||
{ id: 'in', label: 'in', toSI: v => v * 0.0254, fromSI: v => v / 0.0254 },
|
||||
{ id: 'ft', label: 'ft', toSI: v => v * 0.3048, fromSI: v => v / 0.3048 },
|
||||
],
|
||||
},
|
||||
volume: {
|
||||
units: [
|
||||
{ id: 'm³', label: 'm³', toSI: v => v, fromSI: v => v },
|
||||
{ id: 'L', label: 'L', toSI: v => v * 0.001, fromSI: v => v * 1000 },
|
||||
{ id: 'cm³', label: 'cm³', toSI: v => v * 1e-6, fromSI: v => v * 1e6 },
|
||||
{ id: 'in³', label: 'in³', toSI: v => v * 1.6387e-5, fromSI: v => v / 1.6387e-5 },
|
||||
],
|
||||
},
|
||||
dimensionless: {
|
||||
units: [
|
||||
{ id: '—', label: '—', toSI: v => v, fromSI: v => v },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export function getUnitsForFamily(familyId) {
|
||||
return UNIT_FAMILIES[familyId]?.units ?? UNIT_FAMILIES.dimensionless.units
|
||||
}
|
||||
272
src/engine/variables.js
Normal file
272
src/engine/variables.js
Normal file
@@ -0,0 +1,272 @@
|
||||
// All rocketry variable definitions
|
||||
// id: internal key used in equations
|
||||
// symbol: display symbol (unicode)
|
||||
// name: human-readable name
|
||||
// units: SI units
|
||||
// description: tooltip text
|
||||
// category: grouping in palette
|
||||
// unitFamily: key into UNIT_FAMILIES for conversion
|
||||
|
||||
export const VARIABLES = {
|
||||
// ── Thrust ────────────────────────────────────────────────────────────
|
||||
F: {
|
||||
id: 'F', symbol: 'F', name: 'Thrust', units: 'N',
|
||||
description: 'Net thrust force produced by the engine',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'force',
|
||||
},
|
||||
mdot: {
|
||||
id: 'mdot', symbol: 'ṁ', name: 'Mass Flow Rate', units: 'kg/s',
|
||||
description: 'Total propellant mass flow rate through the engine',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'massflow',
|
||||
},
|
||||
Ve: {
|
||||
id: 'Ve', symbol: 'Vₑ', name: 'Exhaust Velocity', units: 'm/s',
|
||||
description: 'Actual kinematic exit velocity at the nozzle exit plane',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
ceff: {
|
||||
id: 'ceff', symbol: 'cₑ', name: 'Effective Exhaust Velocity', units: 'm/s',
|
||||
description: 'F/ṁ = Isp·g₀; includes both momentum and pressure thrust',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
pe: {
|
||||
id: 'pe', symbol: 'pₑ', name: 'Exit Pressure', units: 'Pa',
|
||||
description: 'Static pressure at the nozzle exit plane',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'pressure',
|
||||
},
|
||||
pa: {
|
||||
id: 'pa', symbol: 'pₐ', name: 'Ambient Pressure', units: 'Pa',
|
||||
description: 'Surrounding ambient static pressure',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'pressure',
|
||||
},
|
||||
Ae: {
|
||||
id: 'Ae', symbol: 'Aₑ', name: 'Exit Area', units: 'm²',
|
||||
description: 'Cross-sectional area of the nozzle exit',
|
||||
category: 'Thrust',
|
||||
unitFamily: 'area',
|
||||
},
|
||||
|
||||
// ── Specific Impulse ──────────────────────────────────────────────────
|
||||
Isp: {
|
||||
id: 'Isp', symbol: 'Isp', name: 'Specific Impulse', units: 's',
|
||||
description: 'Thrust produced per unit weight flow of propellant',
|
||||
category: 'Specific Impulse',
|
||||
unitFamily: 'time',
|
||||
},
|
||||
g0: {
|
||||
id: 'g0', symbol: 'g₀', name: 'Standard Gravity', units: 'm/s²',
|
||||
description: 'Standard gravitational acceleration (9.80665 m/s²)',
|
||||
category: 'Specific Impulse',
|
||||
unitFamily: 'acceleration',
|
||||
},
|
||||
|
||||
// ── Characteristic Velocity / Thrust Coefficient ──────────────────────
|
||||
cstar: {
|
||||
id: 'cstar', symbol: 'c*', name: 'Characteristic Velocity', units: 'm/s',
|
||||
description: 'Measure of combustion efficiency; c* = p₀·Aₜ/ṁ',
|
||||
category: 'Nozzle Performance',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
CF: {
|
||||
id: 'CF', symbol: 'Cꜰ', name: 'Thrust Coefficient', units: '—',
|
||||
description: 'Dimensionless nozzle performance factor; Cꜰ = F/(p₀·Aₜ)',
|
||||
category: 'Nozzle Performance',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
p0: {
|
||||
id: 'p0', symbol: 'p₀', name: 'Chamber Pressure', units: 'Pa',
|
||||
description: 'Stagnation (total) pressure in the combustion chamber',
|
||||
category: 'Nozzle Performance',
|
||||
unitFamily: 'pressure',
|
||||
},
|
||||
At: {
|
||||
id: 'At', symbol: 'Aₜ', name: 'Throat Area', units: 'm²',
|
||||
description: 'Cross-sectional area at the nozzle throat',
|
||||
category: 'Nozzle Performance',
|
||||
unitFamily: 'area',
|
||||
},
|
||||
|
||||
// ── Tsiolkovsky Rocket Equation ───────────────────────────────────────
|
||||
dv: {
|
||||
id: 'dv', symbol: 'Δv', name: 'Delta-v', units: 'm/s',
|
||||
description: 'Total velocity change budget for a manoeuvre',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
m0: {
|
||||
id: 'm0', symbol: 'm₀', name: 'Initial Mass', units: 'kg',
|
||||
description: 'Total vehicle mass at ignition (wet mass)',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'mass',
|
||||
},
|
||||
mf: {
|
||||
id: 'mf', symbol: 'mf', name: 'Final Mass', units: 'kg',
|
||||
description: 'Vehicle mass after propellant is expended (dry mass)',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'mass',
|
||||
},
|
||||
mp: {
|
||||
id: 'mp', symbol: 'mₚ', name: 'Propellant Mass', units: 'kg',
|
||||
description: 'Mass of propellant consumed: mₚ = m₀ − mf',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'mass',
|
||||
},
|
||||
MR: {
|
||||
id: 'MR', symbol: 'MR', name: 'Mass Ratio', units: '—',
|
||||
description: 'Ratio of initial to final mass: MR = m₀/mf',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
zeta: {
|
||||
id: 'zeta', symbol: 'ζ', name: 'Propellant Mass Fraction', units: '—',
|
||||
description: 'Fraction of initial mass that is propellant: ζ = mₚ/m₀',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
tb: {
|
||||
id: 'tb', symbol: 'tᵦ', name: 'Burn Time', units: 's',
|
||||
description: 'Duration of engine burn',
|
||||
category: 'Rocket Equation',
|
||||
unitFamily: 'time',
|
||||
},
|
||||
|
||||
// ── Isentropic Flow ───────────────────────────────────────────────────
|
||||
M: {
|
||||
id: 'M', symbol: 'M', name: 'Mach Number', units: '—',
|
||||
description: 'Local Mach number at a cross-section',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
gamma: {
|
||||
id: 'gamma', symbol: 'γ', name: 'Ratio of Specific Heats', units: '—',
|
||||
description: 'Ratio of specific heats cₚ/cᵥ for the gas (≈1.4 for air, ~1.2 for rocket exhaust)',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
R: {
|
||||
id: 'R', symbol: 'R', name: 'Specific Gas Constant', units: 'J/(kg·K)',
|
||||
description: 'Gas constant for the propellant gas: R = R̄/M_mol',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'spec_gas',
|
||||
},
|
||||
T: {
|
||||
id: 'T', symbol: 'T', name: 'Static Temperature', units: 'K',
|
||||
description: 'Local static temperature at a cross-section',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'temperature',
|
||||
},
|
||||
T0: {
|
||||
id: 'T0', symbol: 'T₀', name: 'Stagnation Temperature', units: 'K',
|
||||
description: 'Total (stagnation) temperature, equal to chamber temperature',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'temperature',
|
||||
},
|
||||
p_static: {
|
||||
id: 'p_static', symbol: 'p', name: 'Static Pressure', units: 'Pa',
|
||||
description: 'Local static pressure at a cross-section',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'pressure',
|
||||
},
|
||||
rho: {
|
||||
id: 'rho', symbol: 'ρ', name: 'Density', units: 'kg/m³',
|
||||
description: 'Local gas density at a cross-section',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'density',
|
||||
},
|
||||
rho0: {
|
||||
id: 'rho0', symbol: 'ρ₀', name: 'Stagnation Density', units: 'kg/m³',
|
||||
description: 'Total (stagnation) density',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'density',
|
||||
},
|
||||
a_sound: {
|
||||
id: 'a_sound', symbol: 'a', name: 'Speed of Sound', units: 'm/s',
|
||||
description: 'Local speed of sound: a = √(γRT)',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
v_flow: {
|
||||
id: 'v_flow', symbol: 'v', name: 'Flow Velocity', units: 'm/s',
|
||||
description: 'Local bulk flow velocity: v = M·a',
|
||||
category: 'Isentropic Flow',
|
||||
unitFamily: 'velocity',
|
||||
},
|
||||
|
||||
// ── Nozzle Geometry ───────────────────────────────────────────────────
|
||||
Me: {
|
||||
id: 'Me', symbol: 'Mₑ', name: 'Exit Mach Number', units: '—',
|
||||
description: 'Mach number at the nozzle exit plane',
|
||||
category: 'Nozzle Geometry',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
eps: {
|
||||
id: 'eps', symbol: 'ε', name: 'Expansion Ratio', units: '—',
|
||||
description: 'Nozzle area expansion ratio: ε = Aₑ/Aₜ',
|
||||
category: 'Nozzle Geometry',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
Te: {
|
||||
id: 'Te', symbol: 'Tₑ', name: 'Exit Temperature', units: 'K',
|
||||
description: 'Static temperature at the nozzle exit plane',
|
||||
category: 'Nozzle Geometry',
|
||||
unitFamily: 'temperature',
|
||||
},
|
||||
|
||||
// ── Performance Metrics ───────────────────────────────────────────────
|
||||
TWR: {
|
||||
id: 'TWR', symbol: 'TWR', name: 'Thrust-to-Weight Ratio', units: '—',
|
||||
description: 'Ratio of thrust to vehicle weight at ignition',
|
||||
category: 'Performance',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
m_vehicle: {
|
||||
id: 'm_vehicle', symbol: 'mᵥ', name: 'Vehicle Mass', units: 'kg',
|
||||
description: 'Vehicle mass used for TWR calculation',
|
||||
category: 'Performance',
|
||||
unitFamily: 'mass',
|
||||
},
|
||||
J: {
|
||||
id: 'J', symbol: 'J', name: 'Total Impulse', units: 'N·s',
|
||||
description: 'Integral of thrust over burn time: J = F·tᵦ (constant thrust)',
|
||||
category: 'Performance',
|
||||
unitFamily: 'impulse',
|
||||
},
|
||||
|
||||
// ── Mass & Propellant ─────────────────────────────────────────────────
|
||||
OF: {
|
||||
id: 'OF', symbol: 'O/F', name: 'Oxidiser/Fuel Ratio', units: '—',
|
||||
description: 'Mass ratio of oxidiser to fuel flow rates',
|
||||
category: 'Propellant',
|
||||
unitFamily: 'dimensionless',
|
||||
},
|
||||
mdot_ox: {
|
||||
id: 'mdot_ox', symbol: 'ṁₒₓ', name: 'Oxidiser Flow Rate', units: 'kg/s',
|
||||
description: 'Mass flow rate of oxidiser',
|
||||
category: 'Propellant',
|
||||
unitFamily: 'massflow',
|
||||
},
|
||||
mdot_f: {
|
||||
id: 'mdot_f', symbol: 'ṁf', name: 'Fuel Flow Rate', units: 'kg/s',
|
||||
description: 'Mass flow rate of fuel',
|
||||
category: 'Propellant',
|
||||
unitFamily: 'massflow',
|
||||
},
|
||||
}
|
||||
|
||||
// All categories in desired display order
|
||||
export const CATEGORIES = [
|
||||
'Thrust',
|
||||
'Specific Impulse',
|
||||
'Nozzle Performance',
|
||||
'Rocket Equation',
|
||||
'Isentropic Flow',
|
||||
'Nozzle Geometry',
|
||||
'Performance',
|
||||
'Propellant',
|
||||
]
|
||||
Reference in New Issue
Block a user