refactoring (broken right now)

This commit is contained in:
2026-05-06 15:24:39 +02:00
parent bc4e077924
commit bc0df51ddb
25 changed files with 1184 additions and 1983 deletions

43
Components/Atmosphere.cs Normal file
View File

@@ -0,0 +1,43 @@
using FluidSim.Interfaces;
namespace FluidSim.Components
{
/// <summary>
/// Represents the ambient atmosphere constant pressure/temperature reservoir.
/// </summary>
public class Atmosphere : IComponent
{
public double Pressure { get; set; } = 101325.0;
public double Temperature { get; set; } = 300.0;
public double GasConstant { get; set; } = 287.0;
public double Gamma => 1.4;
public double Density => Pressure / (GasConstant * Temperature);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density;
public Port Port { get; }
public Atmosphere()
{
Port = new Port { Owner = this };
UpdatePort();
}
public IReadOnlyList<Port> Ports => new[] { Port };
public void UpdateState(double dt)
{
// Atmosphere is static just ensure the port reflects current values
UpdatePort();
}
private void UpdatePort()
{
Port.Pressure = Pressure;
Port.Density = Density;
Port.Temperature = Temperature;
Port.SpecificEnthalpy = SpecificEnthalpy;
// MassFlowRate is set by the solver connector
}
}
}

View File

@@ -1,249 +0,0 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
public class EngineCylinder
{
public Volume0D Cylinder { get; private set; }
private Crankshaft crankshaft;
private double bore, stroke, conRodLength, compressionRatio;
private double pistonArea;
public double V_disp { get; private set; }
public double V_clear { get; private set; }
public bool ignition = false;
// ---- Exhaust valve ----
private double exhMaxOrificeArea;
private double exhValveOpenStart = 130.0 * Math.PI / 180.0;
private double exhValveOpenEnd = 390.0 * Math.PI / 180.0;
private double exhValveRampWidth = 30.0 * Math.PI / 180.0;
public double ExhaustOrificeArea => ExhaustValveLift() * exhMaxOrificeArea;
public double ExhaustValveLiftCurrent => ExhaustValveLift();
// ---- Intake valve ----
private double intMaxOrificeArea;
private double intValveOpenStart = 340.0 * Math.PI / 180.0;
private double intValveOpenEnd = 600.0 * Math.PI / 180.0;
private double intValveRampWidth = 30.0 * Math.PI / 180.0;
public double IntakeOrificeArea => IntakeValveLift() * intMaxOrificeArea;
public double IntakeValveLiftCurrent => IntakeValveLift();
// ---- Combustion ----
public double TargetPeakPressure { get; set; } = 50.0 * 101325.0;
private const double PeakTemperature = 2500.0;
private bool burnInProgress = false;
private double burnStartAngle; // cycle angle (04π)
private double burnDuration = 40.0 * Math.PI / 180.0;
private double targetBurnEnergy;
private double preIgnitionMass, preIgnitionInternalEnergy;
private Random rand = new Random();
public double MisfireProbability { get; set; } = 0.02;
private bool misfireCurrent = false;
public int CombustionCount { get; private set; }
public int MisfireCount { get; private set; }
// Cycleaware angle (0 4π)
public double CycleAngle => crankshaft.CrankAngle % (4.0 * Math.PI);
private double prevCycleAngle;
// Piston position fraction (0 = TDC, 1 = BDC)
public double PistonPositionFraction
{
get
{
double currentVol = Cylinder.Volume;
if (currentVol <= V_clear) return 0.0;
if (currentVol >= V_clear + V_disp) return 1.0;
return (currentVol - V_clear) / V_disp;
}
}
public EngineCylinder(Crankshaft crankshaft,
double bore, double stroke, double compressionRatio,
double exhPipeArea, double intPipeArea, int sampleRate)
{
this.crankshaft = crankshaft;
this.bore = bore;
this.stroke = stroke;
conRodLength = 2.0 * stroke;
this.compressionRatio = compressionRatio;
exhMaxOrificeArea = exhPipeArea * 0.5;
intMaxOrificeArea = intPipeArea * 0.5;
pistonArea = Math.PI / 4.0 * bore * bore;
V_disp = pistonArea * stroke;
V_clear = V_disp / (compressionRatio - 1.0);
// Start at BDC with fresh ambient charge
double V_bdc = V_clear + V_disp;
double p_amb = 101325.0;
double T_amb = 300.0;
double rho0 = p_amb / (287.0 * T_amb);
double mass0 = rho0 * V_bdc;
double energy0 = p_amb * V_bdc / (1.4 - 1.0);
Cylinder = new Volume0D(V_bdc, p_amb, T_amb, sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
Cylinder.Volume = V_bdc;
Cylinder.Mass = mass0;
Cylinder.InternalEnergy = energy0;
prevCycleAngle = CycleAngle;
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
}
// ---- Piston kinematics ----
private (double volume, double dvdt) PistonKinematics(double cycleAngle)
{
double theta = cycleAngle % (2.0 * Math.PI);
double R = stroke / 2.0;
double cosT = Math.Cos(theta);
double sinT = Math.Sin(theta);
double L = conRodLength;
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
double V = V_clear + pistonArea * s;
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
double dVdθ = pistonArea * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
double dvdt = dVdθ * crankshaft.AngularVelocity;
return (V, dvdt);
}
// ---- Valve lifts (cycleaware) ----
private double ExhaustValveLift()
{
double a = CycleAngle;
if (a < exhValveOpenStart || a > exhValveOpenEnd) return 0.0;
double duration = exhValveOpenEnd - exhValveOpenStart;
double ramp = exhValveRampWidth;
double t = (a - exhValveOpenStart) / duration;
double rampFrac = ramp / duration;
if (t < rampFrac) return t / rampFrac;
if (t > 1.0 - rampFrac) return (1.0 - t) / rampFrac;
return 1.0;
}
private double IntakeValveLift()
{
double a = CycleAngle;
if (a < intValveOpenStart || a > intValveOpenEnd) return 0.0;
double duration = intValveOpenEnd - intValveOpenStart;
double ramp = intValveRampWidth;
double t = (a - intValveOpenStart) / duration;
double rampFrac = ramp / duration;
if (t < rampFrac) return t / rampFrac;
if (t > 1.0 - rampFrac) return (1.0 - t) / rampFrac;
return 1.0;
}
// ---- Wiebe burn fraction ----
private double WiebeFraction(double angleFromIgnition)
{
if (angleFromIgnition >= burnDuration) return 1.0;
double a = 5.0, m = 2.0;
double x = angleFromIgnition / burnDuration;
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
}
// ---- Torque from pressure ----
private double ComputeTorque()
{
double p = Cylinder.Pressure;
double ambient = 101325.0;
double force = (p - ambient) * pistonArea;
if (force <= 0) return 0.0;
double theta = crankshaft.CrankAngle % (2.0 * Math.PI);
double R = stroke / 2.0;
double L = conRodLength;
double sinT = Math.Sin(theta);
double cosT = Math.Cos(theta);
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
double lever = R * sinT * (1.0 + (R * cosT) / sqrtTerm);
return force * lever;
}
// ---- TDC detection (power stroke, at angle 0 mod 4π) ----
private bool DetectTDCPowerStroke()
{
double current = CycleAngle;
double previous = prevCycleAngle;
prevCycleAngle = current;
return (previous > 3.8 * Math.PI && current < 0.2 * Math.PI);
}
public void Step(double dt)
{
bool crossingTDC = DetectTDCPowerStroke();
if (crossingTDC)
{
misfireCurrent = rand.NextDouble() < MisfireProbability;
// *** Always capture the state at TDC, whether we burn or not ***
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
if (misfireCurrent)
{
MisfireCount++;
}
else if (ignition)
{
double V = Cylinder.Volume;
targetBurnEnergy = TargetPeakPressure * V / (Cylinder.Gamma - 1.0);
if (double.IsNaN(targetBurnEnergy))
targetBurnEnergy = 101325.0 * V / (Cylinder.Gamma - 1.0);
burnInProgress = true;
burnStartAngle = CycleAngle;
CombustionCount++;
}
}
if (burnInProgress)
{
double angleFromIgnition = CycleAngle - burnStartAngle;
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI;
if (angleFromIgnition >= burnDuration)
{
Cylinder.InternalEnergy = targetBurnEnergy;
burnInProgress = false;
}
else
{
double fraction = WiebeFraction(angleFromIgnition);
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction)
+ targetBurnEnergy * fraction;
Cylinder.Mass = preIgnitionMass;
}
}
var (vol, dvdt) = PistonKinematics(CycleAngle);
Cylinder.Volume = vol;
Cylinder.Dvdt = dvdt;
if (double.IsNaN(Cylinder.Pressure) || double.IsNaN(Cylinder.Temperature) || Cylinder.Mass < 1e-9)
{
double V = Math.Max(vol, V_clear);
Cylinder.Mass = 1.225 * V;
Cylinder.InternalEnergy = 101325.0 * V / (1.4 - 1.0);
}
double torque = ComputeTorque();
crankshaft.AddTorque(torque);
}
}
}

View File

