Ablative config
This commit is contained in:
@@ -9,9 +9,16 @@ command -v node >/dev/null || { echo "Error: node not found"; exit 1; }
|
|||||||
command -v npm >/dev/null || { echo "Error: npm not found"; exit 1; }
|
command -v npm >/dev/null || { echo "Error: npm not found"; exit 1; }
|
||||||
command -v sudo >/dev/null || { echo "Error: sudo not found"; exit 1; }
|
command -v sudo >/dev/null || { echo "Error: sudo not found"; exit 1; }
|
||||||
|
|
||||||
|
# Detect node path and current user
|
||||||
|
NODE_PATH=$(which node)
|
||||||
|
CURRENT_USER=$(whoami)
|
||||||
|
|
||||||
echo "Rocketry Service Installer"
|
echo "Rocketry Service Installer"
|
||||||
echo "=========================="
|
echo "=========================="
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Node path: $NODE_PATH"
|
||||||
|
echo "Service user: $CURRENT_USER"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Port prompt
|
# Port prompt
|
||||||
read -p "Port to run on [default: 8080]: " PORT
|
read -p "Port to run on [default: 8080]: " PORT
|
||||||
@@ -165,9 +172,9 @@ After=network.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=nobody
|
User=$CURRENT_USER
|
||||||
WorkingDirectory=/opt/rocketry
|
WorkingDirectory=/opt/rocketry
|
||||||
ExecStart=/usr/bin/node /opt/rocketry/server.js
|
ExecStart=$NODE_PATH /opt/rocketry/server.js
|
||||||
Environment="PORT=$PORT"
|
Environment="PORT=$PORT"
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|||||||
14
src/App.jsx
14
src/App.jsx
@@ -5,6 +5,7 @@ import EnginePage from './pages/EnginePage.jsx'
|
|||||||
import RocketPage from './pages/RocketPage.jsx'
|
import RocketPage from './pages/RocketPage.jsx'
|
||||||
import KnowledgebaseFuelsPage from './pages/KnowledgebaseFuelsPage.jsx'
|
import KnowledgebaseFuelsPage from './pages/KnowledgebaseFuelsPage.jsx'
|
||||||
import KnowledgebaseEquationsPage from './pages/KnowledgebaseEquationsPage.jsx'
|
import KnowledgebaseEquationsPage from './pages/KnowledgebaseEquationsPage.jsx'
|
||||||
|
import KnowledgebaseAblativesPage from './pages/KnowledgebaseAblativesPage.jsx'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -90,6 +91,18 @@ export default function App() {
|
|||||||
>
|
>
|
||||||
Fuels / Oxidisers
|
Fuels / Oxidisers
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/knowledgebase/ablatives"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`block px-4 py-2 text-sm transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-slate-700 text-white'
|
||||||
|
: 'text-slate-300 hover:bg-slate-700 hover:text-white'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Ablative Materials
|
||||||
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/knowledgebase/equations"
|
to="/knowledgebase/equations"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
@@ -113,6 +126,7 @@ export default function App() {
|
|||||||
<Route path="/design/rocket" element={<RocketPage />} />
|
<Route path="/design/rocket" element={<RocketPage />} />
|
||||||
<Route path="/knowledgebase/fuels" element={<KnowledgebaseFuelsPage />} />
|
<Route path="/knowledgebase/fuels" element={<KnowledgebaseFuelsPage />} />
|
||||||
<Route path="/knowledgebase/equations" element={<KnowledgebaseEquationsPage />} />
|
<Route path="/knowledgebase/equations" element={<KnowledgebaseEquationsPage />} />
|
||||||
|
<Route path="/knowledgebase/ablatives" element={<KnowledgebaseAblativesPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function PropellantCard({ propellant, onApply }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PropellantModal({ onClose, onApply, existingVarIds, description }) {
|
export function PropellantModal({ onClose, onApply, description }) {
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [activeType, setActiveType] = useState('All')
|
const [activeType, setActiveType] = useState('All')
|
||||||
|
|
||||||
@@ -166,7 +166,6 @@ export function PropellantModal({ onClose, onApply, existingVarIds, description
|
|||||||
key={p.id}
|
key={p.id}
|
||||||
propellant={p}
|
propellant={p}
|
||||||
onApply={handleApply}
|
onApply={handleApply}
|
||||||
existingVarIds={existingVarIds}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useRef } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { useDraggable } from '@dnd-kit/core'
|
import { useDraggable } from '@dnd-kit/core'
|
||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { VARIABLES } from '../engine/variables.js'
|
import { VARIABLES } from '../engine/variables.js'
|
||||||
import { getUnitsForFamily } from '../engine/units.js'
|
import { getUnitsForFamily } from '../engine/units.js'
|
||||||
import { formatValue } from '../engine/format.js'
|
import { formatValue } from '../engine/format.js'
|
||||||
@@ -49,8 +51,26 @@ export function WorkspaceCard({
|
|||||||
getUnit,
|
getUnit,
|
||||||
onUnitChange,
|
onUnitChange,
|
||||||
sciNotation,
|
sciNotation,
|
||||||
|
areaRatioBranch,
|
||||||
|
onToggleBranch,
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
|
const [inputError, setInputError] = useState(false)
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: varId })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}
|
||||||
|
|
||||||
const v = VARIABLES[varId]
|
const v = VARIABLES[varId]
|
||||||
if (!v) return null
|
if (!v) return null
|
||||||
|
|
||||||
@@ -60,10 +80,28 @@ export function WorkspaceCard({
|
|||||||
const isUserSet = userValue !== undefined && userValue !== ''
|
const isUserSet = userValue !== undefined && userValue !== ''
|
||||||
const isSolved = !!solvedInfo
|
const isSolved = !!solvedInfo
|
||||||
|
|
||||||
// State: user-entered (blue), solved (green), unknown (grey)
|
const handleInputChange = (rawDisplayValue) => {
|
||||||
const borderColor = isUserSet ? 'border-blue-500' : isSolved ? 'border-green-500' : 'border-slate-600'
|
if (rawDisplayValue === '') {
|
||||||
const symbolColor = isUserSet ? 'text-blue-300' : isSolved ? 'text-green-300' : 'text-slate-400'
|
setInputError(false)
|
||||||
const valueBg = isUserSet ? 'bg-blue-950' : isSolved ? 'bg-green-950' : 'bg-slate-800'
|
onValueChange(rawDisplayValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parsed = parseFloat(rawDisplayValue)
|
||||||
|
if (isNaN(parsed)) return
|
||||||
|
|
||||||
|
// Validate positive constraint
|
||||||
|
if (v.positive && parsed < 0) {
|
||||||
|
setInputError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setInputError(false)
|
||||||
|
onValueChange(rawDisplayValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State: user-entered (blue), solved (green), unknown (grey), error (red)
|
||||||
|
const borderColor = inputError ? 'border-red-500' : isUserSet ? 'border-blue-500' : isSolved ? 'border-green-500' : 'border-slate-600'
|
||||||
|
const symbolColor = inputError ? 'text-red-300' : isUserSet ? 'text-blue-300' : isSolved ? 'text-green-300' : 'text-slate-400'
|
||||||
|
const valueBg = inputError ? 'bg-red-950' : isUserSet ? 'bg-blue-950' : isSolved ? 'bg-green-950' : 'bg-slate-800'
|
||||||
|
|
||||||
// Display value converted from stored SI to current display unit
|
// Display value converted from stored SI to current display unit
|
||||||
const inputDisplayValue = isUserSet
|
const inputDisplayValue = isUserSet
|
||||||
@@ -74,7 +112,13 @@ export function WorkspaceCard({
|
|||||||
const isNonSI = unit.id !== siUnit.id
|
const isNonSI = unit.id !== siUnit.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group`}>
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={`rounded-xl border-2 ${borderColor} bg-slate-800 p-3 flex flex-col gap-1 relative group ${isDragging ? 'opacity-50' : ''}`}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-slate-700 text-slate-400 hover:bg-red-700 hover:text-white opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center leading-none"
|
className="absolute top-1.5 right-1.5 w-5 h-5 rounded-full bg-slate-700 text-slate-400 hover:bg-red-700 hover:text-white opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center leading-none"
|
||||||
@@ -101,6 +145,15 @@ export function WorkspaceCard({
|
|||||||
) : (
|
) : (
|
||||||
<span className="text-slate-400 text-xs">{unit.label}</span>
|
<span className="text-slate-400 text-xs">{unit.label}</span>
|
||||||
)}
|
)}
|
||||||
|
{isSolved && varId === 'Me' && solvedInfo?.equationName === 'Isentropic Area Ratio' && onToggleBranch && (
|
||||||
|
<button
|
||||||
|
onClick={onToggleBranch}
|
||||||
|
title={`Switch to ${areaRatioBranch === 'supersonic' ? 'subsonic' : 'supersonic'}`}
|
||||||
|
className="ml-auto text-xs px-1.5 py-0.5 rounded border transition-colors bg-amber-900/40 text-amber-300 border-amber-600 hover:border-amber-400 hover:bg-amber-900/60"
|
||||||
|
>
|
||||||
|
{areaRatioBranch === 'supersonic' ? 'SS' : 'Sub'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-300 text-xs truncate">{v.name}</div>
|
<div className="text-slate-300 text-xs truncate">{v.name}</div>
|
||||||
<div className={`mt-1 rounded-md ${valueBg} px-2 py-1`}>
|
<div className={`mt-1 rounded-md ${valueBg} px-2 py-1`}>
|
||||||
@@ -122,11 +175,14 @@ export function WorkspaceCard({
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="number"
|
type="number"
|
||||||
value={inputDisplayValue}
|
value={inputDisplayValue}
|
||||||
onChange={e => onValueChange(e.target.value)}
|
onChange={e => handleInputChange(e.target.value)}
|
||||||
placeholder="Enter value…"
|
placeholder="Enter value…"
|
||||||
className="w-full bg-transparent font-mono text-sm text-blue-200 placeholder-slate-600 outline-none"
|
className={`w-full bg-transparent font-mono text-sm ${inputError ? 'text-red-200' : 'text-blue-200'} placeholder-slate-600 outline-none`}
|
||||||
/>
|
/>
|
||||||
{isUserSet && isNonSI && (
|
{inputError && (
|
||||||
|
<p className="text-xs text-red-400 mt-0.5">Must be positive</p>
|
||||||
|
)}
|
||||||
|
{isUserSet && isNonSI && !inputError && (
|
||||||
<div className="text-blue-800 text-[10px] mt-0.5 font-mono">
|
<div className="text-blue-800 text-[10px] mt-0.5 font-mono">
|
||||||
= {formatValue(userValue, sciNotation)} {siUnit.label}
|
= {formatValue(userValue, sciNotation)} {siUnit.label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useDroppable } from '@dnd-kit/core'
|
import { useDroppable } from '@dnd-kit/core'
|
||||||
|
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'
|
||||||
import { EQUATION_PRESETS } from '../engine/equations.js'
|
import { EQUATION_PRESETS } from '../engine/equations.js'
|
||||||
import { WorkspaceCard } from './VariableCard.jsx'
|
import { WorkspaceCard } from './VariableCard.jsx'
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ export function Workspace({
|
|||||||
getUnit,
|
getUnit,
|
||||||
setUnit,
|
setUnit,
|
||||||
sciNotation,
|
sciNotation,
|
||||||
|
areaRatioBranch,
|
||||||
|
onToggleAreaRatioBranch,
|
||||||
}) {
|
}) {
|
||||||
const { setNodeRef, isOver } = useDroppable({ id: 'workspace' })
|
const { setNodeRef, isOver } = useDroppable({ id: 'workspace' })
|
||||||
|
|
||||||
@@ -57,6 +60,7 @@ export function Workspace({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<SortableContext items={workspaceVarIds} strategy={rectSortingStrategy}>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 content-start">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 content-start">
|
||||||
{workspaceVarIds.map(varId => (
|
{workspaceVarIds.map(varId => (
|
||||||
<WorkspaceCard
|
<WorkspaceCard
|
||||||
@@ -69,6 +73,8 @@ export function Workspace({
|
|||||||
getUnit={getUnit}
|
getUnit={getUnit}
|
||||||
onUnitChange={unitId => setUnit(varId, unitId)}
|
onUnitChange={unitId => setUnit(varId, unitId)}
|
||||||
sciNotation={sciNotation}
|
sciNotation={sciNotation}
|
||||||
|
areaRatioBranch={areaRatioBranch}
|
||||||
|
onToggleBranch={onToggleAreaRatioBranch}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/* Drop indicator card when dragging over a non-empty workspace */}
|
{/* Drop indicator card when dragging over a non-empty workspace */}
|
||||||
@@ -78,6 +84,7 @@ export function Workspace({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</SortableContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,18 +3,25 @@ import { useState } from 'react'
|
|||||||
/**
|
/**
|
||||||
* Collapsible section wrapper for engine design input groups.
|
* Collapsible section wrapper for engine design input groups.
|
||||||
*/
|
*/
|
||||||
export default function DesignSection({ title, children, defaultOpen = true }) {
|
export default function DesignSection({ title, children, defaultOpen = true, headerAction }) {
|
||||||
const [open, setOpen] = useState(defaultOpen)
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-slate-700 rounded-lg overflow-hidden mb-4">
|
<div className="border border-slate-700 rounded-lg overflow-hidden mb-4">
|
||||||
|
<div className="w-full flex items-center justify-between px-4 py-3 bg-slate-800 hover:bg-slate-700 text-left transition-colors group">
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpen(o => !o)}
|
onClick={() => setOpen(o => !o)}
|
||||||
className="w-full flex items-center justify-between px-4 py-3 bg-slate-800 hover:bg-slate-700 text-left transition-colors"
|
className="flex items-center gap-2 flex-1"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-slate-200">{title}</span>
|
<span className="text-sm font-semibold text-slate-200">{title}</span>
|
||||||
<span className="text-slate-400 text-xs select-none">{open ? '▲' : '▼'}</span>
|
<span className="text-slate-400 text-xs select-none">{open ? '▲' : '▼'}</span>
|
||||||
</button>
|
</button>
|
||||||
|
{headerAction && (
|
||||||
|
<div className="ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{headerAction}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="p-4 bg-slate-900 space-y-3">
|
<div className="p-4 bg-slate-900 space-y-3">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Pure calculation functions for engine design (no React)
|
// Pure calculation functions for engine design (no React)
|
||||||
|
import { ABLATIVE_MATERIALS } from './knowledgebaseData.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combustion chamber geometry from thermodynamic results and design inputs.
|
* Combustion chamber geometry from thermodynamic results and design inputs.
|
||||||
@@ -85,9 +86,10 @@ export function calcInjector(thermo, injector) {
|
|||||||
* Cooling analysis based on selected method.
|
* Cooling analysis based on selected method.
|
||||||
* For regenerative cooling: simplified Bartz heat flux estimate.
|
* For regenerative cooling: simplified Bartz heat flux estimate.
|
||||||
* For film cooling: propellant fraction and Isp penalty estimate.
|
* For film cooling: propellant fraction and Isp penalty estimate.
|
||||||
* For ablative/uncooled: informational only.
|
* For ablative: material selection, erosion, and remaining thickness calculation.
|
||||||
|
* For uncooled: informational only.
|
||||||
*/
|
*/
|
||||||
export function calcCooling(thermo, cooling, chamberGeom) {
|
export function calcCooling(thermo, cooling, chamberGeom, burnTime) {
|
||||||
const { p0, T0, cstar, mdot } = thermo
|
const { p0, T0, cstar, mdot } = thermo
|
||||||
const { method } = cooling
|
const { method } = cooling
|
||||||
|
|
||||||
@@ -120,8 +122,55 @@ export function calcCooling(thermo, cooling, chamberGeom) {
|
|||||||
return { method, filmFraction, mdot_film, ispPenalty }
|
return { method, filmFraction, mdot_film, ispPenalty }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'ablative') {
|
||||||
|
const { ablativeMaterial = 'carbon_phenolic', ablativeThickness = 10 } = cooling
|
||||||
|
const material = ABLATIVE_MATERIALS.find(m => m.id === ablativeMaterial)
|
||||||
|
if (!material || !burnTime) return { method, material }
|
||||||
|
|
||||||
|
// Pressure correction (if p0 available)
|
||||||
|
const pressureFactor = (p0 && isFinite(p0) && material.refPressure)
|
||||||
|
? Math.pow(p0 / material.refPressure, material.pressureExponent ?? 0.4)
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const effectiveRate = material.erosionRate * pressureFactor
|
||||||
|
const effectiveRateMin = material.erosionRateRange[0] * pressureFactor
|
||||||
|
const effectiveRateMax = material.erosionRateRange[1] * pressureFactor
|
||||||
|
|
||||||
|
const erosionMm = effectiveRate * burnTime
|
||||||
|
const erosionMmMin = effectiveRateMin * burnTime
|
||||||
|
const erosionMmMax = effectiveRateMax * burnTime
|
||||||
|
|
||||||
|
const remainingMm = ablativeThickness - erosionMm
|
||||||
|
const remainingMmBest = ablativeThickness - erosionMmMin // best case
|
||||||
|
const remainingMmWorst = ablativeThickness - erosionMmMax // worst case
|
||||||
|
|
||||||
|
const fraction = remainingMm / ablativeThickness // 0–1
|
||||||
|
|
||||||
|
// Warning thresholds (based on worst case)
|
||||||
|
const WARNING_MM = 2 // warn below 2 mm
|
||||||
|
const CRITICAL_MM = 0.5 // critical below 0.5 mm
|
||||||
|
|
||||||
|
return {
|
||||||
|
method,
|
||||||
|
material,
|
||||||
|
ablativeThickness,
|
||||||
|
pressureFactor,
|
||||||
|
effectiveRate,
|
||||||
|
erosionMm,
|
||||||
|
erosionMmMin,
|
||||||
|
erosionMmMax,
|
||||||
|
remainingMm,
|
||||||
|
remainingMmBest,
|
||||||
|
remainingMmWorst,
|
||||||
|
fraction,
|
||||||
|
status: remainingMmWorst < CRITICAL_MM ? 'critical'
|
||||||
|
: remainingMmWorst < WARNING_MM ? 'warning'
|
||||||
|
: 'ok',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const notes = {
|
const notes = {
|
||||||
ablative: 'Ablative liner — consult manufacturer data for material thickness and char rate.',
|
ablative: 'Ablative liner — configure material and initial thickness in inputs.',
|
||||||
uncooled: 'Uncooled — confirm combustion gas temperature is within material thermal limits for the burn duration.',
|
uncooled: 'Uncooled — confirm combustion gas temperature is within material thermal limits for the burn duration.',
|
||||||
}
|
}
|
||||||
return { method, note: notes[method] ?? '' }
|
return { method, note: notes[method] ?? '' }
|
||||||
|
|||||||
@@ -345,14 +345,15 @@ export const EQUATIONS = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
id: 'area_ratio_mach',
|
id: 'area_ratio_mach',
|
||||||
name: 'Isentropic Area Ratio (supersonic)',
|
name: 'Isentropic Area Ratio',
|
||||||
formula: 'ε = (1/Mₑ)·[(2/(γ+1))·(1+(γ−1)/2·Mₑ²)]^((γ+1)/(2(γ−1)))',
|
formula: 'ε = (1/Mₑ)·[(2/(γ+1))·(1+(γ−1)/2·Mₑ²)]^((γ+1)/(2(γ−1)))',
|
||||||
category: 'Nozzle Geometry',
|
category: 'Nozzle Geometry',
|
||||||
description: 'Relates exit area ratio to exit Mach number for isentropic supersonic flow. Critical for nozzle design.',
|
description: 'Relates exit area ratio to exit Mach number for isentropic flow. Choose supersonic or subsonic branch.',
|
||||||
variables: ['eps', 'Me', 'gamma'],
|
variables: ['eps', 'Me', 'gamma'],
|
||||||
|
hasBranch: true,
|
||||||
solvers: {
|
solvers: {
|
||||||
eps: v => areaRatioFromMach(v.Me, v.gamma),
|
eps: v => areaRatioFromMach(v.Me, v.gamma),
|
||||||
Me: v => machFromAreaRatio(v.eps, v.gamma, true),
|
Me: (v, opts) => machFromAreaRatio(v.eps, v.gamma, opts?.supersonic ?? true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -581,19 +582,6 @@ export const EQUATIONS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Subsonic Area Ratio ───────────────────────────────────────────────
|
|
||||||
{
|
|
||||||
id: 'area_ratio_mach_subsonic',
|
|
||||||
name: 'Isentropic Area Ratio (subsonic)',
|
|
||||||
formula: 'ε = (1/Mₑ)·[(2/(γ+1))·(1+(γ−1)/2·Mₑ²)]^((γ+1)/(2(γ−1)))',
|
|
||||||
category: 'Nozzle Geometry',
|
|
||||||
description: 'Isentropic area ratio as a function of Mach number (subsonic branch). Used for intake and subsonic flow analysis.',
|
|
||||||
variables: ['eps', 'Me', 'gamma'],
|
|
||||||
solvers: {
|
|
||||||
eps: v => areaRatioFromMach(v.Me, v.gamma),
|
|
||||||
Me: v => machFromAreaRatio(v.eps, v.gamma, false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// Equation presets: named groups that seed the workspace with a useful set of variables
|
// Equation presets: named groups that seed the workspace with a useful set of variables
|
||||||
|
|||||||
@@ -752,3 +752,127 @@ export const PROPELLANTS = [
|
|||||||
notes: 'Catalytic decomposition; vacuum Isp',
|
notes: 'Catalytic decomposition; vacuum Isp',
|
||||||
})),
|
})),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ── Ablative liner materials ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const ABLATIVE_MATERIALS = [
|
||||||
|
{
|
||||||
|
id: 'carbon_phenolic',
|
||||||
|
name: 'Carbon-Phenolic',
|
||||||
|
composition: 'Carbon fiber reinforced phenolic resin',
|
||||||
|
description:
|
||||||
|
'Carbon-phenolic is a high-performance ablative material widely used in nozzle throats and combustion chamber walls. It combines excellent thermal stability with good mechanical properties and has proven heritage in operational rocket engines. The phenolic matrix char-yields ~55%, providing a protective char layer that insulates the underlying structure.',
|
||||||
|
erosionRate: 0.15,
|
||||||
|
erosionRateRange: [0.05, 0.30],
|
||||||
|
maxTemp: 3500,
|
||||||
|
density: 1400,
|
||||||
|
thermalConductivity: 3.5,
|
||||||
|
charYield: 0.55,
|
||||||
|
pressureExponent: 0.35,
|
||||||
|
applications: ['nozzle throat', 'combustion chamber wall'],
|
||||||
|
notes: 'Proven material for high-pressure engines (6.9 MPa reference). Good ablation resistance; expensive relative to alternatives.',
|
||||||
|
refPressure: 6.9e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'silica_phenolic',
|
||||||
|
name: 'Silica-Phenolic',
|
||||||
|
composition: 'Silica-filled phenolic resin',
|
||||||
|
description:
|
||||||
|
'Silica-phenolic offers moderate performance with good thermal properties and lower cost than carbon-phenolic. The silica filler improves thermal conductivity and provides additional structural support. Suitable for moderate-temperature applications and less demanding environments.',
|
||||||
|
erosionRate: 0.25,
|
||||||
|
erosionRateRange: [0.15, 0.40],
|
||||||
|
maxTemp: 3200,
|
||||||
|
density: 1350,
|
||||||
|
thermalConductivity: 2.5,
|
||||||
|
charYield: 0.50,
|
||||||
|
pressureExponent: 0.38,
|
||||||
|
applications: ['nozzle throat', 'chamber sections'],
|
||||||
|
notes: 'Lower performance than carbon-phenolic but more cost-effective. Good for educational and amateur applications.',
|
||||||
|
refPressure: 6.9e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'graphite_phenolic',
|
||||||
|
name: 'Graphite-Phenolic',
|
||||||
|
composition: 'Graphite-filled phenolic resin',
|
||||||
|
description:
|
||||||
|
'Graphite-phenolic combines the strength of phenolic matrix with excellent thermal conductivity from graphite fillers. It provides superior performance in high heat flux regions and is ideal for engines with extreme thermal conditions. Excellent char stability and low ablation rates.',
|
||||||
|
erosionRate: 0.10,
|
||||||
|
erosionRateRange: [0.05, 0.20],
|
||||||
|
maxTemp: 3600,
|
||||||
|
density: 1450,
|
||||||
|
thermalConductivity: 8.0,
|
||||||
|
charYield: 0.58,
|
||||||
|
pressureExponent: 0.33,
|
||||||
|
applications: ['nozzle throat (high heat flux)', 'combustion chamber'],
|
||||||
|
notes: 'Premium material for highest-performance applications. Superior thermal properties justify higher cost.',
|
||||||
|
refPressure: 6.9e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'epdm',
|
||||||
|
name: 'EPDM Rubber',
|
||||||
|
composition: 'Ethylene propylene diene monomer polymer',
|
||||||
|
description:
|
||||||
|
'EPDM is a flexible elastomeric ablator suitable for low-pressure engines and non-critical applications. It provides good insulation properties and flexibility, reducing structural loads. Inexpensive and easy to manufacture in various geometries.',
|
||||||
|
erosionRate: 0.50,
|
||||||
|
erosionRateRange: [0.30, 0.80],
|
||||||
|
maxTemp: 2200,
|
||||||
|
density: 920,
|
||||||
|
thermalConductivity: 0.25,
|
||||||
|
charYield: 0.40,
|
||||||
|
pressureExponent: 0.50,
|
||||||
|
applications: ['low-pressure chamber', 'insulation layer'],
|
||||||
|
notes: 'Low cost and simple manufacturing. Limited to low-pressure (<1 MPa) applications due to high erosion rates.',
|
||||||
|
refPressure: 0.5e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cork_composite',
|
||||||
|
name: 'Cork Composite',
|
||||||
|
composition: 'Cork particles in polymer binder',
|
||||||
|
description:
|
||||||
|
'Cork-based composites are the lowest-cost ablative option, popular in amateur rocketry. Cork provides excellent insulation and low density. Performance is modest but adequate for low-pressure, short-duration burns.',
|
||||||
|
erosionRate: 0.90,
|
||||||
|
erosionRateRange: [0.50, 1.50],
|
||||||
|
maxTemp: 1800,
|
||||||
|
density: 700,
|
||||||
|
thermalConductivity: 0.15,
|
||||||
|
charYield: 0.35,
|
||||||
|
pressureExponent: 0.52,
|
||||||
|
applications: ['amateur engines', 'low-pressure chambers'],
|
||||||
|
notes: 'Extremely low cost and accessibility. Suitable only for educational and amateur applications with short burn times.',
|
||||||
|
refPressure: 0.3e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'silicone_rubber',
|
||||||
|
name: 'Silicone Rubber',
|
||||||
|
composition: 'Siloxane polymer',
|
||||||
|
description:
|
||||||
|
'Silicone rubber provides flexibility and excellent low-pressure performance with good char stability. It remains serviceable over a wide temperature range and is less brittle than phenolics. Ideal for flexible throat liners and low-pressure applications.',
|
||||||
|
erosionRate: 0.65,
|
||||||
|
erosionRateRange: [0.40, 1.00],
|
||||||
|
maxTemp: 2400,
|
||||||
|
density: 1100,
|
||||||
|
thermalConductivity: 0.20,
|
||||||
|
charYield: 0.45,
|
||||||
|
pressureExponent: 0.48,
|
||||||
|
applications: ['flexible throat', 'low-pressure chamber'],
|
||||||
|
notes: 'Good flexibility reduces structural stress. Higher erosion rates limit use to low-moderate pressure engines.',
|
||||||
|
refPressure: 0.5e6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pica',
|
||||||
|
name: 'PICA',
|
||||||
|
composition: 'Phenolic Impregnated Carbon Ablator',
|
||||||
|
description:
|
||||||
|
'PICA (Phenolic Impregnated Carbon Ablator) is an ultra-high-performance ablative material with exceptionally low erosion rates. Developed for spacecraft thermal protection and advanced rocket engines, it combines carbon fibre structure with phenolic impregnation. Extremely expensive but unsurpassed in performance.',
|
||||||
|
erosionRate: 0.06,
|
||||||
|
erosionRateRange: [0.03, 0.12],
|
||||||
|
maxTemp: 3800,
|
||||||
|
density: 1500,
|
||||||
|
thermalConductivity: 5.5,
|
||||||
|
charYield: 0.60,
|
||||||
|
pressureExponent: 0.30,
|
||||||
|
applications: ['nozzle throat (ultra-high performance)', 'chamber (extreme conditions)'],
|
||||||
|
notes: 'Premium aerospace-grade material with proven heritage in advanced engines. Cost is very high; used only when performance demands justify expense.',
|
||||||
|
refPressure: 6.9e6,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { VARIABLES } from './variables.js'
|
|||||||
* Run the constraint-propagation solver.
|
* Run the constraint-propagation solver.
|
||||||
*
|
*
|
||||||
* @param {Record<string, number>} knownValues – user-entered { varId: value }
|
* @param {Record<string, number>} knownValues – user-entered { varId: value }
|
||||||
|
* @param {Record<string, any>} opts – equation-specific options: { 'area_ratio_mach': { supersonic: true/false } }
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* solved: Record<string, { value: number, equationId: string, equationName: string }>,
|
* solved: Record<string, { value: number, equationId: string, equationName: string }>,
|
||||||
* missing: Record<string, string[][]> – varId → list of variable-sets that would unlock it
|
* missing: Record<string, string[][]> – varId → list of variable-sets that would unlock it
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function solve(knownValues) {
|
export function solve(knownValues, opts = {}) {
|
||||||
const known = { ...knownValues }
|
const known = { ...knownValues }
|
||||||
const solved = {}
|
const solved = {}
|
||||||
|
|
||||||
@@ -28,7 +29,8 @@ export function solve(knownValues) {
|
|||||||
: eq.variables.filter(v => v !== target)
|
: eq.variables.filter(v => v !== target)
|
||||||
if (others.every(v => v in known)) {
|
if (others.every(v => v in known)) {
|
||||||
try {
|
try {
|
||||||
const val = solver(known)
|
const eqOpts = opts[eq.id] ?? {}
|
||||||
|
const val = solver(known, eqOpts)
|
||||||
if (isFinite(val) && !isNaN(val)) {
|
if (isFinite(val) && !isNaN(val)) {
|
||||||
known[target] = val
|
known[target] = val
|
||||||
solved[target] = { value: val, equationId: eq.id, equationName: eq.name }
|
solved[target] = { value: val, equationId: eq.id, equationName: eq.name }
|
||||||
|
|||||||
@@ -14,42 +14,49 @@ export const VARIABLES = {
|
|||||||
description: 'Net thrust force produced by the engine',
|
description: 'Net thrust force produced by the engine',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'force',
|
unitFamily: 'force',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
mdot: {
|
mdot: {
|
||||||
id: 'mdot', symbol: 'ṁ', name: 'Mass Flow Rate', units: 'kg/s',
|
id: 'mdot', symbol: 'ṁ', name: 'Mass Flow Rate', units: 'kg/s',
|
||||||
description: 'Total propellant mass flow rate through the engine',
|
description: 'Total propellant mass flow rate through the engine',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'massflow',
|
unitFamily: 'massflow',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
Ve: {
|
Ve: {
|
||||||
id: 'Ve', symbol: 'Vₑ', name: 'Exhaust Velocity', units: 'm/s',
|
id: 'Ve', symbol: 'Vₑ', name: 'Exhaust Velocity', units: 'm/s',
|
||||||
description: 'Actual kinematic exit velocity at the nozzle exit plane',
|
description: 'Actual kinematic exit velocity at the nozzle exit plane',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'velocity',
|
unitFamily: 'velocity',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
ceff: {
|
ceff: {
|
||||||
id: 'ceff', symbol: 'cₑ', name: 'Effective Exhaust Velocity', units: 'm/s',
|
id: 'ceff', symbol: 'cₑ', name: 'Effective Exhaust Velocity', units: 'm/s',
|
||||||
description: 'F/ṁ = Isp·g₀; includes both momentum and pressure thrust',
|
description: 'F/ṁ = Isp·g₀; includes both momentum and pressure thrust',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'velocity',
|
unitFamily: 'velocity',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
pe: {
|
pe: {
|
||||||
id: 'pe', symbol: 'pₑ', name: 'Exit Pressure', units: 'Pa',
|
id: 'pe', symbol: 'pₑ', name: 'Exit Pressure', units: 'Pa',
|
||||||
description: 'Static pressure at the nozzle exit plane',
|
description: 'Static pressure at the nozzle exit plane',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'pressure',
|
unitFamily: 'pressure',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
pa: {
|
pa: {
|
||||||
id: 'pa', symbol: 'pₐ', name: 'Ambient Pressure', units: 'Pa',
|
id: 'pa', symbol: 'pₐ', name: 'Ambient Pressure', units: 'Pa',
|
||||||
description: 'Surrounding ambient static pressure',
|
description: 'Surrounding ambient static pressure',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'pressure',
|
unitFamily: 'pressure',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
Ae: {
|
Ae: {
|
||||||
id: 'Ae', symbol: 'Aₑ', name: 'Exit Area', units: 'm²',
|
id: 'Ae', symbol: 'Aₑ', name: 'Exit Area', units: 'm²',
|
||||||
description: 'Cross-sectional area of the nozzle exit',
|
description: 'Cross-sectional area of the nozzle exit',
|
||||||
category: 'Thrust',
|
category: 'Thrust',
|
||||||
unitFamily: 'area',
|
unitFamily: 'area',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Specific Impulse ──────────────────────────────────────────────────
|
// ── Specific Impulse ──────────────────────────────────────────────────
|
||||||
@@ -58,12 +65,14 @@ export const VARIABLES = {
|
|||||||
description: 'Thrust produced per unit weight flow of propellant',
|
description: 'Thrust produced per unit weight flow of propellant',
|
||||||
category: 'Specific Impulse',
|
category: 'Specific Impulse',
|
||||||
unitFamily: 'time',
|
unitFamily: 'time',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
g0: {
|
g0: {
|
||||||
id: 'g0', symbol: 'g₀', name: 'Standard Gravity', units: 'm/s²',
|
id: 'g0', symbol: 'g₀', name: 'Standard Gravity', units: 'm/s²',
|
||||||
description: 'Standard gravitational acceleration (9.80665 m/s²)',
|
description: 'Standard gravitational acceleration (9.80665 m/s²)',
|
||||||
category: 'Specific Impulse',
|
category: 'Specific Impulse',
|
||||||
unitFamily: 'acceleration',
|
unitFamily: 'acceleration',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Characteristic Velocity / Thrust Coefficient ──────────────────────
|
// ── Characteristic Velocity / Thrust Coefficient ──────────────────────
|
||||||
@@ -72,24 +81,28 @@ export const VARIABLES = {
|
|||||||
description: 'Measure of combustion efficiency; c* = p₀·Aₜ/ṁ',
|
description: 'Measure of combustion efficiency; c* = p₀·Aₜ/ṁ',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'velocity',
|
unitFamily: 'velocity',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
CF: {
|
CF: {
|
||||||
id: 'CF', symbol: 'Cꜰ', name: 'Thrust Coefficient', units: '—',
|
id: 'CF', symbol: 'Cꜰ', name: 'Thrust Coefficient', units: '—',
|
||||||
description: 'Dimensionless nozzle performance factor; Cꜰ = F/(p₀·Aₜ)',
|
description: 'Dimensionless nozzle performance factor; Cꜰ = F/(p₀·Aₜ)',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
p0: {
|
p0: {
|
||||||
id: 'p0', symbol: 'p₀', name: 'Chamber Pressure', units: 'Pa',
|
id: 'p0', symbol: 'p₀', name: 'Chamber Pressure', units: 'Pa',
|
||||||
description: 'Stagnation (total) pressure in the combustion chamber',
|
description: 'Stagnation (total) pressure in the combustion chamber',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'pressure',
|
unitFamily: 'pressure',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
At: {
|
At: {
|
||||||
id: 'At', symbol: 'Aₜ', name: 'Throat Area', units: 'm²',
|
id: 'At', symbol: 'Aₜ', name: 'Throat Area', units: 'm²',
|
||||||
description: 'Cross-sectional area at the nozzle throat',
|
description: 'Cross-sectional area at the nozzle throat',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'area',
|
unitFamily: 'area',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
Lstar: {
|
Lstar: {
|
||||||
@@ -97,6 +110,7 @@ export const VARIABLES = {
|
|||||||
description: 'Characteristic length of the combustion chamber: L* = Vᶜ/Aₜ. Higher values indicate larger chambers.',
|
description: 'Characteristic length of the combustion chamber: L* = Vᶜ/Aₜ. Higher values indicate larger chambers.',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'length',
|
unitFamily: 'length',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
Vc: {
|
Vc: {
|
||||||
@@ -104,6 +118,7 @@ export const VARIABLES = {
|
|||||||
description: 'Total volume of the combustion chamber',
|
description: 'Total volume of the combustion chamber',
|
||||||
category: 'Nozzle Performance',
|
category: 'Nozzle Performance',
|
||||||
unitFamily: 'volume',
|
unitFamily: 'volume',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Tsiolkovsky Rocket Equation ───────────────────────────────────────
|
// ── Tsiolkovsky Rocket Equation ───────────────────────────────────────
|
||||||
@@ -118,36 +133,42 @@ export const VARIABLES = {
|
|||||||
description: 'Total vehicle mass at ignition (wet mass)',
|
description: 'Total vehicle mass at ignition (wet mass)',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'mass',
|
unitFamily: 'mass',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
mf: {
|
mf: {
|
||||||
id: 'mf', symbol: 'mf', name: 'Final Mass', units: 'kg',
|
id: 'mf', symbol: 'mf', name: 'Final Mass', units: 'kg',
|
||||||
description: 'Vehicle mass after propellant is expended (dry mass)',
|
description: 'Vehicle mass after propellant is expended (dry mass)',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'mass',
|
unitFamily: 'mass',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
mp: {
|
mp: {
|
||||||
id: 'mp', symbol: 'mₚ', name: 'Propellant Mass', units: 'kg',
|
id: 'mp', symbol: 'mₚ', name: 'Propellant Mass', units: 'kg',
|
||||||
description: 'Mass of propellant consumed: mₚ = m₀ − mf',
|
description: 'Mass of propellant consumed: mₚ = m₀ − mf',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'mass',
|
unitFamily: 'mass',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
MR: {
|
MR: {
|
||||||
id: 'MR', symbol: 'MR', name: 'Mass Ratio', units: '—',
|
id: 'MR', symbol: 'MR', name: 'Mass Ratio', units: '—',
|
||||||
description: 'Ratio of initial to final mass: MR = m₀/mf',
|
description: 'Ratio of initial to final mass: MR = m₀/mf',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
zeta: {
|
zeta: {
|
||||||
id: 'zeta', symbol: 'ζ', name: 'Propellant Mass Fraction', units: '—',
|
id: 'zeta', symbol: 'ζ', name: 'Propellant Mass Fraction', units: '—',
|
||||||
description: 'Fraction of initial mass that is propellant: ζ = mₚ/m₀',
|
description: 'Fraction of initial mass that is propellant: ζ = mₚ/m₀',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
tb: {
|
tb: {
|
||||||
id: 'tb', symbol: 'tᵦ', name: 'Burn Time', units: 's',
|
id: 'tb', symbol: 'tᵦ', name: 'Burn Time', units: 's',
|
||||||
description: 'Duration of engine burn',
|
description: 'Duration of engine burn',
|
||||||
category: 'Rocket Equation',
|
category: 'Rocket Equation',
|
||||||
unitFamily: 'time',
|
unitFamily: 'time',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Isentropic Flow ───────────────────────────────────────────────────
|
// ── Isentropic Flow ───────────────────────────────────────────────────
|
||||||
@@ -156,18 +177,21 @@ export const VARIABLES = {
|
|||||||
description: 'Local Mach number at a cross-section',
|
description: 'Local Mach number at a cross-section',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
gamma: {
|
gamma: {
|
||||||
id: 'gamma', symbol: 'γ', name: 'Ratio of Specific Heats', units: '—',
|
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)',
|
description: 'Ratio of specific heats cₚ/cᵥ for the gas (≈1.4 for air, ~1.2 for rocket exhaust)',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
R: {
|
R: {
|
||||||
id: 'R', symbol: 'R', name: 'Specific Gas Constant', units: 'J/(kg·K)',
|
id: 'R', symbol: 'R', name: 'Specific Gas Constant', units: 'J/(kg·K)',
|
||||||
description: 'Gas constant for the propellant gas: R = R̄/M_mol',
|
description: 'Gas constant for the propellant gas: R = R̄/M_mol',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'spec_gas',
|
unitFamily: 'spec_gas',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
Mm: {
|
Mm: {
|
||||||
@@ -175,48 +199,56 @@ export const VARIABLES = {
|
|||||||
description: 'Molar mass of the exhaust gas mixture. Used to calculate specific gas constant: R = R̄/Mₘ',
|
description: 'Molar mass of the exhaust gas mixture. Used to calculate specific gas constant: R = R̄/Mₘ',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'molar_mass',
|
unitFamily: 'molar_mass',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
T: {
|
T: {
|
||||||
id: 'T', symbol: 'T', name: 'Static Temperature', units: 'K',
|
id: 'T', symbol: 'T', name: 'Static Temperature', units: 'K',
|
||||||
description: 'Local static temperature at a cross-section',
|
description: 'Local static temperature at a cross-section',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'temperature',
|
unitFamily: 'temperature',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
T0: {
|
T0: {
|
||||||
id: 'T0', symbol: 'T₀', name: 'Stagnation Temperature', units: 'K',
|
id: 'T0', symbol: 'T₀', name: 'Stagnation Temperature', units: 'K',
|
||||||
description: 'Total (stagnation) temperature, equal to chamber temperature',
|
description: 'Total (stagnation) temperature, equal to chamber temperature',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'temperature',
|
unitFamily: 'temperature',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
p_static: {
|
p_static: {
|
||||||
id: 'p_static', symbol: 'p', name: 'Static Pressure', units: 'Pa',
|
id: 'p_static', symbol: 'p', name: 'Static Pressure', units: 'Pa',
|
||||||
description: 'Local static pressure at a cross-section',
|
description: 'Local static pressure at a cross-section',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'pressure',
|
unitFamily: 'pressure',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
rho: {
|
rho: {
|
||||||
id: 'rho', symbol: 'ρ', name: 'Density', units: 'kg/m³',
|
id: 'rho', symbol: 'ρ', name: 'Density', units: 'kg/m³',
|
||||||
description: 'Local gas density at a cross-section',
|
description: 'Local gas density at a cross-section',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'density',
|
unitFamily: 'density',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
rho0: {
|
rho0: {
|
||||||
id: 'rho0', symbol: 'ρ₀', name: 'Stagnation Density', units: 'kg/m³',
|
id: 'rho0', symbol: 'ρ₀', name: 'Stagnation Density', units: 'kg/m³',
|
||||||
description: 'Total (stagnation) density',
|
description: 'Total (stagnation) density',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'density',
|
unitFamily: 'density',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
a_sound: {
|
a_sound: {
|
||||||
id: 'a_sound', symbol: 'a', name: 'Speed of Sound', units: 'm/s',
|
id: 'a_sound', symbol: 'a', name: 'Speed of Sound', units: 'm/s',
|
||||||
description: 'Local speed of sound: a = √(γRT)',
|
description: 'Local speed of sound: a = √(γRT)',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'velocity',
|
unitFamily: 'velocity',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
v_flow: {
|
v_flow: {
|
||||||
id: 'v_flow', symbol: 'v', name: 'Flow Velocity', units: 'm/s',
|
id: 'v_flow', symbol: 'v', name: 'Flow Velocity', units: 'm/s',
|
||||||
description: 'Local bulk flow velocity: v = M·a',
|
description: 'Local bulk flow velocity: v = M·a',
|
||||||
category: 'Isentropic Flow',
|
category: 'Isentropic Flow',
|
||||||
unitFamily: 'velocity',
|
unitFamily: 'velocity',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Nozzle Geometry ───────────────────────────────────────────────────
|
// ── Nozzle Geometry ───────────────────────────────────────────────────
|
||||||
@@ -225,18 +257,21 @@ export const VARIABLES = {
|
|||||||
description: 'Mach number at the nozzle exit plane',
|
description: 'Mach number at the nozzle exit plane',
|
||||||
category: 'Nozzle Geometry',
|
category: 'Nozzle Geometry',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
eps: {
|
eps: {
|
||||||
id: 'eps', symbol: 'ε', name: 'Expansion Ratio', units: '—',
|
id: 'eps', symbol: 'ε', name: 'Expansion Ratio', units: '—',
|
||||||
description: 'Nozzle area expansion ratio: ε = Aₑ/Aₜ',
|
description: 'Nozzle area expansion ratio: ε = Aₑ/Aₜ',
|
||||||
category: 'Nozzle Geometry',
|
category: 'Nozzle Geometry',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
Te: {
|
Te: {
|
||||||
id: 'Te', symbol: 'Tₑ', name: 'Exit Temperature', units: 'K',
|
id: 'Te', symbol: 'Tₑ', name: 'Exit Temperature', units: 'K',
|
||||||
description: 'Static temperature at the nozzle exit plane',
|
description: 'Static temperature at the nozzle exit plane',
|
||||||
category: 'Nozzle Geometry',
|
category: 'Nozzle Geometry',
|
||||||
unitFamily: 'temperature',
|
unitFamily: 'temperature',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Performance Metrics ───────────────────────────────────────────────
|
// ── Performance Metrics ───────────────────────────────────────────────
|
||||||
@@ -245,18 +280,21 @@ export const VARIABLES = {
|
|||||||
description: 'Ratio of thrust to vehicle weight at ignition',
|
description: 'Ratio of thrust to vehicle weight at ignition',
|
||||||
category: 'Performance',
|
category: 'Performance',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
m_vehicle: {
|
m_vehicle: {
|
||||||
id: 'm_vehicle', symbol: 'mᵥ', name: 'Vehicle Mass', units: 'kg',
|
id: 'm_vehicle', symbol: 'mᵥ', name: 'Vehicle Mass', units: 'kg',
|
||||||
description: 'Vehicle mass used for TWR calculation',
|
description: 'Vehicle mass used for TWR calculation',
|
||||||
category: 'Performance',
|
category: 'Performance',
|
||||||
unitFamily: 'mass',
|
unitFamily: 'mass',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
J: {
|
J: {
|
||||||
id: 'J', symbol: 'J', name: 'Total Impulse', units: 'N·s',
|
id: 'J', symbol: 'J', name: 'Total Impulse', units: 'N·s',
|
||||||
description: 'Integral of thrust over burn time: J = F·tᵦ (constant thrust)',
|
description: 'Integral of thrust over burn time: J = F·tᵦ (constant thrust)',
|
||||||
category: 'Performance',
|
category: 'Performance',
|
||||||
unitFamily: 'impulse',
|
unitFamily: 'impulse',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Mass & Propellant ─────────────────────────────────────────────────
|
// ── Mass & Propellant ─────────────────────────────────────────────────
|
||||||
@@ -265,18 +303,21 @@ export const VARIABLES = {
|
|||||||
description: 'Mass ratio of oxidiser to fuel flow rates',
|
description: 'Mass ratio of oxidiser to fuel flow rates',
|
||||||
category: 'Propellant',
|
category: 'Propellant',
|
||||||
unitFamily: 'dimensionless',
|
unitFamily: 'dimensionless',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
mdot_ox: {
|
mdot_ox: {
|
||||||
id: 'mdot_ox', symbol: 'ṁₒₓ', name: 'Oxidiser Flow Rate', units: 'kg/s',
|
id: 'mdot_ox', symbol: 'ṁₒₓ', name: 'Oxidiser Flow Rate', units: 'kg/s',
|
||||||
description: 'Mass flow rate of oxidiser',
|
description: 'Mass flow rate of oxidiser',
|
||||||
category: 'Propellant',
|
category: 'Propellant',
|
||||||
unitFamily: 'massflow',
|
unitFamily: 'massflow',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
mdot_f: {
|
mdot_f: {
|
||||||
id: 'mdot_f', symbol: 'ṁf', name: 'Fuel Flow Rate', units: 'kg/s',
|
id: 'mdot_f', symbol: 'ṁf', name: 'Fuel Flow Rate', units: 'kg/s',
|
||||||
description: 'Mass flow rate of fuel',
|
description: 'Mass flow rate of fuel',
|
||||||
category: 'Propellant',
|
category: 'Propellant',
|
||||||
unitFamily: 'massflow',
|
unitFamily: 'massflow',
|
||||||
|
positive: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function useEngineDesign() {
|
|||||||
const [chamber, setChamber] = useState({ Lstar: 1.0, contractionRatio: 8, convAngleDeg: 30 })
|
const [chamber, setChamber] = useState({ Lstar: 1.0, contractionRatio: 8, convAngleDeg: 30 })
|
||||||
const [nozzle, setNozzle] = useState({ type: 'conical', divAngleDeg: 15 })
|
const [nozzle, setNozzle] = useState({ type: 'conical', divAngleDeg: 15 })
|
||||||
const [injector, setInjector] = useState({ type: 'doublet', N: 20, dpFraction: 0.2, Cd: 0.7, rhoFuel: 800, rhoOx: 1140 })
|
const [injector, setInjector] = useState({ type: 'doublet', N: 20, dpFraction: 0.2, Cd: 0.7, rhoFuel: 800, rhoOx: 1140 })
|
||||||
const [cooling, setCooling] = useState({ method: 'regenerative', channelCount: 60, mu: 6e-5, cp: 2000, Pr: 0.7, T_wall: 800, filmFraction: 0.05 })
|
const [cooling, setCooling] = useState({ method: 'regenerative', channelCount: 60, mu: 6e-5, cp: 2000, Pr: 0.7, T_wall: 800, filmFraction: 0.05, ablativeMaterial: 'carbon_phenolic', ablativeThickness: 10 })
|
||||||
const [feedSystem, setFeedSystem] = useState({
|
const [feedSystem, setFeedSystem] = useState({
|
||||||
type: 'pressure_fed', feedFactor: 1.3,
|
type: 'pressure_fed', feedFactor: 1.3,
|
||||||
rhoFuel: 800, rhoOx: 1140, pressurantR: 2077, pressurantT: 300,
|
rhoFuel: 800, rhoOx: 1140, pressurantR: 2077, pressurantT: 300,
|
||||||
@@ -52,13 +52,41 @@ export function useEngineDesign() {
|
|||||||
const chamberGeometry = useMemo(() => calcChamber(allThermo, chamber), [allThermo, chamber])
|
const chamberGeometry = useMemo(() => calcChamber(allThermo, chamber), [allThermo, chamber])
|
||||||
const nozzleGeometry = useMemo(() => calcNozzle(allThermo, nozzle), [allThermo, nozzle])
|
const nozzleGeometry = useMemo(() => calcNozzle(allThermo, nozzle), [allThermo, nozzle])
|
||||||
const injectorGeometry = useMemo(() => calcInjector(allThermo, injector), [allThermo, injector])
|
const injectorGeometry = useMemo(() => calcInjector(allThermo, injector), [allThermo, injector])
|
||||||
const coolingResults = useMemo(() => calcCooling(allThermo, cooling, chamberGeometry), [allThermo, cooling, chamberGeometry])
|
const coolingResults = useMemo(() => calcCooling(allThermo, cooling, chamberGeometry, burnTime), [allThermo, cooling, chamberGeometry, burnTime])
|
||||||
const feedResults = useMemo(() => calcFeedSystem(allThermo, feedSystem, burnTime), [allThermo, feedSystem, burnTime])
|
const feedResults = useMemo(() => calcFeedSystem(allThermo, feedSystem, burnTime), [allThermo, feedSystem, burnTime])
|
||||||
|
|
||||||
function setThermoInput(key, value) {
|
function setThermoInput(key, value) {
|
||||||
setThermoInputs(prev => ({ ...prev, [key]: value }))
|
setThermoInputs(prev => ({ ...prev, [key]: value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply propellant values to thermoInputs and densities. */
|
||||||
|
function applyPropellant(propellant) {
|
||||||
|
// Map propellant values to thermo inputs
|
||||||
|
const thermoUpdates = {}
|
||||||
|
if (propellant.values?.T0 !== undefined) thermoUpdates.T0 = propellant.values.T0
|
||||||
|
if (propellant.values?.gamma !== undefined) thermoUpdates.gamma = propellant.values.gamma
|
||||||
|
if (propellant.values?.R !== undefined) thermoUpdates.R = propellant.values.R
|
||||||
|
if (propellant.values?.OF !== undefined) thermoUpdates.OF = propellant.values.OF
|
||||||
|
|
||||||
|
setThermoInputs(prev => ({ ...prev, ...thermoUpdates }))
|
||||||
|
|
||||||
|
// Also update densities if available
|
||||||
|
if (propellant.values?.rhoFuel !== undefined || propellant.values?.rhoOx !== undefined) {
|
||||||
|
setInjector(prev => ({
|
||||||
|
...prev,
|
||||||
|
rhoFuel: propellant.values?.rhoFuel ?? prev.rhoFuel,
|
||||||
|
rhoOx: propellant.values?.rhoOx ?? prev.rhoOx,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (propellant.values?.rhoFuel !== undefined || propellant.values?.rhoOx !== undefined) {
|
||||||
|
setFeedSystem(prev => ({
|
||||||
|
...prev,
|
||||||
|
rhoFuel: propellant.values?.rhoFuel ?? prev.rhoFuel,
|
||||||
|
rhoOx: propellant.values?.rhoOx ?? prev.rhoOx,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Restore all design state from a parsed import (inputs section). */
|
/** Restore all design state from a parsed import (inputs section). */
|
||||||
function loadDesign(inputs) {
|
function loadDesign(inputs) {
|
||||||
if (inputs.thermodynamics) {
|
if (inputs.thermodynamics) {
|
||||||
@@ -86,5 +114,6 @@ export function useEngineDesign() {
|
|||||||
chamberGeometry, nozzleGeometry, injectorGeometry, coolingResults, feedResults,
|
chamberGeometry, nozzleGeometry, injectorGeometry, coolingResults, feedResults,
|
||||||
// Actions
|
// Actions
|
||||||
loadDesign,
|
loadDesign,
|
||||||
|
applyPropellant,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export function useSolver() {
|
|||||||
// Selected unit per variable: { varId: unitId }
|
// Selected unit per variable: { varId: unitId }
|
||||||
const [unitSelections, setUnitSelections] = useState({})
|
const [unitSelections, setUnitSelections] = useState({})
|
||||||
const [sciNotation, setSciNotation] = useState(false)
|
const [sciNotation, setSciNotation] = useState(false)
|
||||||
|
// Area ratio branch: 'supersonic' or 'subsonic'
|
||||||
|
const [areaRatioBranch, setAreaRatioBranch] = useState('supersonic')
|
||||||
|
|
||||||
// Ref so getUnit/setValue always see the latest selections without stale closures
|
// Ref so getUnit/setValue always see the latest selections without stale closures
|
||||||
const unitSelectionsRef = useRef(unitSelections)
|
const unitSelectionsRef = useRef(unitSelections)
|
||||||
@@ -40,12 +42,17 @@ export function useSolver() {
|
|||||||
setSciNotation(prev => !prev)
|
setSciNotation(prev => !prev)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Run solver whenever userValues or workspace changes
|
const toggleAreaRatioBranch = useCallback(() => {
|
||||||
|
setAreaRatioBranch(prev => prev === 'supersonic' ? 'subsonic' : 'supersonic')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Run solver whenever userValues, workspace, or branch changes
|
||||||
const { solved, missingReport } = useMemo(() => {
|
const { solved, missingReport } = useMemo(() => {
|
||||||
const { solved, missing: _missing } = solve(userValues)
|
const opts = { area_ratio_mach: { supersonic: areaRatioBranch === 'supersonic' } }
|
||||||
|
const { solved, missing: _missing } = solve(userValues, opts)
|
||||||
const report = getMissingReport(workspaceVarIds, userValues, solved)
|
const report = getMissingReport(workspaceVarIds, userValues, solved)
|
||||||
return { solved, missingReport: report }
|
return { solved, missingReport: report }
|
||||||
}, [userValues, workspaceVarIds])
|
}, [userValues, workspaceVarIds, areaRatioBranch])
|
||||||
|
|
||||||
const addVariable = useCallback((varId) => {
|
const addVariable = useCallback((varId) => {
|
||||||
setWorkspaceVarIds(prev => prev.includes(varId) ? prev : [...prev, varId])
|
setWorkspaceVarIds(prev => prev.includes(varId) ? prev : [...prev, varId])
|
||||||
@@ -70,6 +77,18 @@ export function useSolver() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const reorderVariable = useCallback((fromId, toId) => {
|
||||||
|
setWorkspaceVarIds(prev => {
|
||||||
|
const fromIdx = prev.indexOf(fromId)
|
||||||
|
const toIdx = prev.indexOf(toId)
|
||||||
|
if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return prev
|
||||||
|
const next = [...prev]
|
||||||
|
next.splice(fromIdx, 1)
|
||||||
|
next.splice(toIdx, 0, fromId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
// rawDisplayValue is in the currently-selected display unit; we convert to SI for storage
|
// rawDisplayValue is in the currently-selected display unit; we convert to SI for storage
|
||||||
const setValue = useCallback((varId, rawDisplayValue) => {
|
const setValue = useCallback((varId, rawDisplayValue) => {
|
||||||
setUserValues(prev => {
|
setUserValues(prev => {
|
||||||
@@ -143,13 +162,16 @@ export function useSolver() {
|
|||||||
allKnown,
|
allKnown,
|
||||||
unitSelections,
|
unitSelections,
|
||||||
sciNotation,
|
sciNotation,
|
||||||
|
areaRatioBranch,
|
||||||
getUnitId,
|
getUnitId,
|
||||||
getUnit,
|
getUnit,
|
||||||
setUnit,
|
setUnit,
|
||||||
toggleSciNotation,
|
toggleSciNotation,
|
||||||
|
toggleAreaRatioBranch,
|
||||||
addVariable,
|
addVariable,
|
||||||
addVariables,
|
addVariables,
|
||||||
removeVariable,
|
removeVariable,
|
||||||
|
reorderVariable,
|
||||||
setValue,
|
setValue,
|
||||||
addPreset,
|
addPreset,
|
||||||
applyPropellant,
|
applyPropellant,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef, useState, useEffect } from 'react'
|
import { useRef, useState, useEffect } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useEngineDesign } from '../hooks/useEngineDesign.js'
|
import { useEngineDesign } from '../hooks/useEngineDesign.js'
|
||||||
|
import { PropellantModal } from '../components/PropellantModal.jsx'
|
||||||
import {
|
import {
|
||||||
exportEngineJSON,
|
exportEngineJSON,
|
||||||
exportEngineOdt,
|
exportEngineOdt,
|
||||||
@@ -13,6 +14,7 @@ import ErrorBoundary from '../components/ErrorBoundary.jsx'
|
|||||||
import { formatValue } from '../engine/format.js'
|
import { formatValue } from '../engine/format.js'
|
||||||
import { getUnitsForFamily } from '../engine/units.js'
|
import { getUnitsForFamily } from '../engine/units.js'
|
||||||
import { ENGINE_FIELD_INFO } from '../engine/engineFieldInfo.js'
|
import { ENGINE_FIELD_INFO } from '../engine/engineFieldInfo.js'
|
||||||
|
import { ABLATIVE_MATERIALS } from '../engine/knowledgebaseData.js'
|
||||||
|
|
||||||
/* ── Info popup ───────────────────────────────────────────────────── */
|
/* ── Info popup ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -231,6 +233,7 @@ function ResultSection({ title, children }) {
|
|||||||
|
|
||||||
export default function EnginePage() {
|
export default function EnginePage() {
|
||||||
const importRef = useRef(null)
|
const importRef = useRef(null)
|
||||||
|
const [showPropellants, setShowPropellants] = useState(false)
|
||||||
const {
|
const {
|
||||||
thermoInputs, setThermoInput,
|
thermoInputs, setThermoInput,
|
||||||
chamber, setChamber,
|
chamber, setChamber,
|
||||||
@@ -246,6 +249,7 @@ export default function EnginePage() {
|
|||||||
coolingResults: cr,
|
coolingResults: cr,
|
||||||
feedResults: fr,
|
feedResults: fr,
|
||||||
loadDesign,
|
loadDesign,
|
||||||
|
applyPropellant,
|
||||||
} = useEngineDesign()
|
} = useEngineDesign()
|
||||||
|
|
||||||
function handleExportJSON() {
|
function handleExportJSON() {
|
||||||
@@ -320,7 +324,17 @@ export default function EnginePage() {
|
|||||||
{/* ── Left: Inputs ── */}
|
{/* ── Left: Inputs ── */}
|
||||||
<div className="w-[420px] shrink-0 overflow-y-auto p-4 border-r border-slate-700">
|
<div className="w-[420px] shrink-0 overflow-y-auto p-4 border-r border-slate-700">
|
||||||
|
|
||||||
<DesignSection title="Thermodynamic Inputs">
|
<DesignSection
|
||||||
|
title="Thermodynamic Inputs"
|
||||||
|
headerAction={
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPropellants(true)}
|
||||||
|
className="px-2 py-1 text-xs bg-blue-700 hover:bg-blue-600 text-white rounded transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
⛽ Load Propellant
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
<NumInput
|
<NumInput
|
||||||
label="Chamber Pressure (p₀)"
|
label="Chamber Pressure (p₀)"
|
||||||
value={thermoInputs.p0}
|
value={thermoInputs.p0}
|
||||||
@@ -577,6 +591,24 @@ export default function EnginePage() {
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{cooling.method === 'ablative' && (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
label="Liner Material"
|
||||||
|
value={cooling.ablativeMaterial}
|
||||||
|
onChange={v => setCooling(c => ({ ...c, ablativeMaterial: v }))}
|
||||||
|
options={ABLATIVE_MATERIALS.map(m => ({ value: m.id, label: m.name }))}
|
||||||
|
/>
|
||||||
|
<NumInput
|
||||||
|
label="Initial Thickness"
|
||||||
|
value={cooling.ablativeThickness}
|
||||||
|
onChange={v => setCooling(c => ({ ...c, ablativeThickness: v }))}
|
||||||
|
units="mm"
|
||||||
|
step="1"
|
||||||
|
placeholder="10"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DesignSection>
|
</DesignSection>
|
||||||
|
|
||||||
<DesignSection title="Feed System">
|
<DesignSection title="Feed System">
|
||||||
@@ -694,6 +726,52 @@ export default function EnginePage() {
|
|||||||
<ResultRow label="Est. Isp Penalty" value={cr.ispPenalty} unit="%" infoKey="ispPenalty_result" />
|
<ResultRow label="Est. Isp Penalty" value={cr.ispPenalty} unit="%" infoKey="ispPenalty_result" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{cr.method === 'ablative' && cr.material && (
|
||||||
|
<>
|
||||||
|
<div className="text-sm mb-2">
|
||||||
|
<span className="text-slate-400">Material: </span>
|
||||||
|
<span className="text-slate-100 font-semibold">{cr.material.name}</span>
|
||||||
|
</div>
|
||||||
|
{cr.pressureFactor && Math.abs(cr.pressureFactor - 1.0) > 0.01 && (
|
||||||
|
<ResultRow label="Pressure Correction" value={cr.pressureFactor} unit="×" />
|
||||||
|
)}
|
||||||
|
<ResultRow label="Erosion Rate" value={cr.effectiveRate} unit="mm/s" />
|
||||||
|
<ResultRow label="Initial Thickness" value={cr.ablativeThickness} unit="mm" />
|
||||||
|
<div className="flex items-center gap-2 text-sm py-1.5">
|
||||||
|
<span className="text-slate-400 w-48 shrink-0">Eroded</span>
|
||||||
|
<span className="font-mono text-green-400">
|
||||||
|
{formatValue(cr.erosionMm)} mm
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
({formatValue(cr.erosionMmMin)}–{formatValue(cr.erosionMmMax)} mm)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm py-1.5">
|
||||||
|
<span className="text-slate-400 w-48 shrink-0">Remaining Thickness</span>
|
||||||
|
<span className={`font-mono font-semibold ${
|
||||||
|
cr.status === 'critical' ? 'text-red-400' :
|
||||||
|
cr.status === 'warning' ? 'text-amber-400' :
|
||||||
|
'text-green-400'
|
||||||
|
}`}>
|
||||||
|
{formatValue(cr.remainingMm)} mm
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
(worst: {formatValue(cr.remainingMmWorst)} mm)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{(cr.status === 'warning' || cr.status === 'critical') && (
|
||||||
|
<div className={`text-xs px-3 py-2 rounded mt-2 ${
|
||||||
|
cr.status === 'critical'
|
||||||
|
? 'bg-red-900/30 border border-red-700 text-red-200'
|
||||||
|
: 'bg-amber-900/30 border border-amber-700 text-amber-200'
|
||||||
|
}`}>
|
||||||
|
{cr.status === 'critical'
|
||||||
|
? '🚨 CRITICAL: Liner thickness below safe minimum!'
|
||||||
|
: '⚠️ WARNING: Liner thickness approaching minimum!'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{cr.note && (
|
{cr.note && (
|
||||||
<p className="text-xs text-slate-400 italic mt-1">{cr.note}</p>
|
<p className="text-xs text-slate-400 italic mt-1">{cr.note}</p>
|
||||||
)}
|
)}
|
||||||
@@ -720,6 +798,14 @@ export default function EnginePage() {
|
|||||||
</ResultSection>
|
</ResultSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showPropellants && (
|
||||||
|
<PropellantModal
|
||||||
|
onClose={() => setShowPropellants(false)}
|
||||||
|
onApply={applyPropellant}
|
||||||
|
description="Select a propellant to pre-fill γ, R, T₀, O/F, and densities."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
197
src/pages/KnowledgebaseAblativesPage.jsx
Normal file
197
src/pages/KnowledgebaseAblativesPage.jsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { useState, useMemo } from 'react'
|
||||||
|
import { ABLATIVE_MATERIALS } from '../engine/knowledgebaseData.js'
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function categoryBadgeClass(app) {
|
||||||
|
const map = {
|
||||||
|
'nozzle throat': 'bg-red-900 text-red-200 border border-red-700',
|
||||||
|
'nozzle throat (high heat flux)': 'bg-red-900 text-red-200 border border-red-700',
|
||||||
|
'nozzle throat (ultra-high performance)': 'bg-red-900 text-red-200 border border-red-700',
|
||||||
|
'combustion chamber wall': 'bg-orange-900 text-orange-200 border border-orange-700',
|
||||||
|
'combustion chamber': 'bg-orange-900 text-orange-200 border border-orange-700',
|
||||||
|
'chamber sections': 'bg-orange-900 text-orange-200 border border-orange-700',
|
||||||
|
'chamber (extreme conditions)': 'bg-orange-900 text-orange-200 border border-orange-700',
|
||||||
|
'flexible throat': 'bg-purple-900 text-purple-200 border border-purple-700',
|
||||||
|
'low-pressure chamber': 'bg-blue-900 text-blue-200 border border-blue-700',
|
||||||
|
'insulation layer': 'bg-slate-700 text-slate-200 border border-slate-500',
|
||||||
|
'amateur engines': 'bg-green-900 text-green-200 border border-green-700',
|
||||||
|
}
|
||||||
|
return map[app] ?? 'bg-slate-700 text-slate-200 border border-slate-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
function performanceBadge(material) {
|
||||||
|
const avgErosion = (material.erosionRateRange[0] + material.erosionRateRange[1]) / 2
|
||||||
|
if (avgErosion < 0.15) return { label: 'Ultra-High Performance', color: 'bg-red-900 text-red-200 border border-red-700' }
|
||||||
|
if (avgErosion < 0.35) return { label: 'High Performance', color: 'bg-orange-900 text-orange-200 border border-orange-700' }
|
||||||
|
if (avgErosion < 0.60) return { label: 'Moderate', color: 'bg-blue-900 text-blue-200 border border-blue-700' }
|
||||||
|
return { label: 'Low Cost', color: 'bg-green-900 text-green-200 border border-green-700' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropRow({ label, value, unit }) {
|
||||||
|
if (value === null || value === undefined) return null
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[160px_1fr] gap-x-3 py-1.5 border-b border-slate-800">
|
||||||
|
<span className="text-slate-400 text-sm">{label}</span>
|
||||||
|
<span className="text-slate-100 text-sm">{typeof value === 'number' ? value.toFixed(2) : value}{unit ? ` ${unit}` : ''}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main page ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function KnowledgebaseAblativesPage() {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [selectedId, setSelectedId] = useState(ABLATIVE_MATERIALS[0].id)
|
||||||
|
|
||||||
|
// Filtered list
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = search.toLowerCase()
|
||||||
|
return ABLATIVE_MATERIALS.filter(m => {
|
||||||
|
if (q && !m.name.toLowerCase().includes(q) && !m.composition.toLowerCase().includes(q)) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}, [search])
|
||||||
|
|
||||||
|
const selected = ABLATIVE_MATERIALS.find(m => m.id === selectedId) ?? ABLATIVE_MATERIALS[0]
|
||||||
|
|
||||||
|
// Ensure selected item is still in filtered list; if not, show first filtered
|
||||||
|
const effectiveSelected =
|
||||||
|
filtered.find(m => m.id === selectedId)
|
||||||
|
? selected
|
||||||
|
: (filtered[0] ?? selected)
|
||||||
|
|
||||||
|
const perf = performanceBadge(effectiveSelected)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
{/* ── Left panel ────────────────────────────────────────────────────── */}
|
||||||
|
<div className="w-72 shrink-0 flex flex-col border-r border-slate-700 bg-slate-900">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="p-3 border-b border-slate-700">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search materials…"
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="w-full px-3 py-1.5 rounded-md bg-slate-800 border border-slate-600 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Material list */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p className="p-3 text-xs text-slate-500">No materials match your search.</p>
|
||||||
|
) : (
|
||||||
|
filtered.map(material => (
|
||||||
|
<button
|
||||||
|
key={material.id}
|
||||||
|
onClick={() => setSelectedId(material.id)}
|
||||||
|
className={`block w-full text-left px-4 py-2 text-sm transition-colors border-l-2 ${
|
||||||
|
selectedId === material.id
|
||||||
|
? 'bg-slate-700 text-white border-l-blue-500'
|
||||||
|
: 'text-slate-300 hover:bg-slate-800 border-l-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="font-medium">{material.name}</div>
|
||||||
|
<div className="text-xs text-slate-500">{material.composition}</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Right panel ────────────────────────────────────────────────────── */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{effectiveSelected && (
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-1">{effectiveSelected.name}</h2>
|
||||||
|
<p className="text-sm text-slate-400">{effectiveSelected.composition}</p>
|
||||||
|
</div>
|
||||||
|
<div className={`px-3 py-1 rounded text-xs font-semibold border ${perf.color}`}>
|
||||||
|
{perf.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-300 leading-relaxed mb-4">
|
||||||
|
{effectiveSelected.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Specifications table */}
|
||||||
|
<div className="mb-6 bg-slate-800/50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-200 mb-3 uppercase tracking-wider">Specifications</h3>
|
||||||
|
<div className="divide-y divide-slate-800">
|
||||||
|
<PropRow
|
||||||
|
label="Erosion Rate"
|
||||||
|
value={effectiveSelected.erosionRate}
|
||||||
|
unit="mm/s"
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-[160px_1fr] gap-x-3 py-1.5 border-b border-slate-800">
|
||||||
|
<span className="text-slate-400 text-sm">Erosion Range</span>
|
||||||
|
<span className="text-slate-100 text-sm">{effectiveSelected.erosionRateRange[0].toFixed(2)} – {effectiveSelected.erosionRateRange[1].toFixed(2)} mm/s</span>
|
||||||
|
</div>
|
||||||
|
<PropRow
|
||||||
|
label="Max Temperature"
|
||||||
|
value={effectiveSelected.maxTemp}
|
||||||
|
unit="K"
|
||||||
|
/>
|
||||||
|
<PropRow
|
||||||
|
label="Density"
|
||||||
|
value={effectiveSelected.density}
|
||||||
|
unit="kg/m³"
|
||||||
|
/>
|
||||||
|
<PropRow
|
||||||
|
label="Thermal Conductivity"
|
||||||
|
value={effectiveSelected.thermalConductivity}
|
||||||
|
unit="W/(m·K)"
|
||||||
|
/>
|
||||||
|
<PropRow
|
||||||
|
label="Char Yield"
|
||||||
|
value={(effectiveSelected.charYield * 100).toFixed(1)}
|
||||||
|
unit="%"
|
||||||
|
/>
|
||||||
|
<PropRow
|
||||||
|
label="Reference Pressure"
|
||||||
|
value={(effectiveSelected.refPressure / 1e6).toFixed(1)}
|
||||||
|
unit="MPa"
|
||||||
|
/>
|
||||||
|
<PropRow
|
||||||
|
label="Pressure Exponent (n)"
|
||||||
|
value={effectiveSelected.pressureExponent}
|
||||||
|
unit="—"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Applications */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-200 mb-3 uppercase tracking-wider">Applications</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{effectiveSelected.applications.map((app, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium border ${categoryBadgeClass(app)}`}
|
||||||
|
>
|
||||||
|
{app}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div className="bg-slate-800/50 rounded-lg p-4">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-200 mb-2 uppercase tracking-wider">Notes & Guidance</h3>
|
||||||
|
<p className="text-sm text-slate-300 leading-relaxed">
|
||||||
|
{effectiveSelected.notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,10 +22,13 @@ export default function Solver() {
|
|||||||
getUnit,
|
getUnit,
|
||||||
setUnit,
|
setUnit,
|
||||||
sciNotation,
|
sciNotation,
|
||||||
|
areaRatioBranch,
|
||||||
toggleSciNotation,
|
toggleSciNotation,
|
||||||
|
toggleAreaRatioBranch,
|
||||||
addVariable,
|
addVariable,
|
||||||
addVariables,
|
addVariables,
|
||||||
removeVariable,
|
removeVariable,
|
||||||
|
reorderVariable,
|
||||||
setValue,
|
setValue,
|
||||||
addPreset,
|
addPreset,
|
||||||
applyPropellant,
|
applyPropellant,
|
||||||
@@ -48,10 +51,20 @@ export default function Solver() {
|
|||||||
|
|
||||||
function handleDragEnd(event) {
|
function handleDragEnd(event) {
|
||||||
const { over, active } = event
|
const { over, active } = event
|
||||||
if (over?.id === 'workspace') {
|
if (!over) {
|
||||||
|
setActiveVarId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette → workspace drop
|
||||||
|
if (over.id === 'workspace') {
|
||||||
const { varId } = active.data.current ?? {}
|
const { varId } = active.data.current ?? {}
|
||||||
if (varId) addVariable(varId)
|
if (varId) addVariable(varId)
|
||||||
}
|
}
|
||||||
|
// Workspace → workspace reorder
|
||||||
|
else if (workspaceVarIds.includes(active.id) && workspaceVarIds.includes(over.id)) {
|
||||||
|
reorderVariable(active.id, over.id)
|
||||||
|
}
|
||||||
setActiveVarId(null)
|
setActiveVarId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +141,8 @@ export default function Solver() {
|
|||||||
getUnit={getUnit}
|
getUnit={getUnit}
|
||||||
setUnit={setUnit}
|
setUnit={setUnit}
|
||||||
sciNotation={sciNotation}
|
sciNotation={sciNotation}
|
||||||
|
areaRatioBranch={areaRatioBranch}
|
||||||
|
onToggleAreaRatioBranch={toggleAreaRatioBranch}
|
||||||
/>
|
/>
|
||||||
<ResultsPanel
|
<ResultsPanel
|
||||||
workspaceVarIds={workspaceVarIds}
|
workspaceVarIds={workspaceVarIds}
|
||||||
@@ -147,7 +162,6 @@ export default function Solver() {
|
|||||||
<PropellantModal
|
<PropellantModal
|
||||||
onClose={() => setShowPropellants(false)}
|
onClose={() => setShowPropellants(false)}
|
||||||
onApply={applyPropellant}
|
onApply={applyPropellant}
|
||||||
existingVarIds={workspaceVarIds}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user