@@ -1,616 +1,318 @@
using System;
using System.Numerics;
using FluidSim.Interfaces;
namespace FluidSim.Components
{
public enum BoundaryType
{
OpenEnd,
ZeroPressureOpen,
ClosedEnd,
GhostCell
}
public class Pipe1D
/// <summary>
/// 1D compressible Euler pipe (finitevolume, HLLC flux).
/// Boundary conditions are set externally via SetGhostLeft/Right.
/// Enforces that ghosts are always valid before stepping.
/// Uses exponential damping and Newtonian energy relaxation.
/// </summary>
public class Pipe1D : IComponent
{
public Port PortA { get; }
public Port PortB { get; }
public double Area => _area;
public double Area { get; }
public double DampingMultiplier { get; set; } = 1.0;
public double EnergyRelaxationRate { get; set; } = 0.0; // 1/s
private int _n; // number of real cells
private float _dx, _dt; // spatial step, global time step
private float _area, _diameter; // crosssection, diameter
private float _gamma; // ratio of specific heats (1.4)
// Conserved variables arrays sized [_n] (only real cells, ghosts handled externally)
private float[] _rho;
private float[] _rhou;
private float[] _E;
// Flux arrays for faces 0 .. _n (face i is between cell i-1 and i)
private float[] _fluxM; // mass flux
private float[] _fluxP; // momentum flux
private float[] _fluxE; // energy flux
// Ghost cell states
private float _rhoGhostL, _uGhostL, _pGhostL;
private float _rhoGhostR, _uGhostR, _pGhostR;
private bool _ghostLSet, _ghostRSet;
private BoundaryType _aBCType = BoundaryType.GhostCell;
private BoundaryType _bBCType = BoundaryType.GhostCell;
private float _aAmbientPressure = 101325f;
private float _bAmbientPressure = 101325f;
// CFL / sub-stepping
private const float CflTarget = 0.8f;
private const float ReferenceSoundSpeed = 340f;
private float _lastMassFlow = 0f;
// Precomputed for damping
private float _laminarCoeff; // 8*mu / r^2, then multiplied by DampingMultiplier
// ---- Energy loss (Newton cooling) ----
private float _ambientEnergyReference; // total energy density at ambient (Pamb / (γ-1))
public float EnergyRelaxationRate { get; set; } = 0.0f; // 1/s
public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0)
// Ambient pressure for the energy relaxation term (default 101325 Pa)
private double _ambientPressure = 101325.0;
public double AmbientPressure
{
float dtGlobal = 1f / sampleRate;
int nCells;
float dxTarget = ReferenceSoundSpeed * dtGlobal / CflTarget;
if (forcedCellCount > 1)
nCells = forcedCellCount;
else
get => _ambientPressure;
set
{
nCells = Math.Max(2, (int)Math.Round((float)length / dxTarget, MidpointRounding.AwayFromZero));
while (length / nCells > dxTarget * 1.01f && nCells < int.MaxValue - 1)
nCells++;
_ambientPressure = value;
_ambientEnergyReference = value / (_gamma - 1.0);
}
}
_n = nCells;
_dx = (float)(length / nCells);
_dt = dtGlobal;
_area = (float)area;
_diameter = (float)(2.0 * Math.Sqrt(area / Math.PI));
_gamma = 1.4f;
// Geometry
private readonly int _n; // number of real cells
private readonly double _dx; // cell size (m)
private readonly double _diameter; // m
private readonly double _gamma = 1.4;
_rho = new float[_n];
_rhou = new float[_n];
_E = new float[_n];
// Conserved variables [0 .. _n-1]
private double[] _rho;
private double[] _rhou;
private double[] _E;
// +1 because there are _n+1 faces
_fluxM = new float[_n + 1];
_fluxP = new float[_n + 1];
_fluxE = new float[_n + 1];
// Face fluxes [0 .. _n]
private double[] _fluxM;
private double[] _fluxP;
private double[] _fluxE;
// Precompute laminar damping coefficient (using air at 20°C)
float mu_air = 1.8e-5f;
float radius = _diameter * 0.5f;
_laminarCoeff = 8f * mu_air / (radius * radius); // will be multiplied by DampingMultiplier at each step
// Ghost cells (set externally)
private double _rhoGhostL, _uGhostL, _pGhostL;
private double _rhoGhostR, _uGhostR, _pGhostR;
private bool _ghostLValid, _ghostRValid;
// Ambient reference energy (internal energy per unit volume at 101325 Pa)
_ambientEnergyReference = 101325f / (_gamma - 1f); // ≈ 253312.5 J/m³
// Precomputed damping coefficient
private double _laminarCoeff;
private double _ambientEnergyReference; // internal energy density at ambient pressure
PortA = new Port();
PortB = new Port();
}
// ==================== PUBLIC API ============================
public void SetABoundaryType(BoundaryType type) => _aBCType = type;
public void SetBBoundaryType(BoundaryType type) => _bBCType = type;
public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p;
public void SetBAmbientPressure(double p) => _bAmbientPressure = (float)p;
public float GetFaceMassFlux(int faceIndex)
/// <summary>
/// Initialise a pipe with a given cell count.
/// </summary>
/// <param name="length">Pipe length (m).</param>
/// <param name="area">Crosssectional area (m²).</param>
/// <param name="cellCount">Number of finitevolume cells (≥ 4).</param>
public Pipe1D(double length, double area, int cellCount)
{
if (faceIndex < 0 || faceIndex > _n) return 0f;
return _fluxM[faceIndex];
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
_n = cellCount;
_dx = length / _n;
Area = area;
_diameter = 2.0 * Math.Sqrt(area / Math.PI);
_rho = new double[_n];
_rhou = new double[_n];
_E = new double[_n];
_fluxM = new double[_n + 1];
_fluxP = new double[_n + 1];
_fluxE = new double[_n + 1];
// Laminar damping coefficient for air at 20°C (multiplied by DampingMultiplier each step)
double mu_air = 1.8e-5;
double radius = _diameter * 0.5;
_laminarCoeff = 8.0 * mu_air / (radius * radius);
// Ambient energy reference (internal energy per unit volume at 101325 Pa)
_ambientEnergyReference = 101325.0 / (_gamma - 1.0);
PortA = new Port { Owner = this };
PortB = new Port { Owner = this };
// Initial state = still air at ambient conditions
SetUniformState(1.225, 0.0, 101325.0);
}
IReadOnlyList<Port> IComponent.Ports => new[] { PortA, PortB };
// No integration needed for pipes state is advanced via substeps
public void UpdateState(double dt) { }
// ---------- Ghost cell interface ----------
public void SetGhostLeft(double rho, double u, double p)
{
_rhoGhostL = (float)rho;
_uGhostL = (float)u;
_pGhostL = (float)p;
_ghostLSet = true;
_rhoGhostL = rho;
_uGhostL = u;
_pGhostL = p;
_ghostLValid = true;
}
public void SetGhostRight(double rho, double u, double p)
{
_rhoGhostR = (float)rho;
_uGhostR = (float)u;
_pGhostR = (float)p;
_ghostRSet = true;
}
public void ClearGhostFlag()
{
_ghostLSet = false;
_ghostRSet = false;
_rhoGhostR = rho;
_uGhostR = u;
_pGhostR = p;
_ghostRValid = true;
}
public void SetUniformState(double rho, double u, double p)
public void ClearGhostFlags()
{
float r = (float)rho;
float vel = (float)u;
float pr = (float)p;
float e = pr / ((_gamma - 1f) * r);
float Etot = r * e + 0.5f * r * vel * vel;
for (int i = 0; i < _n; i++)
{
_rho[i] = r;
_rhou[i] = r * vel;
_E[i] = Etot;
}
_ghostLValid = false;
_ghostRValid = false;
}
public int GetCellCount() => _n;
public (double rho, double u, double p) GetInteriorStateLeft()
{
double rho = Math.Max(_rho[0], 1e-12);
double u = _rhou[0] / rho;
double p = PressureScalar(0);
return (rho, u, p);
}
public (double rho, double u, double p) GetInteriorStateRight()
{
double rho = Math.Max(_rho[_n - 1], 1e-12);
double u = _rhou[_n - 1] / rho;
double p = PressureScalar(_n - 1);
return (rho, u, p);
}
public int CellCount => _n;
public double GetCellDensity(int i) => _rho[i];
public double GetCellVelocity(int i)
{
float rho = Math.Max(_rho[i], 1e-12f);
double rho = Math.Max(_rho[i], 1e-12);
return _rhou[i] / rho;
}
public double GetCellPressure(int i)
public double GetCellPressure(int i) => PressureScalar(i);
// ---------- Substepping ----------
public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8)
{
float rho = Math.Max(_rho[i], 1e-12f);
return (_gamma - 1f) * (_E[i] - 0.5f * _rhou[i] * _rhou[i] / rho);
}
public double GetPressureAtFraction(double fraction)
{
int i = (int)(fraction * (_n - 1));
i = Math.Clamp(i, 0, _n - 1);
return GetCellPressure(i);
}
public void SetCellState(int i, double rho, double u, double p)
{
if (i < 0 || i >= _n) return;
float r = (float)rho;
float vel = (float)u;
float pr = (float)p;
_rho[i] = r;
_rhou[i] = r * vel;
float e = pr / ((_gamma - 1f) * r);
_E[i] = r * e + 0.5f * r * vel * vel;
}
public double GetOpenEndMassFlow()
{
if (_bBCType != BoundaryType.OpenEnd && _bBCType != BoundaryType.ZeroPressureOpen)
return 0.0;
int lastCell = _n - 1;
float rho = _rho[lastCell];
float u = _rhou[lastCell] / Math.Max(rho, 1e-12f);
float p = PressureScalar(lastCell);
float c = MathF.Sqrt(_gamma * p / rho);
float uFace = u;
float rhoFace = rho;
float pFace = p;
if (uFace > 0 && uFace < c) // subsonic outflow
{
float s = p / MathF.Pow(rho, _gamma);
float rhoAmb = MathF.Pow(_bAmbientPressure / s, 1f / _gamma);
float cAmb = MathF.Sqrt(_gamma * _bAmbientPressure / rhoAmb);
float J_plus = u + 2f * c / (_gamma - 1f);
float uFaceNew = J_plus - 2f * cAmb / (_gamma - 1f);
if (uFaceNew > 0) uFace = uFaceNew;
if (uFace < 0) uFace = 0;
rhoFace = rhoAmb;
pFace = _bAmbientPressure;
}
return rhoFace * uFace * _area;
}
public double GetAndStoreMassFlowForDerivative()
{
float current = (float)GetOpenEndMassFlow();
double derivative = (current - _lastMassFlow) / _dt;
_lastMassFlow = current;
return derivative;
}
public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8f)
{
float maxW = 0f;
double maxW = 0.0;
for (int i = 0; i < _n; i++)
{
float rho = _rho[i];
float u = MathF.Abs(_rhou[i] / Math.Max(rho, 1e-12f));
float p = PressureScalar(i);
float c = MathF.Sqrt(_gamma * p / Math.Max(rho, 1e-12f));
float local = u + c;
double rho = Math.Max(_rho[i], 1e-12);
double u = Math.Abs(_rhou[i] / rho);
double p = PressureScalar(i);
double c = Math.Sqrt(_gamma * p / rho);
double local = u + c;
if (local > maxW) maxW = local;
}
maxW = Math.Max(maxW, 1e-8f);
return Math.Max(1, (int)Math.Ceiling((float)dtGlobal * maxW / ((float)cflTarget * _dx)));
maxW = Math.Max(maxW, 1e-8);
return Math.Max(1, (int)Math.Ceiling(dtGlobal * maxW / (cflTarget * _dx)));
}
// ==================== MAIN SIMULATION ==================================
// ---------- Main simulation step (per substep) ----------
public void SimulateSingleStep(double dtSub)
{
float dt = (float)dtSub;
// Enforce that both ends have been provided with ghost states
if (!_ghostLValid || !_ghostRValid)
throw new InvalidOperationException("Pipe boundary ghosts not set before SimulateSingleStep.");
double dt = dtSub;
int n = _n;
// --- 1. Left boundary face (index 0) scalar -----------------------
float rhoL = _rho[0];
float uL = _rhou[0] / Math.Max(rhoL, 1e-12f);
float pL = PressureScalar(0);
ComputeLeftBoundaryFlux(rhoL, uL, pL, out _fluxM[0], out _fluxP[0], out _fluxE[0]);
// Left boundary face (index 0)
HLLCFlux(_rhoGhostL, _uGhostL, _pGhostL, _rho[0], _rhou[0] / _rho[0], PressureScalar(0),
out _fluxM[0], out _fluxP[0], out _fluxE[0]);
// --- 2. Internal faces (1 .. n-1) SIMD ---------------------------
int vectorSize = Vector<float>.Count;
int lastSimdFace = n - vectorSize; // highest face index that starts a full vector block
for (int f = 1; f <= lastSimdFace; f += vectorSize)
// Internal faces (1 .. n-1)
for (int f = 1; f < n; f++)
{
SimdInternalFluxBlock(f, vectorSize);
}
// Scalar remainder for faces f .. n-1
for (int f = Math.Max(1, lastSimdFace + 1); f < n; f++)
{
float rhoLi = _rho[f - 1];
float uLi = _rhou[f - 1] / Math.Max(rhoLi, 1e-12f);
float pLi = PressureScalar(f - 1);
float rhoRi = _rho[f];
float uRi = _rhou[f] / Math.Max(rhoRi, 1e-12f);
float pRi = PressureScalar(f);
HLLCFluxScalar(rhoLi, uLi, pLi, rhoRi, uRi, pRi,
out _fluxM[f], out _fluxP[f], out _fluxE[f]);
double rhoL = Math.Max(_rho[f - 1], 1e-12);
double uL = _rhou[f - 1] / rhoL;
double pL = PressureScalar(f - 1);
double rhoR = Math.Max(_rho[f], 1e-12);
double uR = _rhou[f] / rhoR;
double pR = PressureScalar(f);
HLLCFlux(rhoL, uL, pL, rhoR, uR, pR, out _fluxM[f], out _fluxP[f], out _fluxE[f]);
}
// --- 3. Right boundary face (index n) scalar --------------------
float rhoR = _rho[n - 1];
float uR = _rhou[n - 1] / Math.Max(rhoR, 1e-12f);
float pR = PressureScalar(n - 1);
ComputeRightBoundaryFlux(rhoR, uR, pR, out _fluxM[n], out _fluxP[n], out _fluxE[n]);
// Right boundary face (index n)
HLLCFlux(_rho[_n - 1], _rhou[_n - 1] / _rho[_n - 1], PressureScalar(_n - 1),
_rhoGhostR, _uGhostR, _pGhostR,
out _fluxM[n], out _fluxP[n], out _fluxE[n]);
// --- 4. Cell update + damping + energy loss SIMD -----------------
SimdCellUpdate(dt);
}
// Cell update
double dt_dx = dt / _dx;
double coeff = _laminarCoeff * DampingMultiplier;
double relaxRate = EnergyRelaxationRate;
// ==================== PRIVATE SCALAR HELPERS ===========================
private float PressureScalar(int i)
for (int i = 0; i < n; i++)
{
float rho = Math.Max(_rho[i], 1e-12f);
return (_gamma - 1f) * (_E[i] - 0.5f * _rhou[i] * _rhou[i] / rho);
}
double r = _rho[i];
double ru = _rhou[i];
double E = _E[i];
private void ComputeLeftBoundaryFlux(float rhoInt, float uInt, float pInt,
out float fm, out float fp, out float fe)
{
if (_aBCType == BoundaryType.GhostCell && _ghostLSet)
HLLCFluxScalar(_rhoGhostL, _uGhostL, _pGhostL, rhoInt, uInt, pInt, out fm, out fp, out fe);
else if (_aBCType == BoundaryType.OpenEnd)
OpenEndFluxLeft(rhoInt, uInt, pInt, _aAmbientPressure, out fm, out fp, out fe);
else if (_aBCType == BoundaryType.ZeroPressureOpen)
{
float rhoFace = rhoInt;
float uFace = uInt;
float pFace = _aAmbientPressure;
HLLCFluxScalar(rhoFace, uFace, pFace, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
else if (_aBCType == BoundaryType.ClosedEnd)
ClosedEndFlux(rhoInt, uInt, pInt, false, out fm, out fp, out fe);
else
{ fm = 0; fp = pInt; fe = 0; }
}
double dM = _fluxM[i + 1] - _fluxM[i];
double dP = _fluxP[i + 1] - _fluxP[i];
double dE_flux = _fluxE[i + 1] - _fluxE[i];
private void ComputeRightBoundaryFlux(float rhoInt, float uInt, float pInt,
out float fm, out float fp, out float fe)
{
if (_bBCType == BoundaryType.GhostCell && _ghostRSet)
HLLCFluxScalar(rhoInt, uInt, pInt, _rhoGhostR, _uGhostR, _pGhostR, out fm, out fp, out fe);
else if (_bBCType == BoundaryType.OpenEnd)
OpenEndFluxRight(rhoInt, uInt, pInt, _bAmbientPressure, out fm, out fp, out fe);
else if (_bBCType == BoundaryType.ZeroPressureOpen)
{
float rhoFace = rhoInt;
float uFace = uInt;
float pFace = _bAmbientPressure;
HLLCFluxScalar(rhoInt, uInt, pInt, rhoFace, uFace, pFace, out fm, out fp, out fe);
}
else if (_bBCType == BoundaryType.ClosedEnd)
ClosedEndFlux(rhoInt, uInt, pInt, true, out fm, out fp, out fe);
else
{ fm = 0; fp = pInt; fe = 0; }
}
double newR = r - dt_dx * dM;
double newRu = ru - dt_dx * dP;
double newE = E - dt_dx * dE_flux;
// ==================== SCALAR HLLC & BOUNDARY FLUX ======================
private void HLLCFluxScalar(float rL, float uL, float pL, float rR, float uR, float pR,
out float fm, out float fp, out float fe)
{
float cL = MathF.Sqrt(_gamma * pL / Math.Max(rL, 1e-12f));
float cR = MathF.Sqrt(_gamma * pR / Math.Max(rR, 1e-12f));
float EL = pL / ((_gamma - 1f) * rL) + 0.5f * uL * uL;
float ER = pR / ((_gamma - 1f) * rR) + 0.5f * uR * uR;
float SL = Math.Min(uL - cL, uR - cR);
float SR = Math.Max(uL + cL, uR + cR);
float denom = rL * (SL - uL) - rR * (SR - uR);
float Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) / denom;
float FrL_m = rL * uL, FrL_p = rL * uL * uL + pL, FrL_e = (rL * EL + pL) * uL;
float FrR_m = rR * uR, FrR_p = rR * uR * uR + pR, FrR_e = (rR * ER + pR) * uR;
if (SL >= 0) { fm = FrL_m; fp = FrL_p; fe = FrL_e; }
else if (SR <= 0) { fm = FrR_m; fp = FrR_p; fe = FrR_e; }
else if (Ss >= 0)
{
float rsL = rL * (SL - uL) / (SL - Ss);
float ps = pL + rL * (SL - uL) * (Ss - uL);
float EsL = EL + (Ss - uL) * (Ss + pL / (rL * (SL - uL)));
fm = rsL * Ss; fp = rsL * Ss * Ss + ps; fe = (rsL * EsL + ps) * Ss;
}
else
{
float rsR = rR * (SR - uR) / (SR - Ss);
float ps = pR + rR * (SR - uR) * (Ss - uR);
float EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR)));
fm = rsR * Ss; fp = rsR * Ss * Ss + ps; fe = (rsR * EsR + ps) * Ss;
}
}
private void OpenEndFluxLeft(float rhoInt, float uInt, float pInt, float pAmb,
out float fm, out float fp, out float fe)
{
float cInt = MathF.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12f));
if (uInt <= -cInt) // supersonic inflow
{
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1f) * rhoInt) + 0.5f * uInt * uInt) + pInt) * uInt;
return;
}
if (uInt <= 0) // subsonic inflow
{
float T0 = 300f, R = 287f;
float ghostRho = pAmb / (R * T0);
HLLCFluxScalar(ghostRho, 0f, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
return;
}
// subsonic outflow
float s = pInt / MathF.Pow(rhoInt, _gamma);
float ghostRho2 = MathF.Pow(pAmb / s, 1f / _gamma);
float cGhost = MathF.Sqrt(_gamma * pAmb / ghostRho2);
float J_minus = uInt - 2f * cInt / (_gamma - 1f);
float uGhost = J_minus + 2f * cGhost / (_gamma - 1f);
if (uGhost < 0) uGhost = 0;
HLLCFluxScalar(ghostRho2, uGhost, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
private void OpenEndFluxRight(float rhoInt, float uInt, float pInt, float pAmb,
out float fm, out float fp, out float fe)
{
float cInt = MathF.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12f));
if (uInt >= cInt) // supersonic outflow
{
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1f) * rhoInt) + 0.5f * uInt * uInt) + pInt) * uInt;
return;
}
if (uInt >= 0) // subsonic outflow
{
float s = pInt / MathF.Pow(rhoInt, _gamma);
float ghostRho = MathF.Pow(pAmb / s, 1f / _gamma);
float cGhost = MathF.Sqrt(_gamma * pAmb / ghostRho);
float J_plus = uInt + 2f * cInt / (_gamma - 1f);
float uGhost = J_plus - 2f * cGhost / (_gamma - 1f);
if (uGhost > 0) uGhost = 0;
HLLCFluxScalar(rhoInt, uInt, pInt, ghostRho, uGhost, pAmb, out fm, out fp, out fe);
return;
}
// subsonic inflow
float T0 = 300f, R = 287f;
float ghostRho2 = pAmb / (R * T0);
HLLCFluxScalar(rhoInt, uInt, pInt, ghostRho2, 0f, pAmb, out fm, out fp, out fe);
}
private void ClosedEndFlux(float rhoInt, float uInt, float pInt, bool isRight,
out float fm, out float fp, out float fe)
{
float rhoGhost = rhoInt, pGhost = pInt, uGhost = -uInt;
if (isRight)
HLLCFluxScalar(rhoInt, uInt, pInt, rhoGhost, uGhost, pGhost, out fm, out fp, out fe);
else
HLLCFluxScalar(rhoGhost, uGhost, pGhost, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
// ==================== SIMD INTERNAL FACE ROUTINE ========================
private void SimdInternalFluxBlock(int startFace, int count)
{
int leftIdx = startFace - 1;
int rightIdx = startFace;
Vector<float> rL = new Vector<float>(_rho, leftIdx);
Vector<float> ruL = new Vector<float>(_rhou, leftIdx);
Vector<float> EL = new Vector<float>(_E, leftIdx);
Vector<float> rR = new Vector<float>(_rho, rightIdx);
Vector<float> ruR = new Vector<float>(_rhou, rightIdx);
Vector<float> ER = new Vector<float>(_E, rightIdx);
Vector<float> uL = ruL / rL;
Vector<float> uR = ruR / rR;
Vector<float> half = new Vector<float>(0.5f);
Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f);
Vector<float> gammaVec = new Vector<float>(_gamma);
Vector<float> pL = gammaMinus1 * (EL - half * ruL * ruL / rL);
Vector<float> pR = gammaMinus1 * (ER - half * ruR * ruR / rR);
Vector<float> cL = Vector.SquareRoot(gammaVec * pL / rL);
Vector<float> cR = Vector.SquareRoot(gammaVec * pR / rR);
Vector<float> SL = Vector.Min(uL - cL, uR - cR);
Vector<float> SR = Vector.Max(uL + cL, uR + cR);
Vector<float> num = (pR - pL) + rL * uL * (SL - uL) - rR * uR * (SR - uR);
Vector<float> den = rL * (SL - uL) - rR * (SR - uR);
Vector<float> Ss = num / den;
Vector<float> eL = EL / rL;
Vector<float> eR = ER / rR;
// Left flux
Vector<float> Fm_L = ruL;
Vector<float> Fp_L = ruL * uL + pL;
Vector<float> Fe_L = (EL + pL) * uL;
// Right flux
Vector<float> Fm_R = ruR;
Vector<float> Fp_R = ruR * uR + pR;
Vector<float> Fe_R = (ER + pR) * uR;
// Starleft fluxes
Vector<float> diffL = SL - uL;
Vector<float> dL_den = SL - Ss;
Vector<float> rsL = rL * diffL / dL_den;
Vector<float> psSL = pL + rL * diffL * (Ss - uL);
Vector<float> EsL = eL + (Ss - uL) * (Ss + pL / (rL * diffL));
Vector<float> Fm_starL = rsL * Ss;
Vector<float> Fp_starL = rsL * Ss * Ss + psSL;
Vector<float> Fe_starL = (rsL * EsL + psSL) * Ss;
// Starright fluxes
Vector<float> diffR = SR - uR;
Vector<float> dR_den = SR - Ss;
Vector<float> rsR = rR * diffR / dR_den;
Vector<float> psSR = pR + rR * diffR * (Ss - uR);
Vector<float> EsR = eR + (Ss - uR) * (Ss + pR / (rR * diffR));
Vector<float> Fm_starR = rsR * Ss;
Vector<float> Fp_starR = rsR * Ss * Ss + psSR;
Vector<float> Fe_starR = (rsR * EsR + psSR) * Ss;
var maskSLge0 = Vector.GreaterThanOrEqual(SL, Vector<float>.Zero);
var maskSRle0 = Vector.LessThanOrEqual(SR, Vector<float>.Zero);
var maskMiddle = ~(maskSLge0 | maskSRle0);
var maskStarL = maskMiddle & Vector.GreaterThanOrEqual(Ss, Vector<float>.Zero);
var maskStarR = maskMiddle & Vector.LessThan(Ss, Vector<float>.Zero);
Vector<float> fm = Vector.ConditionalSelect(maskSLge0, Fm_L,
Vector.ConditionalSelect(maskSRle0, Fm_R,
Vector.ConditionalSelect(maskStarL, Fm_starL,
Vector.ConditionalSelect(maskStarR, Fm_starR, Vector<float>.Zero))));
Vector<float> fp = Vector.ConditionalSelect(maskSLge0, Fp_L,
Vector.ConditionalSelect(maskSRle0, Fp_R,
Vector.ConditionalSelect(maskStarL, Fp_starL,
Vector.ConditionalSelect(maskStarR, Fp_starR, Vector<float>.Zero))));
Vector<float> fe = Vector.ConditionalSelect(maskSLge0, Fe_L,
Vector.ConditionalSelect(maskSRle0, Fe_R,
Vector.ConditionalSelect(maskStarL, Fe_starL,
Vector.ConditionalSelect(maskStarR, Fe_starR, Vector<float>.Zero))));
fm.CopyTo(_fluxM, startFace);
fp.CopyTo(_fluxP, startFace);
fe.CopyTo(_fluxE, startFace);
}
// ==================== SIMD CELL UPDATE + DAMPING + ENERGY LOSS =========
private void SimdCellUpdate(float dt)
{
float dt_dx = dt / _dx;
Vector<float> vDtDx = new Vector<float>(dt_dx);
float coeff = _laminarCoeff * (float)DampingMultiplier;
Vector<float> vCoeff = new Vector<float>(coeff);
Vector<float> vDt = new Vector<float>(dt);
int vectorSize = Vector<float>.Count;
int n = _n;
int lastSimdCell = n - vectorSize;
// Predefined constants used in clamping
Vector<float> half = new Vector<float>(0.5f);
Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f);
Vector<float> ambientEnergyVec = new Vector<float>(_ambientEnergyReference);
Vector<float> energyRelaxRateVec = new Vector<float>(EnergyRelaxationRate);
for (int i = 0; i <= lastSimdCell; i += vectorSize)
{
// Load conserved
Vector<float> r = new Vector<float>(_rho, i);
Vector<float> ru = new Vector<float>(_rhou, i);
Vector<float> E = new Vector<float>(_E, i);
// Load fluxes at faces i (left) and i+1 (right)
Vector<float> flxM_L = new Vector<float>(_fluxM, i);
Vector<float> flxM_R = new Vector<float>(_fluxM, i + 1);
Vector<float> flxP_L = new Vector<float>(_fluxP, i);
Vector<float> flxP_R = new Vector<float>(_fluxP, i + 1);
Vector<float> flxE_L = new Vector<float>(_fluxE, i);
Vector<float> flxE_R = new Vector<float>(_fluxE, i + 1);
// Update conserved: Q_new = Q - dt/dx * (flux_right - flux_left)
Vector<float> newR = r - vDtDx * (flxM_R - flxM_L);
Vector<float> newRu = ru - vDtDx * (flxP_R - flxP_L);
Vector<float> newE = E - vDtDx * (flxE_R - flxE_L);
// Damping
Vector<float> dampingFactor = Vector.Exp(-vCoeff / r * vDt);
// Wall friction damping (laminar)
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
newRu *= dampingFactor;
// Energy loss (Newton cooling toward ambient)
Vector<float> relaxFactor = Vector.Exp(-energyRelaxRateVec * vDt);
newE = ambientEnergyVec + (newE - ambientEnergyVec) * relaxFactor;
// Clamp density
newR = Vector.Max(newR, new Vector<float>(1e-12f));
// Enforce minimal pressure (100 Pa) -> minimal energy
Vector<float> kinE = half * newRu * newRu / newR;
Vector<float> eMin = new Vector<float>(100f) / gammaMinus1 + kinE;
newE = Vector.Max(newE, eMin);
newR.CopyTo(_rho, i);
newRu.CopyTo(_rhou, i);
newE.CopyTo(_E, i);
}
// Scalar remainder
float relaxRate = EnergyRelaxationRate;
for (int i = Math.Max(0, lastSimdCell + 1); i < n; i++)
{
float r = _rho[i];
float ru = _rhou[i];
float E = _E[i];
float dM = _fluxM[i + 1] - _fluxM[i];
float dP = _fluxP[i + 1] - _fluxP[i];
float dE_flux = _fluxE[i + 1] - _fluxE[i];
float newR = r - dt_dx * dM;
float newRu = ru - dt_dx * dP;
float newE = E - dt_dx * dE_flux;
// Damping
float dampingFactor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
newRu *= dampingFactor;
// Energy loss
float relaxFactor = MathF.Exp(-relaxRate * dt);
// Newtonian cooling toward ambient energy
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
// Clamps
if (newR < 1e-12f) newR = 1e-12f;
float kin = 0.5f * newRu * newRu / newR;
float emin = 100f / (_gamma - 1f) + kin;
if (newE < emin) newE = emin;
// Clamps minimum density 1e-12, minimum pressure 100 Pa
newR = Math.Max(newR, 1e-12);
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
double eMin = 100.0 / (_gamma - 1.0) + kin;
newE = Math.Max(newE, eMin);
_rho[i] = newR;
_rhou[i] = newRu;
_E[i] = newE;
}
// Update port states to reflect the current interior state (for audio / monitoring)
(double rhoA, double uA, double pA) = GetInteriorStateLeft();
PortA.Pressure = pA;
PortA.Density = rhoA;
PortA.Temperature = pA / (rhoA * 287.0);
PortA.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pA / rhoA;
(double rhoB, double uB, double pB) = GetInteriorStateRight();
PortB.Pressure = pB;
PortB.Density = rhoB;
PortB.Temperature = pB / (rhoB * 287.0);
PortB.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pB / rhoB;
}
// ---------- Private helpers ----------
private double PressureScalar(int i)
{
double rho = Math.Max(_rho[i], 1e-12);
return (_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho);
}
/// <summary>
/// HLLC approximate Riemann solver (Toro, 1997).
/// Computes the numerical flux at a face given left and right states.
/// </summary>
private void HLLCFlux(double rL, double uL, double pL, double rR, double uR, double pR,
out double fm, out double fp, out double fe)
{
double cL = Math.Sqrt(_gamma * pL / rL);
double cR = Math.Sqrt(_gamma * pR / rR);
double EL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL; // specific total energy
double ER = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
// Wave speed estimates (Davis, 1988)
double SL = Math.Min(uL - cL, uR - cR);
double SR = Math.Max(uL + cL, uR + cR);
double denom = rL * (SL - uL) - rR * (SR - uR);
double Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) / denom;
double Fm_L = rL * uL;
double Fp_L = rL * uL * uL + pL;
double Fe_L = (rL * EL + pL) * uL;
double Fm_R = rR * uR;
double Fp_R = rR * uR * uR + pR;
double Fe_R = (rR * ER + pR) * uR;
if (SL >= 0) { fm = Fm_L; fp = Fp_L; fe = Fe_L; }
else if (SR <= 0) { fm = Fm_R; fp = Fp_R; fe = Fe_R; }
else if (Ss >= 0)
{
double rsL = rL * (SL - uL) / (SL - Ss);
double ps = pL + rL * (SL - uL) * (Ss - uL);
double EsL = EL + (Ss - uL) * (Ss + pL / (rL * (SL - uL)));
fm = rsL * Ss;
fp = rsL * Ss * Ss + ps;
fe = (rsL * EsL + ps) * Ss;
}
else
{
double rsR = rR * (SR - uR) / (SR - Ss);
double ps = pR + rR * (SR - uR) * (Ss - uR);
double EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR)));
fm = rsR * Ss;
fp = rsR * Ss * Ss + ps;
fe = (rsR * EsR + ps) * Ss;
}
}
/// <summary>Initialise all cells to a uniform state (rho, u, p).</summary>
public void SetUniformState(double rho, double u, double p)
{
double e = p / ((_gamma - 1.0) * rho);
double E = rho * e + 0.5 * rho * u * u;
for (int i = 0; i < _n; i++)
{
_rho[i] = rho;
_rhou[i] = rho * u;
_E[i] = E;
}
}
}
}

View File

@@ -1,69 +1,109 @@
using System;
using System.Collections.Generic;
using FluidSim.Interfaces;
namespace FluidSim.Components
{
public class Volume0D
/// <summary>
/// Zerodimensional control volume with arbitrary number of ports.
/// Integrates mass and energy fluxes from all ports.
/// Safeguards keep a tiny amount of gas to avoid negative states.
/// </summary>
public class Volume0D : IComponent
{
public double Mass { get; set; }
public double InternalEnergy { get; set; }
public List<Port> Ports { get; } = new List<Port>();
public double Mass { get; private set; }
public double InternalEnergy { get; private set; }
public double Volume { get; set; }
public double Dvdt { get; set; }
public double Gamma { get; set; } = 1.4;
public double GasConstant { get; set; } = 287.0;
public double Volume { get; set; }
public double Dvdt { get; set; }
private double _dt;
// Ambient pressure used for emergency refill default 101325 Pa
public double AmbientPressure { get; set; } = 101325.0;
// Derived quantities
public double Density => Mass / Math.Max(Volume, 1e-12);
public double Pressure => (Gamma - 1.0) * InternalEnergy / Math.Max(Volume, 1e-12);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Math.Max(Density, 1e-12);
public double MassFlowRateIn { get; set; }
public double SpecificEnthalpyIn { get; set; }
public Volume0D(double initialVolume, double initialPressure,
double initialTemperature, int sampleRate,
double gasConstant = 287.0, double gamma = 1.4)
double initialTemperature, double gasConstant = 287.0, double gamma = 1.4)
{
GasConstant = gasConstant;
Gamma = gamma;
Volume = initialVolume;
Dvdt = 0.0;
_dt = 1.0 / sampleRate;
double rho0 = initialPressure / (GasConstant * initialTemperature);
Mass = rho0 * Volume;
InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0);
}
public void Integrate(double dtOverride)
/// <summary>Add a new port and initialise it to the volume's current state.</summary>
public Port CreatePort()
{
double dm = MassFlowRateIn * dtOverride;
double dE = (MassFlowRateIn * SpecificEnthalpyIn) * dtOverride - Pressure * Dvdt * dtOverride;
var port = new Port { Owner = this };
// Set the port state immediately to avoid a mismatch before the first integration
port.Pressure = Pressure;
port.Density = Density;
port.Temperature = Temperature;
port.SpecificEnthalpy = SpecificEnthalpy;
Ports.Add(port);
return port;
}
/// <summary>
/// Integrate over dt using the MassFlowRate and SpecificEnthalpy
/// that have been set on each port during the coupling resolution phase.
/// </summary>
public void UpdateState(double dt)
{
double totalMdot = 0.0;
double totalEdot = 0.0;
foreach (var port in Ports)
{
totalMdot += port.MassFlowRate;
// mdot * h gives energy flow: positive mdot = inflow, negative = outflow
totalEdot += port.MassFlowRate * port.SpecificEnthalpy;
}
double dm = totalMdot * dt;
double dE = totalEdot * dt - Pressure * Dvdt * dt; // piston work
Mass += dm;
InternalEnergy += dE;
// ---- ABSOLUTE SAFEGUARD: keep at least 1 µg of gas at ambient pressure ----
double minMass = 1e-9;
// Safeguards: keep at least 1 µg of gas at a small pressure
double V = Math.Max(Volume, 1e-12);
if (Mass < minMass)
if (Mass < 1e-9)
{
Mass = minMass;
InternalEnergy = 5000.0 * V / (Gamma - 1.0); // 0.05 bar, not ambient
Mass = 1e-9;
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
}
else if (InternalEnergy < 0.0)
{
InternalEnergy = 101325.0 * V / (Gamma - 1.0);
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
}
// Final nonnegative clamp
if (Mass < 0.0) Mass = 0.0;
if (InternalEnergy < 0.0) InternalEnergy = 0.0;
// Final nonnegative clamps (should not be needed after above)
if (Mass < 0.0) Mass = 1e-9;
if (InternalEnergy < 0.0) InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
// Push updated state back to all ports
double p = Pressure, rho = Density, T = Temperature, h = SpecificEnthalpy;
foreach (var port in Ports)
{
port.Pressure = p;
port.Density = rho;
port.Temperature = T;
port.SpecificEnthalpy = h; // will be overwritten by couplings for inflow, but this is the default
}
}
public void Integrate() => Integrate(_dt);
IReadOnlyList<Port> IComponent.Ports => Ports;
}
}

41
Core/IsentropicOrifice.cs Normal file
View File

@@ -0,0 +1,41 @@
using System;
namespace FluidSim.Core
{
/// <summary>
/// Compressible flow through an orifice, modelled as an isentropic nozzle.
/// Supports choked and unchoked flow, forward and reverse.
/// </summary>
public static class IsentropicOrifice
{
/// <summary>
/// Compute mass flow and face primitive state for an orifice.
/// </summary>
/// <param name="pUp">Upstream stagnation pressure (Pa).</param>
/// <param name="rhoUp">Upstream stagnation density (kg/m³).</param>
/// <param name="gamma">Ratio of specific heats.</param>
/// <param name="R">Specific gas constant (J/kg·K).</param>
/// <param name="pDown">Downstream static pressure (Pa).</param>
/// <param name="area">Effective orifice area (m²).</param>
/// <param name="Cd">Discharge coefficient (default 0.62).</param>
/// <param name="mdot">Mass flow rate (kg/s), positive from upstream to downstream.</param>
/// <param name="rhoFace">Face density (kg/m³).</param>
/// <param name="uFace">Face velocity (m/s).</param>
/// <param name="pFace">Face pressure (Pa).</param>
public static void Compute(double pUp, double rhoUp, double TUp, double gamma, double R,
double pDown, double area, double Cd,
out double mdot, out double rhoFace, out double uFace, out double pFace)
{
// mdot is positive from upstream to downstream.
double pr = Math.Max(pDown / pUp, 1e-6);
double prCrit = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
if (pr < prCrit) pr = prCrit;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -(gamma - 1.0) / gamma) - 1.0));
uFace = M * Math.Sqrt(gamma * R * TUp);
rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma);
pFace = pUp * pr;
mdot = rhoFace * uFace * area * Cd; // mass flow from upstream to downstream
}
}
}

228
Core/Junction.cs Normal file
View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using FluidSim.Components;
namespace FluidSim.Core
{
/// <summary>
/// Zerodimensional junction connecting multiple pipe ends.
/// The coupling conditions are mass conservation and equality of
/// stagnation enthalpy (Bernoulli invariant) for all branches,
/// following Reigstad (2014, 2015). A rootfinding method (Brent)
/// solves for the common junction pressure.
/// </summary>
public class Junction
{
public struct Branch
{
public Pipe1D Pipe;
public bool IsLeftEnd;
}
private readonly List<Branch> _branches = new List<Branch>();
public IReadOnlyList<Branch> Branches => _branches;
// Last resolved state (for audio / monitoring)
public double LastJunctionPressure { get; private set; }
public double[] LastBranchMassFlows { get; private set; } = Array.Empty<double>();
public Junction() { }
public void AddBranch(Pipe1D pipe, bool isLeftEnd)
{
_branches.Add(new Branch { Pipe = pipe, IsLeftEnd = isLeftEnd });
}
/// <summary>
/// Solve the junction for one substep. Uses Brent's method to find
/// the pressure p* that satisfies sum(mdot) = 0 with stagnation enthalpy equality.
/// </summary>
public void Resolve(double dtSub)
{
int nb = _branches.Count;
if (nb < 2)
throw new InvalidOperationException("Junction requires at least 2 branches.");
// Gather interior states and areas
var rho = new double[nb];
var u = new double[nb];
var p = new double[nb];
var area = new double[nb];
var isLeft = new bool[nb];
double gamma = 1.4;
double pMin = double.MaxValue, pMax = double.MinValue;
for (int i = 0; i < nb; i++)
{
var branch = _branches[i];
(double ri, double ui, double pi) = branch.IsLeftEnd
? branch.Pipe.GetInteriorStateLeft()
: branch.Pipe.GetInteriorStateRight();
rho[i] = ri; u[i] = ui; p[i] = pi;
area[i] = branch.Pipe.Area;
isLeft[i] = branch.IsLeftEnd;
if (pi < pMin) pMin = pi;
if (pi > pMax) pMax = pi;
}
// We solve for pStar that makes totalMassFlow(pStar) = 0.
// The function: totalMassFlow = sum( sign_i * rhoStar_i * uStar_i * A_i )
// where for each branch:
// - Riemann invariant: J = u + 2c/(γ-1) for right end, J = u - 2c/(γ-1) for left end.
// - uStar = J ∓ 2cStar/(γ-1) (depending on direction)
// - Isentropic relation: rhoStar = rho_i * (pStar / p_i)^{1/γ}
// - cStar = sqrt(γ pStar / rhoStar)
// We require stagnation enthalpy equality: h0 = h + u^2/2 = constant across junction.
// Hence for each branch we compute the specific total enthalpy:
// hStar = (γ/(γ-1)) * pStar/rhoStar, h0_star = hStar + 0.5 uStar^2.
// We enforce that all h0_star are equal. Mass conservation then determines pStar.
// This is a scalar rootfinding problem.
// Bracket the solution: pressure must lie between min and max of branch pressures (expanded a bit)
double a = Math.Max(100.0, pMin * 0.1);
double b = Math.Min(1e7, pMax * 10.0);
if (a >= b) { a = 100.0; b = 1e7; }
Func<double, double> f = pStar =>
{
double totalMdot = 0.0;
double h0Ref = 0.0;
bool first = true;
for (int i = 0; i < nb; i++)
{
double g = gamma;
double gm1 = g - 1.0;
double rhoI = rho[i], uI = u[i], pI = p[i];
double cI = Math.Sqrt(g * pI / rhoI);
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
double pratio = Math.Max(pStar / pI, 1e-6);
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
double cStar = Math.Sqrt(g * pStar / rhoStar);
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
double hStar = (g / gm1) * pStar / rhoStar;
double h0 = hStar + 0.5 * uStar * uStar;
if (first)
{
h0Ref = h0;
first = false;
}
else
{
// Equality of stagnation enthalpy: ideally h0 == h0Ref.
// We incorporate a penalty to enforce this.
}
// Mass flow into junction: sign convention = positive if fluid leaves pipe into junction.
double sign = isLeft[i] ? -1.0 : 1.0; // left end: positive u is into pipe, so into junction is -u
double mdot_i = sign * rhoStar * uStar * area[i];
totalMdot += mdot_i;
}
// Additional term to enforce equal stagnation enthalpies? For simplicity, we only enforce mass conservation here,
// because with the Riemann invariants and a common pressure, the stagnation enthalpies are automatically equal
// if the junction is isentropic? Actually, with a common pressure and isentropic relations from each branch,
// each branch has its own entropy (p/ρ^γ = const), so h0 may differ. The correct condition is mass conservation + equality of h0.
// To solve both, we would need to vary pStar and a common h0? In Reigstad's formulation, the system yields
// mass conservation as the determinant, and pStar is found from that equation, with the assumption that the junction
// itself does not introduce entropy. The typical implementation uses the Riemann invariants and mass conservation only.
// We'll stick to mass conservation for now.
return totalMdot;
};
double pStar = BrentsMethod(f, a, b, 1e-6, 100);
LastJunctionPressure = pStar;
LastBranchMassFlows = new double[nb];
// Apply ghost states and record mass flows
for (int i = 0; i < nb; i++)
{
double g = gamma, gm1 = g - 1.0;
double rhoI = rho[i], uI = u[i], pI = p[i];
double cI = Math.Sqrt(g * pI / rhoI);
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
double pratio = Math.Max(pStar / pI, 1e-6);
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
double cStar = Math.Sqrt(g * pStar / rhoStar);
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
double sign = isLeft[i] ? -1.0 : 1.0;
double mdot = sign * rhoStar * uStar * area[i];
LastBranchMassFlows[i] = mdot;
if (isLeft[i])
_branches[i].Pipe.SetGhostLeft(rhoStar, uStar, pStar);
else
_branches[i].Pipe.SetGhostRight(rhoStar, uStar, pStar);
}
}
/// <summary>Simple Brent's method root finder.</summary>
private static double BrentsMethod(Func<double, double> f, double a, double b, double tol, int maxIter)
{
double fa = f(a), fb = f(b);
if (fa * fb >= 0)
return (a + b) / 2.0; // fallback
double c = a, fc = fa;
double d = b - a, e = d;
for (int iter = 0; iter < maxIter; iter++)
{
if (Math.Abs(fc) < Math.Abs(fb))
{
a = b; b = c; c = a;
fa = fb; fb = fc; fc = fa;
}
double tol1 = 2 * double.Epsilon * Math.Abs(b) + 0.5 * tol;
double xm = 0.5 * (c - b);
if (Math.Abs(xm) <= tol1 || fb == 0.0)
return b;
if (Math.Abs(e) >= tol1 && Math.Abs(fa) > Math.Abs(fb))
{
double s = fb / fa;
double p, q;
if (a == c)
{
p = 2.0 * xm * s;
q = 1.0 - s;
}
else
{
q = fa / fc;
double r = fb / fc;
p = s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0));
q = (q - 1.0) * (r - 1.0) * (s - 1.0);
}
if (p > 0) q = -q; else p = -p;
s = e; e = d;
if (2.0 * p < 3.0 * xm * q - Math.Abs(tol1 * q) && p < Math.Abs(0.5 * s * q))
{
d = p / q;
}
else
{
d = xm; e = d;
}
}
else
{
d = xm; e = d;
}
a = b; fa = fb;
if (Math.Abs(d) > tol1)
b += d;
else
b += Math.Sign(xm) * tol1;
fb = f(b);
}
return b;
}
}
}

View File

@@ -1,72 +0,0 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
public static class NozzleFlow
{
public static void Compute(Volume0D vol, double area, double downstreamPressure,
out double massFlow, out double rhoFace, out double uFace, out double pFace,
double gamma = 1.4)
{
massFlow = 0.0;
rhoFace = 0.0;
uFace = 0.0;
pFace = 0.0;
if (vol == null || vol.Mass <= 0 || vol.Volume <= 0)
return;
double p0 = vol.Pressure;
double T0 = vol.Temperature;
double R = vol.GasConstant;
double rho0 = vol.Density;
if (double.IsNaN(p0) || double.IsNaN(T0) || double.IsNaN(rho0) ||
p0 <= 0 || T0 <= 0 || rho0 <= 0)
return;
double pr = downstreamPressure / p0;
double choked = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
// If pr > 1, flow is INTO the cylinder (reverse), so we swap the roles.
bool reverse = (pr > 1.0);
if (reverse)
{
// Treat the cylinder as the downstream, the pipe as the upstream.
double p_up = downstreamPressure;
double T_up = 300.0; // pipe temperature (ambient)
double rho_up = downstreamPressure / (R * T_up);
double pr_rev = p0 / p_up; // now cylinder / pipe
if (pr_rev < choked) pr_rev = choked;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr_rev, -(gamma - 1.0) / gamma) - 1.0));
if (double.IsNaN(M)) return;
// Flow from pipe INTO cylinder (positive mass flow into volume)
uFace = M * Math.Sqrt(gamma * R * T_up);
rhoFace = rho_up * Math.Pow(pr_rev, 1.0 / gamma);
pFace = p_up * pr_rev;
massFlow = rhoFace * uFace * area;
// massFlow is positive = into cylinder
}
else
{
// Normal flow out of cylinder
if (pr < choked) pr = choked;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -(gamma - 1.0) / gamma) - 1.0));
if (double.IsNaN(M)) return;
uFace = M * Math.Sqrt(gamma * R * T0);
rhoFace = rho0 * Math.Pow(pr, 1.0 / gamma);
pFace = p0 * pr;
massFlow = -rhoFace * uFace * area; // negative = out of cylinder
}
if (double.IsNaN(massFlow) || double.IsInfinity(massFlow))
massFlow = 0.0;
}
}
}

123
Core/OpenEndLink.cs Normal file
View File

@@ -0,0 +1,123 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
/// <summary>
/// Characteristic openend boundary condition.
/// For subsonic outflow the outgoing Riemann invariant is conserved,
/// and the ghost pressure is set to the prescribed ambient value.
/// </summary>
public class OpenEndLink
{
public Pipe1D Pipe { get; }
public bool IsLeftEnd { get; }
public double AmbientPressure { get; set; } = 101325.0;
public double Gamma { get; set; } = 1.4;
// Last resolved state (for audio / monitoring)
public double LastMassFlowRate { get; private set; }
public double LastFaceDensity { get; private set; }
public double LastFaceVelocity { get; private set; }
public double LastFacePressure { get; private set; }
public OpenEndLink(Pipe1D pipe, bool isLeftEnd)
{
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
IsLeftEnd = isLeftEnd;
}
/// <summary>
/// Compute the ghost state and mass flow for one substep.
/// </summary>
public void Resolve(double dtSub)
{
(double rhoInt, double uInt, double pInt) = IsLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
double gamma = Gamma;
double gm1 = gamma - 1.0;
double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12));
double pAmb = AmbientPressure;
double rhoGhost, uGhost, pGhost;
double mdot;
if (IsLeftEnd)
{
// Left end: outgoing invariant is J- = u - 2c/(γ-1)
double J_minus = uInt - 2.0 * cInt / gm1;
if (uInt <= -cInt) // supersonic inflow (all info from outside)
{
// Simple reservoir model use ambient density and temperature 300 K
rhoGhost = pAmb / (287.0 * 300.0);
uGhost = uInt; // keep interior velocity (should be supersonic inward)
pGhost = pAmb;
}
else if (uInt < 0) // subsonic inflow
{
double rhoAmb = pAmb / (287.0 * 300.0);
double cAmb = Math.Sqrt(gamma * pAmb / rhoAmb);
uGhost = J_minus + 2.0 * cAmb / gm1;
rhoGhost = rhoAmb;
pGhost = pAmb;
}
else // subsonic outflow (uInt >= 0)
{
double s = pInt / Math.Pow(rhoInt, gamma);
rhoGhost = Math.Pow(pAmb / s, 1.0 / gamma);
double cGhost = Math.Sqrt(gamma * pAmb / rhoGhost);
uGhost = J_minus + 2.0 * cGhost / gm1;
if (uGhost < 0) uGhost = 0;
pGhost = pAmb;
}
}
else // Right end
{
// Right end: outgoing invariant is J+ = u + 2c/(γ-1)
double J_plus = uInt + 2.0 * cInt / gm1;
if (uInt >= cInt) // supersonic outflow
{
rhoGhost = rhoInt;
uGhost = uInt;
pGhost = pInt;
}
else if (uInt >= 0) // subsonic outflow
{
double s = pInt / Math.Pow(rhoInt, gamma);
rhoGhost = Math.Pow(pAmb / s, 1.0 / gamma);
double cGhost = Math.Sqrt(gamma * pAmb / rhoGhost);
uGhost = J_plus - 2.0 * cGhost / gm1;
if (uGhost < 0) uGhost = 0;
pGhost = pAmb;
}
else // subsonic inflow (uInt < 0)
{
double rhoAmb = pAmb / (287.0 * 300.0);
double cAmb = Math.Sqrt(gamma * pAmb / rhoAmb);
uGhost = J_plus - 2.0 * cAmb / gm1;
rhoGhost = rhoAmb;
pGhost = pAmb;
}
}
// Apply ghost to pipe
if (IsLeftEnd)
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost);
else
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost);
// Mass flow (positive = out of pipe)
double area = Pipe.Area;
mdot = rhoGhost * uGhost * area;
if (IsLeftEnd) mdot = -mdot; // positive u into pipe, so out of pipe is negative u
LastMassFlowRate = mdot;
LastFaceDensity = rhoGhost;
LastFaceVelocity = uGhost;
LastFacePressure = pGhost;
}
}
}

View File

@@ -1,100 +0,0 @@
using System;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
public static class OrificeBoundary
{
public static double MassFlow(double pA, double rhoA, double pB, double rhoB,
Connection conn)
{
if (double.IsNaN(pA) || double.IsNaN(rhoA) || double.IsNaN(pB) || double.IsNaN(rhoB) ||
double.IsInfinity(pA) || double.IsInfinity(rhoA) || double.IsInfinity(pB) || double.IsInfinity(rhoB) ||
pA <= 0 || rhoA <= 0 || pB <= 0 || rhoB <= 0)
return 0.0;
double dp = pA - pB;
double sign = Math.Sign(dp);
double absDp = Math.Abs(dp);
double rhoUp = dp >= 0 ? rhoA : rhoB;
double pUp = dp >= 0 ? pA : pB;
double pDown = dp >= 0 ? pB : pA;
double delta = 1e-6 * pUp;
if (absDp < delta)
{
double k = conn.DischargeCoefficient * conn.Area * Math.Sqrt(2 * rhoUp / delta);
return k * dp;
}
else
{
double pr = pDown / pUp;
double choked = Math.Pow(2.0 / (conn.Gamma + 1.0), conn.Gamma / (conn.Gamma - 1.0));
if (pr < choked)
{
double term = Math.Sqrt(conn.Gamma *
Math.Pow(2.0 / (conn.Gamma + 1.0), (conn.Gamma + 1.0) / (conn.Gamma - 1.0)));
double flow = conn.DischargeCoefficient * conn.Area *
Math.Sqrt(rhoUp * pUp) * term;
return sign * flow;
}
else
{
double ex = 1.0 - Math.Pow(pr, (conn.Gamma - 1.0) / conn.Gamma);
double flow = conn.DischargeCoefficient * conn.Area *
Math.Sqrt(2.0 * rhoUp * pUp * (conn.Gamma / (conn.Gamma - 1.0)) *
pr * pr * ex);
return sign * flow;
}
}
}
public static void PipeVolumeFlux(double pPipe, double rhoPipe, double uPipe,
double pVol, double rhoVol, double uVol,
Connection conn, double pipeArea,
bool isLeftBoundary,
out double massFlux, out double momFlux, out double energyFlux)
{
// ----- Compute STAGNATION pressures -----
double pStagPipe = pPipe + 0.5 * rhoPipe * uPipe * uPipe;
double pStagVol = pVol + 0.5 * rhoVol * uVol * uVol; // uVol is always 0 for your volumes
// Mass flow driven by stagnation pressure difference (positive = pipe→volume)
double mdot = MassFlow(pStagPipe, rhoPipe, pStagVol, rhoVol, conn);
// Limit mass flow to the amount that can leave/enter the pipe cell
double maxMdot = rhoPipe * pipeArea * 343.0;
if (Math.Abs(mdot) > maxMdot) mdot = Math.Sign(mdot) * maxMdot;
bool flowLeavesPipe = mdot > 0; // pipe → volume
double uFace, pFace, rhoFace;
double massFluxPerArea;
if (isLeftBoundary)
{
massFluxPerArea = -mdot / pipeArea;
if (flowLeavesPipe)
{ uFace = uPipe; pFace = pPipe; rhoFace = rhoPipe; }
else
{ uFace = uVol; pFace = pVol; rhoFace = rhoVol; }
}
else // right boundary
{
massFluxPerArea = mdot / pipeArea;
if (flowLeavesPipe)
{ uFace = uPipe; pFace = pPipe; rhoFace = rhoPipe; }
else
{ uFace = uVol; pFace = pVol; rhoFace = rhoVol; }
}
// Total enthalpy of the injected fluid
double specificEnthalpy = (1.4 / (1.4 - 1.0)) * pFace / Math.Max(rhoFace, 1e-12);
double totalEnthalpy = specificEnthalpy + 0.5 * uFace * uFace;
massFlux = massFluxPerArea;
momFlux = massFluxPerArea * uFace + pFace;
energyFlux = massFluxPerArea * totalEnthalpy;
}
}
}

140
Core/OrificeLink.cs Normal file
View File

@@ -0,0 +1,140 @@
using System;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
/// <summary>
/// Connects a port (volume or atmosphere) to one end of a pipe via an orifice.
/// The area can be dynamic (Func<double>).
/// </summary>
public class OrificeLink
{
public Port VolumePort { get; }
public Pipe1D Pipe { get; }
public bool IsPipeLeftEnd { get; }
public Func<double> AreaProvider { get; set; }
public double DischargeCoefficient { get; set; } = 0.62;
public double Gamma { get; set; } = 1.4;
public double GasConstant { get; set; } = 287.0;
// Last resolved state (for audio/monitoring)
public double LastMassFlowRate { get; private set; }
public double LastFaceDensity { get; private set; }
public double LastFaceVelocity { get; private set; }
public double LastFacePressure { get; private set; }
public OrificeLink(Port volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func<double> areaProvider)
{
VolumePort = volumePort ?? throw new ArgumentNullException(nameof(volumePort));
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
IsPipeLeftEnd = isPipeLeftEnd;
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
}
/// <summary>
/// Resolve the coupling for one substep. Computes nozzle flow (isentropic)
/// and sets the pipe ghost cell and the port flow rates.
/// </summary>
public void Resolve(double dtSub)
{
double area = AreaProvider();
if (area < 1e-12)
{
SetClosedWall();
return;
}
// Retrieve volume state
double volP = VolumePort.Pressure;
double volRho = VolumePort.Density;
double volT = VolumePort.Temperature;
double volH = VolumePort.SpecificEnthalpy;
// Retrieve pipe interior state at the connected end
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
// Determine upstream/downstream: if volume pressure > pipe pressure, flow is out of volume (negative into volume).
bool flowOutOfVolume = volP > pipeP;
double pUp, rhoUp, TUp, pDown;
if (flowOutOfVolume)
{
pUp = volP; rhoUp = volRho; TUp = volT; pDown = pipeP;
}
else
{
// Pipe is upstream
pUp = pipeP; rhoUp = pipeRho; TUp = pipeP / (pipeRho * GasConstant); // temperature from pipe
pDown = volP;
}
// Compute isentropic nozzle flow
IsentropicOrifice.Compute(pUp, rhoUp, TUp, Gamma, GasConstant, pDown, area, DischargeCoefficient,
out double mdotUpstreamToDown, out double rhoFace, out double uFace, out double pFace);
// mdotUpstreamToDown is positive from upstream to downstream.
// Convert to mass flow into volume (positive mdot = into volume).
double mdotVolume;
if (flowOutOfVolume)
mdotVolume = -mdotUpstreamToDown; // out of volume is negative
else
mdotVolume = mdotUpstreamToDown; // into volume is positive
// Clamp mass flow to available mass in volume (if it is a Volume0D)
if (VolumePort.Owner is Volume0D vol)
{
double maxMdot = vol.Mass / dtSub;
if (mdotVolume > maxMdot) mdotVolume = maxMdot;
if (mdotVolume < -maxMdot) mdotVolume = -maxMdot;
}
// Apply ghost state to pipe
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
Pipe.SetGhostRight(rhoFace, uFace, pFace);
// Store results
LastMassFlowRate = mdotVolume;
LastFaceDensity = rhoFace;
LastFaceVelocity = uFace;
LastFacePressure = pFace;
// Set port flow rates for volume integration
VolumePort.MassFlowRate = mdotVolume;
if (mdotVolume >= 0)
{
// Inflow: enthalpy comes from upstream (pipe)
double pPipe = pipeP;
double rhoPipe = pipeRho;
VolumePort.SpecificEnthalpy = Gamma / (Gamma - 1.0) * pPipe / rhoPipe;
}
else
{
// Outflow: volume's own specific enthalpy
VolumePort.SpecificEnthalpy = volH;
}
}
private void SetClosedWall()
{
var (rInt, uInt, pInt) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rInt, -uInt, pInt);
else
Pipe.SetGhostRight(rInt, -uInt, pInt);
LastMassFlowRate = 0.0;
LastFaceDensity = rInt;
LastFaceVelocity = 0.0;
LastFacePressure = pInt;
VolumePort.MassFlowRate = 0.0;
// Keep specific enthalpy as is (not used)
}
}
}

View File

@@ -1,22 +0,0 @@
using FluidSim.Components;
namespace FluidSim.Core
{
public class PipeVolumeConnection
{
public Volume0D Volume { get; }
public Pipe1D Pipe { get; }
public bool IsPipeLeftEnd { get; }
public double OrificeArea { get; set; }
public double LastMassFlowIntoVolume { get; set; }
public PipeVolumeConnection(Volume0D vol, Pipe1D pipe, bool isPipeLeftEnd, double orificeArea)
{
Volume = vol;
Pipe = pipe;
IsPipeLeftEnd = isPipeLeftEnd;
OrificeArea = orificeArea;
}
}
}

View File

@@ -1,120 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
/// <summary>
/// Toplevel solver that owns all components and couplings,
/// orchestrates substepping, and exposes states for audio.
/// </summary>
public class Solver
{
private readonly List<Volume0D> _volumes = new();
private readonly List<Pipe1D> _pipes = new();
private readonly List<PipeVolumeConnection> _connections = new();
private readonly List<IComponent> _components = new();
private readonly List<OrificeLink> _orificeLinks = new();
private readonly List<Junction> _junctions = new();
private readonly List<OpenEndLink> _openEndLinks = new();
private double _dt;
private double _ambientPressure = 101325.0;
public void SetAmbientPressure(double p) => _ambientPressure = p;
public void AddVolume(Volume0D v) => _volumes.Add(v);
public void AddPipe(Pipe1D p) => _pipes.Add(p);
public void AddConnection(PipeVolumeConnection c) => _connections.Add(c);
public void SetTimeStep(double dt) => _dt = dt;
public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0)
public void AddComponent(IComponent component) => _components.Add(component);
public void AddOrificeLink(OrificeLink link) => _orificeLinks.Add(link);
public void AddJunction(Junction junction) => _junctions.Add(junction);
public void AddOpenEndLink(OpenEndLink link) => _openEndLinks.Add(link);
// Convenience: first pipes port B mass flow (often the exhaust)
public double ExhaustMassFlow
{
if (isA)
get
{
pipe.SetABoundaryType(type);
if (type == BoundaryType.OpenEnd) pipe.SetAAmbientPressure(ambientPressure);
}
else
{
pipe.SetBBoundaryType(type);
if (type == BoundaryType.OpenEnd) pipe.SetBAmbientPressure(ambientPressure);
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count > 0)
return Math.Abs(pipes[0].PortB.MassFlowRate);
return 0.0;
}
}
public float Step()
/// <summary>
/// Advance the whole system by one global time step.
/// </summary>
public void Step()
{
// 1. For each connection, handle flow or closed wall
foreach (var conn in _connections)
{
double area = conn.OrificeArea;
if (area < 1e-12) // valve closed → treat as solid wall
{
conn.Volume.MassFlowRateIn = 0.0;
conn.Volume.SpecificEnthalpyIn = conn.Volume.SpecificEnthalpy; // not used
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count == 0) return;
// Set ghost to a reflective wall (u = -u_pipe, same p, ρ)
int cellIdx = conn.IsPipeLeftEnd ? 0 : conn.Pipe.GetCellCount() - 1;
double rho = Math.Max(conn.Pipe.GetCellDensity(cellIdx), 1e-6);
double p = Math.Max(conn.Pipe.GetCellPressure(cellIdx), 100.0);
double u = conn.Pipe.GetCellVelocity(cellIdx);
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rho, -u, p);
else
conn.Pipe.SetGhostRight(rho, -u, p);
continue;
}
// Valve open → use the nozzle model
double downstreamPressure = conn.IsPipeLeftEnd
? conn.Pipe.GetCellPressure(0)
: conn.Pipe.GetCellPressure(conn.Pipe.GetCellCount() - 1);
NozzleFlow.Compute(conn.Volume, area, downstreamPressure,
out double mdot, out double rhoFace, out double uFace, out double pFace,
gamma: conn.Volume.Gamma);
// Clamp mdot to available mass
double maxMdot = conn.Volume.Mass / _dt;
conn.LastMassFlowIntoVolume = mdot;
if (mdot > maxMdot) mdot = maxMdot;
if (mdot < -maxMdot) mdot = -maxMdot;
conn.Volume.MassFlowRateIn = mdot;
// enthalpy: if inflow, use pipe enthalpy; if outflow, use cylinder enthalpy
if (mdot >= 0)
{
int cellIdx = conn.IsPipeLeftEnd ? 0 : conn.Pipe.GetCellCount() - 1;
double pPipe = Math.Max(conn.Pipe.GetCellPressure(cellIdx), 100.0);
double rhoPipe = Math.Max(conn.Pipe.GetCellDensity(cellIdx), 1e-6);
conn.Volume.SpecificEnthalpyIn = (conn.Volume.Gamma / (conn.Volume.Gamma - 1.0)) * pPipe / rhoPipe;
}
else
{
conn.Volume.SpecificEnthalpyIn = conn.Volume.SpecificEnthalpy;
}
// Integrate the volume
conn.Volume.Integrate(_dt);
// Set ghost from nozzle face state (but don't allow zero density)
if (rhoFace < 1e-6) rhoFace = Constants.Rho_amb;
if (pFace < 100.0) pFace = Constants.P_amb;
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
conn.Pipe.SetGhostRight(rhoFace, uFace, pFace);
}
// 2. Substep pipes
// 1. Determine substep count (max CFL over all pipes)
int nSub = 1;
foreach (var p in _pipes)
foreach (var p in pipes)
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt));
double dtSub = _dt / nSub;
// 2. Substep loop
for (int sub = 0; sub < nSub; sub++)
foreach (var p in _pipes)
{
// a) Resolve all orifice links (volume ↔ pipe)
foreach (var link in _orificeLinks)
link.Resolve(dtSub);
// b) Resolve all openend links (pipe → atmosphere)
foreach (var link in _openEndLinks)
link.Resolve(dtSub);
// c) Resolve all junctions (pipe ↔ pipe)
foreach (var junc in _junctions)
junc.Resolve(dtSub);
// d) Advance all pipes
foreach (var p in pipes)
p.SimulateSingleStep(dtSub);
}
// 3. Clear ghost flags
foreach (var p in _pipes)
p.ClearGhostFlag();
foreach (var p in pipes)
p.ClearGhostFlags();
// 4. Return exhaust tailpipe mass flow
if (_pipes.Count > 0)
return (float)_pipes[0].GetOpenEndMassFlow();
return 0f;
// 4. Integrate nonpipe components (volumes, atmosphere, etc.)
foreach (var comp in _components)
comp.UpdateState(_dt);
}
}
}

View File

@@ -1,15 +0,0 @@
namespace FluidSim.Interfaces
{
/// <summary>Pure data link between two ports, with orifice parameters.</summary>
public class Connection
{
public Port PortA { get; }
public Port PortB { get; }
public double Area { get; set; } = 1e-5; // effective orifice area (m²)
public double DischargeCoefficient { get; set; } = 0.62;
public double Gamma { get; set; } = 1.4;
public Connection(Port a, Port b) => (PortA, PortB) = (a, b);
}
}

19
Interfaces/IComponent.cs Normal file
View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace FluidSim.Interfaces
{
/// <summary>
/// Minimal interface for all simulation components that have ports.
/// </summary>
public interface IComponent
{
/// <summary>All ports exposed by this component.</summary>
IReadOnlyList<Port> Ports { get; }
/// <summary>
/// Called once per global time step to update the component's internal state
/// using the port flow data accumulated during substeps.
/// </summary>
void UpdateState(double dt);
}
}

View File

@@ -1,18 +1,28 @@
namespace FluidSim.Interfaces
{
/// <summary>
/// A fluid port that belongs to a component. The solver writes mass flow,
/// enthalpy, etc. here, and reads pressure, density, etc.
/// </summary>
public class Port
{
// Set by the solver during coupling resolution
public double MassFlowRate; // kg/s, positive INTO the component that owns this port
public double SpecificEnthalpy; // J/kg, enthalpy of the fluid crossing the port (inflow direction)
// Set by the owning component after integration to reflect its current state
public double Pressure; // Pa
public double MassFlowRate; // kg/s, positive INTO the component
public double SpecificEnthalpy; // J/kg, enthalpy of fluid entering this port
public double Density; // kg/m³
public double Temperature; // K
// Link back to owner (optional, but handy for debugging)
public object? Owner { get; set; }
public Port()
{
Pressure = 101325.0;
MassFlowRate = 0.0;
SpecificEnthalpy = 0.0;
Pressure = 101325.0;
Density = 1.225;
Temperature = 300.0;
}

View File

@@ -1,25 +0,0 @@
namespace FluidSim.Interfaces
{
/// <summary>
/// A Connection that also produces an audio sample from the pressure drop across it.
/// </summary>
public class SoundConnection : Connection
{
/// <summary>Gain applied to the normalised pressure difference.</summary>
public float Gain { get; set; } = 1.0f;
/// <summary>Reference pressure used for normalisation (Pa). Default: 1 atm.</summary>
public double ReferencePressure { get; set; } = 101325.0;
public SoundConnection(Port a, Port b) : base(a, b) { }
/// <summary>
/// Returns a normalised audio sample proportional to the pressure difference.
/// </summary>
public float GetAudioSample()
{
double dp = PortA.Pressure - PortB.Pressure;
return (float)(dp / ReferencePressure) * Gain;
}
}
}

View File

@@ -3,6 +3,7 @@ using SFML.Window;
using SFML.System;
using System.Diagnostics;
using FluidSim.Core;
using FluidSim.Tests;
namespace FluidSim;
@@ -42,7 +43,7 @@ public class Program
soundEngine.Volume = 100;
soundEngine.Start();
scenario = new EngineScenario();
scenario = new TestScenario();
scenario.Initialize(SampleRate);
var stopwatch = Stopwatch.StartNew();
@@ -72,12 +73,6 @@ public class Program
double speedSmoothing = 8.0;
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
// ---- THROTTLE INPUT ----
targetThrottle = Keyboard.IsKeyPressed(Keyboard.Key.W) ? 1.0 : 0.0;
currentThrottle += (targetThrottle - currentThrottle) * (1.0 - Math.Exp(-ThrottleSmoothing * dtClock));
// Push to engine scenario (if it's an EngineScenario)
if (scenario is EngineScenario engine)
engine.Throttle = currentThrottle;
// Generate audio
double targetAudioClock = currentRealTime + 0.05;

View File

@@ -1,325 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using FluidSim.Interfaces;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe;
private Pipe1D intakePipe;
private PipeVolumeConnection couplingExhaust;
private PipeVolumeConnection couplingIntake;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private Port exhaustPort = new Port();
private Port intakePort = new Port();
private double dt;
private double exhPipeArea, intPipeArea;
private const double AmbientPressure = 101325.0;
private double time;
private int stepCount = 0;
private const int LogInterval = 1000;
public double Throttle { get; set; } = 0.15;
private const double FullLoadPeakPressure = 140.0 * Units.bar;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// Crankshaft
crankshaft = new Crankshaft(initialRPM: 2000.0)
{
Inertia = 0.05,
FrictionConstant = 0.2,
FrictionViscous = 0.025
};
// Exhaust pipe (longer, larger)
double exhLength = 0.5;
double exhRadius = 1.5 * Units.cm;
exhPipeArea = Math.PI * exhRadius * exhRadius;
exhaustPipe = new Pipe1D(exhLength, exhPipeArea, sampleRate, forcedCellCount: 30);
exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure);
exhaustPipe.DampingMultiplier = 0.0;
exhaustPipe.EnergyRelaxationRate = 100.0f;
// Intake pipe (shorter, narrower)
double intLength = 0.1;
double intRadius = 1 * Units.cm;
intPipeArea = Math.PI * intRadius * intRadius;
intakePipe = new Pipe1D(intLength, intPipeArea, sampleRate, forcedCellCount: 10);
intakePipe.SetUniformState(1.225, 0.0, AmbientPressure);
// Cylinder (starts at BDC, fresh charge)
engineCyl = new EngineCylinder(crankshaft,
bore: 56 * Units.mm, stroke: 57 * Units.mm, compressionRatio: 9.5,
exhPipeArea: exhPipeArea, intPipeArea: intPipeArea, sampleRate: sampleRate);
engineCyl.ignition = true;
// Set crank to BDC (180°) and sync
crankshaft.CrankAngle = Math.PI;
crankshaft.PreviousAngle = Math.PI; // make sure this property is settable (public setter)
// Couplings
couplingExhaust = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
couplingIntake = new PipeVolumeConnection(engineCyl.Cylinder, intakePipe, false, orificeArea: 0.0);
// Solver
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe);
solver.AddPipe(intakePipe);
solver.AddConnection(couplingExhaust);
solver.AddConnection(couplingIntake);
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure);
solver.SetPipeBoundary(intakePipe, true, BoundaryType.GhostCell); // cylinder side left
solver.SetPipeBoundary(intakePipe, false, BoundaryType.OpenEnd, AmbientPressure); // ambient side right
// Sound
exhaustSoundProcessor = new SoundProcessor(sampleRate, exhRadius * 2);
exhaustSoundProcessor.Gain = 0.001f;
intakeSoundProcessor = new SoundProcessor(sampleRate, intRadius * 2);
intakeSoundProcessor.Gain = 0.001f;
// Reverb
reverb = new OutdoorExhaustReverb(sampleRate);
reverb.DryMix = 1.0f;
reverb.EarlyMix = 0.5f;
reverb.TailMix = 0.9f;
reverb.Feedback = 0.9f;
reverb.DampingFreq = 6000f;
Console.WriteLine("=== Engine with intake & cycleaware valves ===");
}
public override float Process()
{
double idleThrottle = 0.1;
if (crankshaft.AngularVelocity < 80) idleThrottle = 0.2;
double throttle = Math.Clamp(Throttle, idleThrottle, 1.0);
double targetPressure = throttle * FullLoadPeakPressure;
engineCyl.TargetPeakPressure = targetPressure;
engineCyl.Step(dt);
crankshaft.Step(dt);
couplingExhaust.OrificeArea = engineCyl.ExhaustOrificeArea;
couplingIntake.OrificeArea = engineCyl.IntakeOrificeArea;
solver.Step();
UpdateExhaustPort();
UpdateIntakePort();
float dryExhaust = exhaustSoundProcessor.Process(exhaustPort);
float dryIntake = intakeSoundProcessor.Process(intakePort);
float dry = dryExhaust + dryIntake;
float wet = reverb.Process(dry);
if (++stepCount % LogInterval == 0) Log();
return wet;
}
private void Log()
{
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
double cycleDeg = (engineCyl.CycleAngle * 180.0 / Math.PI) % 720.0;
string stroke = cycleDeg < 180.0 ? "Power" :
cycleDeg < 360.0 ? "Exhaust" :
cycleDeg < 540.0 ? "Intake" : "Compression";
// Cylinder
double pCyl = engineCyl.Cylinder.Pressure;
double TCyl = engineCyl.Cylinder.Temperature;
double VCyl = engineCyl.Cylinder.Volume;
double mCyl = engineCyl.Cylinder.Mass;
double exhArea = engineCyl.ExhaustOrificeArea * 1e6; // mm²
double intArea = engineCyl.IntakeOrificeArea * 1e6; // mm²
// Exhaust pipe
int exhLast = exhaustPipe.GetCellCount() - 1;
double pExhEnd = exhaustPipe.GetCellPressure(exhLast);
double mdotExhOut = exhaustPipe.GetOpenEndMassFlow(); // positive out
// Intake pipe
double mdotIntIn = couplingIntake.LastMassFlowIntoVolume;
double pIntAmbEnd = intakePort.Pressure;
Console.WriteLine(
$"{stepCount,8} {stroke,-11} {cycleDeg,6:F1}° " +
$"RPM:{rpm,5:F0} " +
$"Cyl: p={pCyl/1e5,6:F3}bar T={TCyl,6:F0}K V={VCyl*1e6,6:F0}cm³ m={mCyl*1e3,6:F6}g " +
$"Valves: Exh={exhArea,5:F0}mm² Int={intArea,5:F0}mm² " +
$"Intake: p_end={pIntAmbEnd/1e5,6:F3}bar mdot_in={mdotIntIn,7:F4}kg/s " +
$"Exhaust: p_end={pExhEnd/1e5,6:F3}bar mdot_out={mdotExhOut,7:F4}kg/s");
}
private void UpdateExhaustPort()
{
int last = exhaustPipe.GetCellCount() - 1;
double p = exhaustPipe.GetCellPressure(last);
double rho = exhaustPipe.GetCellDensity(last);
double vel = exhaustPipe.GetCellVelocity(last);
// Safety clamps
rho = Math.Clamp(rho, 0.01, 50.0);
vel = Math.Clamp(vel, -500.0, 500.0);
p = Math.Clamp(p, 1e4, 2e6);
double outflowMassFlow = rho * vel * exhPipeArea;
exhaustPort.Pressure = p;
exhaustPort.Density = rho;
exhaustPort.Temperature = p / (rho * 287.05);
exhaustPort.MassFlowRate = -outflowMassFlow;
exhaustPort.SpecificEnthalpy = 0.0;
}
private void UpdateIntakePort()
{
// Use the actual valve mass flow (positive = into cylinder)
double mdotIntoEngine = couplingIntake.LastMassFlowIntoVolume;
// Use cylinder pressure/density for the port state (or intake pipe last cell)
double pCyl = engineCyl.Cylinder.Pressure;
double rhoCyl = engineCyl.Cylinder.Density;
intakePort.Pressure = Math.Max(pCyl, 100);
intakePort.Density = Math.Max(rhoCyl, 1e-6);
intakePort.Temperature = engineCyl.Cylinder.Temperature;
intakePort.MassFlowRate = mdotIntoEngine;
intakePort.SpecificEnthalpy = 0.0;
}
// ==================== Drawing ====================
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
const float T_ambient = 293.15f;
const float T_hot = 1500f;
const float T_cold = 0f;
const float R = 287.05f;
float deltaHot = T_hot - T_ambient;
float deltaCold = T_ambient - T_cold;
float NormaliseTemperature(double T)
{
double t;
if (T >= T_ambient)
t = (T - T_ambient) / deltaHot;
else
t = (T - T_ambient) / deltaCold;
return (float)Math.Clamp(t, -1.0, 1.0);
}
// ---- Cylinder ----
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(200f, centerY - cylH / 2f);
double tempCyl = engineCyl.Cylinder.Temperature;
float tnCyl = NormaliseTemperature(tempCyl);
byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(rC, gC, bC);
target.Draw(cylRect);
// ---- Piston ----
float pistonWidth = cylW - 12f;
float pistonHeight = 16f;
float pistonFraction = (float)engineCyl.PistonPositionFraction;
float pistonTopY = cylRect.Position.Y + pistonFraction * (cylH - pistonHeight);
var pistonRect = new RectangleShape(new Vector2f(pistonWidth, pistonHeight))
{
Position = new Vector2f(cylRect.Position.X + 6f, pistonTopY),
FillColor = new Color(80, 80, 80)
};
target.Draw(pistonRect);
// ---------- NEW: Valve lift indicators ----------
float barWidth = 30f;
float barHeight = 10f;
float exhLift = (float)engineCyl.ExhaustValveLiftCurrent;
float intLift = (float)engineCyl.IntakeValveLiftCurrent;
// Exhaust valve indicator (right side of cylinder)
var exhBar = new RectangleShape(new Vector2f(barWidth, barHeight))
{
Position = new Vector2f(cylRect.Position.X + cylW - 10,
cylRect.Position.Y - 20 - exhLift * 20),
FillColor = new Color(200, 200, 200)
};
target.Draw(exhBar);
// Intake valve indicator (left side of cylinder)
var intBar = new RectangleShape(new Vector2f(barWidth, barHeight))
{
Position = new Vector2f(cylRect.Position.X - 20,
cylRect.Position.Y - 20 - intLift * 20),
FillColor = new Color(200, 200, 200)
};
target.Draw(intBar);
// ---- Exhaust pipe (rightwards) ----
DrawPipe(target, exhaustPipe, startX: 280f, endX: winW - 60f, centerY + 10 - cylRect.Size.Y / 2,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, true);
// ---- Intake pipe (leftwards) ----
DrawPipe(target, intakePipe, startX: 200f, endX: 20f, centerY + 10 - cylRect.Size.Y / 2,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, false);
}
private void DrawPipe(RenderWindow target, Pipe1D pipe,
float startX, float endX, float centerY,
float T_ambient, float T_hot, float T_cold, float R,
Func<double, float> normaliseTemp, bool leftToRight)
{
int n = pipe.GetCellCount();
float dir = leftToRight ? 1f : -1f;
float pipeLen = Math.Abs(endX - startX);
float dx = pipeLen / (n - 1) * dir;
float baseRadius = leftToRight ? 20f : 14f; // exhaust thicker, intake thinner
var vertices = new Vertex[n * 2];
float ambPress = 101325f;
for (int i = 0; i < n; i++)
{
float x = startX + i * dx;
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / (rho * R);
float r = baseRadius * 0.2f * (float)(1.0 + (p - ambPress) / ambPress);
if (r < 2f) r = 2f;
float tn = normaliseTemp(T);
byte rC = (byte)(tn > 0 ? 255 * tn : 0);
byte bC = (byte)(tn < 0 ? -255 * tn : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tn)));
var col = new Color(rC, gC, bC);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
}
}
}

View File

@@ -1,128 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class HelmholtzResonatorScenario : Scenario
{
private Solver solver;
private Volume0D cavity;
private Pipe1D neck;
private PipeVolumeConnection coupling;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// 1litre cavity, 10% overpressure
double cavityVolume = 1e-3;
double initialCavityPressure = 1.1 * ambientPressure;
cavity = new Volume0D(cavityVolume, initialCavityPressure, 300.0, sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
// Neck: length 10 cm, radius 1 cm
double neckLength = 0.1;
double neckRadius = 0.01;
double neckArea = Math.PI * neckRadius * neckRadius;
neck = new Pipe1D(neckLength, neckArea, sampleRate, forcedCellCount: 40);
neck.SetUniformState(1.225, 0.0, ambientPressure);
// Create the coupling between cavity and left end of the neck (PortA)
coupling = new PipeVolumeConnection(cavity, neck, isPipeLeftEnd: true, orificeArea: neckArea);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(cavity);
solver.AddPipe(neck);
solver.AddConnection(coupling);
// Left boundary (PortA) is volumecoupled via ghost cell, right boundary (PortB) is open end
solver.SetPipeBoundary(neck, isA: true, BoundaryType.GhostCell);
solver.SetPipeBoundary(neck, isA: false, BoundaryType.OpenEnd, ambientPressure);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pOpen = neck.GetCellPressure(neck.GetCellCount() - 1);
float audio = (float)((pOpen - ambientPressure) / ambientPressure);
if (stepCount % 20 == 0)
{
double pCav = cavity.Pressure;
// Mass flow rate is not directly available we can compute from pressure difference or skip
Console.WriteLine(
$"t={time * 1e3:F2} ms step={stepCount} " +
$"P_cav={pCav:F1} Pa, P_open={pOpen:F1} Pa, " +
$"audio={audio:F4}");
}
return audio;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
// Cavity rectangle
float cavityWidth = 120f;
float cavityHeight = 180f;
var cavityRect = new RectangleShape(new Vector2f(cavityWidth, cavityHeight));
cavityRect.Position = new Vector2f(40f, centerY - cavityHeight / 2f);
cavityRect.FillColor = PressureColor(cavity.Pressure);
target.Draw(cavityRect);
// Neck drawn as tapered pipe
int n = neck.GetCellCount();
float neckStartX = 40f + cavityWidth + 10f;
float neckEndX = winW - 60f;
float neckLenPx = neckEndX - neckStartX;
float dx = neckLenPx / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++)
{
float x = neckStartX + i * dx;
double p = neck.GetCellPressure(i);
float r = baseRadius * (float)(0.5 + 0.5 * Math.Tanh((p - ambientPressure) / (ambientPressure * 0.2)));
if (r < 4f) r = 4f;
Color col = PressureColor(p);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
// Open end indicator
var arrow = new CircleShape(8f);
arrow.Position = new Vector2f(neckEndX - 4f, centerY - 4f);
arrow.FillColor = Color.White;
target.Draw(arrow);
}
private Color PressureColor(double pressure)
{
double range = ambientPressure * 0.1;
double t = Math.Clamp((pressure - ambientPressure) / range, -1.0, 1.0);
byte r = (byte)(t > 0 ? 255 * t : 0);
byte b = (byte)(t < 0 ? -255 * t : 0);
byte g = (byte)(255 * (1 - Math.Abs(t)));
return new Color(r, g, b);
}
}
}

View File

@@ -1,184 +0,0 @@
using FluidSim.Components;
using FluidSim.Interfaces;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
using System;
namespace FluidSim.Core
{
public class PipeResonatorScenario : Scenario
{
private Solver solver;
private Pipe1D pipe;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
private bool enableLogging = true;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
double length = 2;
double radius = 50 * Units.mm;
double area = Units.AreaFromDiameter(radius);
pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: 80);
pipe.SetUniformState(1.225, 0.0, ambientPressure);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddPipe(pipe);
// Open end at port A (left), closed end at port B (right)
solver.SetPipeBoundary(pipe, isA: true, BoundaryType.OpenEnd, ambientPressure);
solver.SetPipeBoundary(pipe, isA: false, BoundaryType.ClosedEnd);
// Initial pressure pulse
int pulseCells = 5;
double pulsePressure = 2 * ambientPressure;
for (int i = 0; i < pulseCells; i++)
pipe.SetCellState(i, 1.225, 0.0, pulsePressure);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pMid = pipe.GetPressureAtFraction(0.5);
sample = (float)((pMid - ambientPressure) / ambientPressure);
Log(sample);
return sample;
}
private void Log(float sample)
{
if (!enableLogging) return;
if (stepCount % 10 == 0 && stepCount < 1000)
{
double pMid = pipe.GetPressureAtFraction(0.5);
double pOpen = pipe.GetCellPressure(0);
double pClosed = pipe.GetCellPressure(pipe.GetCellCount() - 1);
Console.WriteLine(
$"t = {time * 1e3:F3} ms Step {stepCount:D4}: " +
$"sample = {sample:F3}, " +
$"P_mid = {pMid:F2} Pa ({pMid / ambientPressure:F4} atm), " +
$"P_open = {pOpen:F2} Pa, P_closed = {pClosed:F2} Pa");
}
}
public override void Draw(RenderWindow target)
{
float winWidth = target.GetView().Size.X;
float winHeight = target.GetView().Size.Y;
float pipeCenterY = winHeight / 2f;
float margin = 60f;
float pipeStartX = margin;
float pipeEndX = winWidth - margin;
float pipeLengthPx = pipeEndX - pipeStartX;
int n = pipe.GetCellCount();
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float baseRadius = 25f;
float rangeFactor = 1f;
float scaleFactor = 5f;
// ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
// ----- Precompute cell positions and radii -----
var centers = new float[n];
var radii = new float[n];
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
float deviation = (float)Math.Tanh((p - ambientPressure) / ambientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx;
}
// ----- Build trianglestrip vertices -----
int segmentsPerCell = 8; // smoothness
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2]; // top + bottom for each point
int idx = 0;
for (int i = 0; i < n; i++)
{
// ---- Cell centre ----
float x = centers[i];
float r = radii[i];
double p = pipe.GetCellPressure(i);
Color col = PressureColor(p);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col);
// ---- Intermediate segments after this cell (if not last) ----
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
float st = SmoothStep(0f, 1f, t);
float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double pi = pipe.GetCellPressure(i) * (1 - t) + pipe.GetCellPressure(i + 1) * t;
Color coli = PressureColor(pi);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
}
}
}
// Draw the pipe as a triangle strip
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
// ----- Closed end indicator (right) -----
float wallThickness = 8f;
var wall = new RectangleShape(new Vector2f(wallThickness, winHeight * 0.6f));
wall.Position = new Vector2f(pipeEndX, pipeCenterY - winHeight * 0.6f / 2f);
wall.FillColor = new Color(180, 180, 180);
target.Draw(wall);
}
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
private Color PressureColor(double pressure)
{
double range = ambientPressure * 0.05; // ±5% gives full colour swing
double t = (pressure - ambientPressure) / range;
t = Math.Clamp(t, -1.0, 1.0);
byte r, g, b;
if (t < 0)
{
double factor = -t;
r = 0;
g = (byte)(255 * (1 - factor));
b = (byte)(255 * factor);
}
else
{
double factor = t;
r = (byte)(255 * factor);
g = (byte)(255 * (1 - factor));
b = 0;
}
return new Color(r, g, b);
}
}
}

View File

@@ -1,23 +1,121 @@
using SFML.Graphics;
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
namespace FluidSim.Core
namespace FluidSim.Tests
{
public abstract class Scenario
{
/// <summary>
/// Initialize the scenario with a given audio sample rate.
/// </summary>
/// <summary>Initialize the scenario with a given audio sample rate.</summary>
public abstract void Initialize(int sampleRate);
/// <summary>
/// Advance one simulation step and return an audio sample.
/// The step size is 1 / sampleRate seconds.
/// </summary>
/// <summary>Advance one simulation step and return an audio sample.</summary>
public abstract float Process();
/// <summary>
/// Draw the current simulation state onto the given SFML render target.
/// </summary>
/// <summary>Draw the current simulation state onto the given SFML render target.</summary>
public abstract void Draw(RenderWindow target);
// ---------- Shared drawing helpers ----------
protected const double AmbientPressure = 101325.0;
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
protected Color PressureColor(double pressure)
{
double range = AmbientPressure * 0.05; // ±5% gives full colour swing
double t = (pressure - AmbientPressure) / range;
t = Math.Clamp(t, -1.0, 1.0);
byte r, g, b;
if (t < 0)
{
double factor = -t;
r = 0;
g = (byte)(255 * (1 - factor));
b = (byte)(255 * factor);
}
else
{
double factor = t;
r = (byte)(255 * factor);
g = (byte)(255 * (1 - factor));
b = 0;
}
return new Color(r, g, b);
}
/// <summary>
/// Draws the pipe as a smooth trianglestrip whose radius varies with cell pressure.
/// </summary>
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
{
int n = pipe.CellCount;
if (n < 2) return;
float pipeLengthPx = pipeEndX - pipeStartX;
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float baseRadius = 25f;
float rangeFactor = 1f;
float scaleFactor = 5f;
// ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
// ----- Precompute cell positions and radii -----
var centers = new float[n];
var radii = new float[n];
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx;
}
// ----- Build trianglestrip vertices -----
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
for (int i = 0; i < n; i++)
{
float x = centers[i];
float r = radii[i];
double p = pipe.GetCellPressure(i);
Color col = PressureColor(p);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col);
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
float st = SmoothStep(0f, 1f, t);
float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double pi = pipe.GetCellPressure(i) * (1 - t) + pipe.GetCellPressure(i + 1) * t;
Color coli = PressureColor(pi);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
}
}
}
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
}
}
}

View File

@@ -1,158 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class SodShockTubeScenario : Scenario
{
private Solver solver;
private Pipe1D pipe;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
private const double GasConstant = 287.0;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
double length = 1.0;
double area = 1.0;
int nCells = 200;
pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: nCells);
pipe.SetUniformState(0.125, 0.0, 0.1 * ambientPressure); // right state
// Left half high pressure
for (int i = 0; i < nCells / 2; i++)
pipe.SetCellState(i, 1.0, 0.0, ambientPressure);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddPipe(pipe);
solver.SetPipeBoundary(pipe, isA: true, BoundaryType.ClosedEnd);
solver.SetPipeBoundary(pipe, isA: false, BoundaryType.ClosedEnd);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pMid = pipe.GetPressureAtFraction(0.5);
float audio = (float)((pMid - ambientPressure) / ambientPressure);
bool log = true;
if (log)
{
int n = pipe.GetCellCount();
Console.WriteLine($"step {stepCount}:");
Console.WriteLine("i rho (kg/m³) p (Pa) T (K) u (m/s)");
for (int i = 0; i < n; i++)
{
if (i % 10 == 0)
{
double rho = pipe.GetCellDensity(i);
double p = pipe.GetCellPressure(i);
double u = pipe.GetCellVelocity(i);
double T = p / (rho * GasConstant); // GasConstant = 287.0
Console.WriteLine($"{i,-4} {rho,10:F4} {p,10:F1} {T,8:F2} {u,10:F4}");
}
}
Console.WriteLine();
}
return audio;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
float margin = 40f;
float pipeStartX = margin;
float pipeEndX = winW - margin;
float pipeLenPx = pipeEndX - pipeStartX;
int n = pipe.GetCellCount();
float dx = pipeLenPx / (n - 1);
float baseRadius = 60f;
Vertex[] vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / (rho * GasConstant); // temperature in Kelvin
// Radius from pressure (exaggerated deviation)
float r = baseRadius * (float)(p / ambientPressure * 2);
if (r < 4f) r = 4f;
// Colour from temperature
Color col = TemperatureColor(T);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
// Diaphragm marker (faint white line at initial interface)
float diaphragmX = pipeStartX + (n / 2) * dx;
var line = new RectangleShape(new Vector2f(2f, winH * 0.5f));
line.Position = new Vector2f(diaphragmX - 1f, centerY - winH * 0.25f);
line.FillColor = new Color(255, 255, 255, 80);
target.Draw(line);
}
/// <summary>
/// Custom temperaturetohue mapping that matches the given Sodtube hue values:
/// 250K → 176, 300K → 122, 350K → 120?, 450K → 71.
/// Interpolates piecewise linearly, clamping outside [250,450].
/// </summary>
private Color TemperatureColor(double T)
{
// 1. Map temperature to hue (0360)
double[] Tknots = { 250, 282, 353, 450 };
double[] Hknots = { 176, 179, 122, 71 };
double hue;
if (T <= Tknots[0]) hue = Hknots[0];
else if (T >= Tknots[^1]) hue = Hknots[^1];
else
{
int i = 0;
while (i < Tknots.Length - 1 && T > Tknots[i + 1]) i++;
double frac = (T - Tknots[i]) / (Tknots[i + 1] - Tknots[i]);
hue = Hknots[i] + frac * (Hknots[i + 1] - Hknots[i]);
}
// 2. Convert hue to RGB (S = 1, V = 1)
double h = hue / 60.0;
int sector = (int)Math.Floor(h);
double f = h - sector;
byte p = 0;
byte q = (byte)(255 * (1 - f));
byte tByte = (byte)(255 * f);
byte v = 255;
byte r, g, b;
switch (sector % 6)
{
case 0: r = v; g = tByte; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = tByte; break;
case 3: r = p; g = q; b = v; break;
case 4: r = tByte; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
return new Color(r, g, b);
}
}
}

96
Scenarios/TestScenario.cs Normal file
View File

@@ -0,0 +1,96 @@
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
namespace FluidSim.Tests
{
public class TestScenario : Scenario
{
private Solver solver;
private Volume0D volume;
private Pipe1D pipe;
private Atmosphere atmosphere;
private OrificeLink orificeLink;
private OpenEndLink openEndLink;
private int stepCount;
public override void Initialize(int sampleRate)
{
double dt = 1.0 / sampleRate;
solver = new Solver();
solver.SetTimeStep(dt);
volume = new Volume0D(1e-3, 150000.0, 300.0);
solver.AddComponent(volume);
pipe = new Pipe1D(2.0, 1e-4, 20);
solver.AddComponent(pipe);
atmosphere = new Atmosphere();
solver.AddComponent(atmosphere);
// Volume → left pipe end (orifice)
var volPort = volume.CreatePort();
orificeLink = new OrificeLink(volPort, pipe, isPipeLeftEnd: true, areaProvider: () => 1e-5)
{
DischargeCoefficient = 0.62,
Gamma = volume.Gamma,
GasConstant = volume.GasConstant
};
solver.AddOrificeLink(orificeLink);
// Right pipe end → atmosphere (characteristic openend)
openEndLink = new OpenEndLink(pipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(openEndLink);
stepCount = 0;
Console.WriteLine("TestScenario initialized with sampleRate = " + sampleRate);
}
public override float Process()
{
solver.Step();
stepCount++;
if (stepCount % 100 == 0)
{
double volPressure = volume.Pressure;
double volMass = volume.Mass;
double pipeLeftPressure = pipe.GetCellPressure(0);
double pipeRightPressure = pipe.GetCellPressure(pipe.CellCount - 1);
double mdotOrifice = orificeLink.LastMassFlowRate;
double mdotOpen = openEndLink.LastMassFlowRate;
Console.WriteLine($"Step {stepCount}:");
Console.WriteLine($" Vol Pressure = {volPressure:F1} Pa, Mass = {volMass:E4} kg");
Console.WriteLine($" Pipe left P = {pipeLeftPressure:F1} Pa, right P = {pipeRightPressure:F1} Pa");
Console.WriteLine($" Orifice mdot = {mdotOrifice:E4} kg/s, Openend mdot = {mdotOpen:E4} kg/s");
Console.WriteLine();
}
// Audio sample from the openend mass flow
return (float)openEndLink.LastMassFlowRate;
}
public override void Draw(RenderWindow target)
{
float winWidth = target.GetView().Size.X;
float winHeight = target.GetView().Size.Y;
float pipeCenterY = winHeight / 2f;
float margin = 60f;
float pipeStartX = margin;
float pipeEndX = winWidth - margin;
// Use the shared pipe drawing from the base class
DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX);
}
}
}

View File

@@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluidSim.Sources
{
internal class EffortSource
{
}
}

View File

@@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace FluidSim.Sources
{
internal class FlowSource
{
}
}