12 Commits

Author SHA1 Message Date
bc0df51ddb refactoring (broken right now) 2026-05-06 15:24:39 +02:00
maxwes08
bc4e077924 added valves display 2026-05-06 14:05:37 +02:00
d6b1d214f5 tuff 2026-05-05 19:39:11 +02:00
608dabff12 sound fixed 2026-05-05 16:10:06 +02:00
547e8706f1 update 2026-05-05 14:02:07 +02:00
f16a1aa763 insane engine sound 2026-05-05 11:24:32 +02:00
d963032e74 General testing 2026-05-05 10:32:30 +02:00
max
ff4c4aef23 Helmholtz test, sod shock tube 2026-05-03 20:33:30 +02:00
max
7dfc8fa2d2 Lots of improvements. Better UI, time scrolling, scenario system 2026-05-03 11:21:24 +02:00
max
c427c1f7d3 Added pipe friction 2026-05-03 02:10:28 +02:00
max
a006a07049 Added boundary states for correct resonances 2026-05-03 01:52:55 +02:00
max
3926ed7ef9 Introduced automatic sub stepping and pipe cell count 2026-05-03 00:20:17 +02:00
31 changed files with 1879 additions and 749 deletions

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net10.0/FluidSim.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/FluidSim.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/FluidSim.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/FluidSim.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -1,41 +0,0 @@
namespace FluidSim.Audio
{
internal class RingBuffer
{
private readonly float[] buffer;
private volatile int readPos;
private volatile int writePos;
public RingBuffer(int capacity)
{
if ((capacity & (capacity - 1)) != 0)
throw new ArgumentException("Capacity must be a power of two.");
buffer = new float[capacity];
}
public int Count => (writePos - readPos) & (buffer.Length - 1);
public int Space => (readPos - writePos - 1) & (buffer.Length - 1);
public int Write(float[] data, int count)
{
int space = Space;
int toWrite = Math.Min(count, space);
int mask = buffer.Length - 1;
for (int i = 0; i < toWrite; i++)
buffer[(writePos + i) & mask] = data[i];
writePos = (writePos + toWrite) & mask;
return toWrite;
}
public int Read(float[] destination, int count)
{
int available = Count;
int toRead = Math.Min(count, available);
int mask = buffer.Length - 1;
for (int i = 0; i < toRead; i++)
destination[i] = buffer[(readPos + i) & mask];
readPos = (readPos + toRead) & mask;
return toRead;
}
}
}

View File

@@ -1,43 +0,0 @@
using SFML.Audio;
using SFML.System;
namespace FluidSim.Audio
{
internal class RingBufferStream : SoundStream
{
private readonly RingBuffer ringBuffer;
public RingBufferStream(RingBuffer buffer)
{
ringBuffer = buffer;
// 2 channels, 44.1 kHz, standard stereo mapping
Initialize(2, 44100, new[] { SoundChannel.FrontLeft, SoundChannel.FrontRight });
}
protected override bool OnGetData(out short[] samples)
{
const int monoBlockSize = 512; // number of mono samples we'll read
float[] temp = new float[monoBlockSize];
int read = ringBuffer.Read(temp, monoBlockSize);
samples = new short[monoBlockSize * 2];
if (read > 0)
{
for (int i = 0; i < read; i++)
{
float clamped = Math.Clamp(temp[i], -1f, 1f);
short final = (short)(clamped * short.MaxValue);
samples[i * 2] = final; // left
samples[i * 2 + 1] = final; // right
}
}
for (int i = read * 2; i < samples.Length; i++)
samples[i] = 0;
return true;
}
protected override void OnSeek(Time timeOffset) =>
throw new NotSupportedException();
}
}

View File

@@ -1,45 +0,0 @@
namespace FluidSim.Audio;
public class SoundEngine : IDisposable
{
private readonly RingBuffer ringBuffer;
private readonly RingBufferStream stream;
private bool isPlaying;
public SoundEngine(int bufferCapacity = 16384)
{
ringBuffer = new RingBuffer(bufferCapacity);
stream = new RingBufferStream(ringBuffer);
}
public void Start()
{
if (isPlaying) return;
stream.Play();
isPlaying = true;
}
public void Stop()
{
if (!isPlaying) return;
stream.Stop();
isPlaying = false;
float[] drain = new float[ringBuffer.Count];
ringBuffer.Read(drain, drain.Length);
}
public int WriteSamples(float[] data, int count) =>
ringBuffer.Write(data, count);
public float Volume
{
get => stream.Volume;
set => stream.Volume = value;
}
public void Dispose()
{
Stop();
stream.Dispose();
}
}

View File

@@ -1,29 +0,0 @@
using FluidSim.Components;
using FluidSim.Interfaces;
using System;
namespace FluidSim.Audio
{
public static class SoundProcessor
{
public static float MaxDeltaP { get; set; } = 100_000f;
public static float MaxArea { get; set; } = 1e-4f;
public static float MaxVelocity { get; set; } = 343f;
public static float ReferenceDensity { get; set; } = 1.225f;
public static float ReferenceSpeedOfSound { get; set; } = 343f;
public static float Gain { get; set; } = 1.0f;
public static double ComputeSample(Connection conn)
{
Port portA = conn.PortA;
Port portB = conn.PortB;
double pressureUp = portA.Pressure;
double pressureDown = portB.Pressure;
// No flow or no pressure drop → silence
double deltaP = pressureUp - pressureDown;
return deltaP / 1;
}
}
}

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,17 +0,0 @@
using FluidSim.Interfaces;
namespace FluidSim.Components
{
/// <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);
}
}

54
Components/Crankshaft.cs Normal file
View File

@@ -0,0 +1,54 @@
// Components/Crankshaft.cs
using System;
namespace FluidSim.Components
{
public class Crankshaft
{
public double AngularVelocity { get; set; } // rad/s
public double CrankAngle { get; set; } // rad, 0 … 4π (fourstroke cycle)
public double PreviousAngle { get; set; } // ← now has public setter
public double Inertia { get; set; } = 0.2;
public double FrictionConstant { get; set; } = 2.0; // N·m
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
private double externalTorque;
public Crankshaft(double initialRPM = 400.0)
{
AngularVelocity = initialRPM * 2.0 * Math.PI / 60.0;
CrankAngle = 0.0;
PreviousAngle = 0.0;
}
public void AddTorque(double torque) => externalTorque += torque;
public void Step(double dt)
{
// Catch NaN before it propagates
if (double.IsNaN(AngularVelocity) || double.IsInfinity(AngularVelocity))
AngularVelocity = 0.0;
if (double.IsNaN(externalTorque) || double.IsInfinity(externalTorque))
externalTorque = 0.0;
PreviousAngle = CrankAngle;
double friction = FrictionConstant * Math.Sign(AngularVelocity) + FrictionViscous * AngularVelocity;
double netTorque = externalTorque - friction;
double alpha = netTorque / Inertia;
AngularVelocity += alpha * dt;
if (AngularVelocity < 0) AngularVelocity = 0;
CrankAngle += AngularVelocity * dt;
if (CrankAngle >= 4.0 * Math.PI)
CrankAngle -= 4.0 * Math.PI;
else if (CrankAngle < 0)
CrankAngle += 4.0 * Math.PI;
externalTorque = 0.0;
}
}
}

46
Components/Nozzleflow.cs Normal file
View File

@@ -0,0 +1,46 @@
using System;
namespace FluidSim.Components
{
public static class NozzleFlow
{
/// <summary>
/// Computes the nozzleexit primitive state and mass flow rate from a
/// volume to a pipe, using isentropic relations. Follows ensim4's flow() logic.
/// </summary>
public static void Compute(double Pt_high, double Tt_high,
double P_low, double gamma, double R, double area,
out double rhoExit, out double uExit,
out double pExit, out double mdot)
{
double gm1 = gamma - 1.0;
double Pt_over_Ps = Pt_high / P_low;
// Mach number (subsonic, clamped to 1)
double M = Math.Sqrt(Math.Max(0.0,
(2.0 / gm1) * (Math.Pow(Pt_over_Ps, gm1 / gamma) - 1.0)));
if (M > 1.0) M = 1.0;
double T_star = Tt_high / (1.0 + 0.5 * gm1 * M * M);
double a_star = Math.Sqrt(gamma * R * T_star);
double u_star = M * a_star;
pExit = Pt_high * Math.Pow(1.0 + 0.5 * gm1 * M * M, -gamma / gm1);
rhoExit = pExit / (R * T_star);
uExit = u_star; // positive away from highpressure side
mdot = rhoExit * uExit * area;
}
/// <summary>
/// Ambient cell for nonreflecting open end (ensim4 calc_ambient_cell).
/// </summary>
public static void ComputeAmbientCell(double rhoInt, double uInt, double pInt,
double pAmbient, double gamma,
out double rhoAmb, out double uAmb,
out double pAmb)
{
pAmb = pAmbient;
uAmb = uInt;
rhoAmb = rhoInt * Math.Pow(pAmb / pInt, 1.0 / gamma);
}
}
}

View File

@@ -3,247 +3,315 @@ using FluidSim.Interfaces;
namespace FluidSim.Components namespace FluidSim.Components
{ {
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 PortA { get; }
public Port PortB { 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; // Ambient pressure for the energy relaxation term (default 101325 Pa)
private double _dx, _dt, _gamma = 1.4, _area; private double _ambientPressure = 101325.0;
private double[] _rho, _rhou, _E; public double AmbientPressure
private double _hydraulicDiameter; {
get => _ambientPressure;
set
{
_ambientPressure = value;
_ambientEnergyReference = value / (_gamma - 1.0);
}
}
private double _rhoLeft, _pLeft, _rhoRight, _pRight; // Geometry
private bool _leftBCSet, _rightBCSet; 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;
public double FrictionFactor { get; set; } // Conserved variables [0 .. _n-1]
private double[] _rho;
private double[] _rhou;
private double[] _E;
public int GetCellCount() => _n; // Face fluxes [0 .. _n]
public double GetCellDensity(int i) => _rho[i]; private double[] _fluxM;
public double GetCellPressure(int i) => Pressure(i); private double[] _fluxP;
public double GetCellVelocity(int i) => _rhou[i] / Math.Max(_rho[i], 1e-12); private double[] _fluxE;
// Ghost cells (set externally)
private double _rhoGhostL, _uGhostL, _pGhostL;
private double _rhoGhostR, _uGhostR, _pGhostR;
private bool _ghostLValid, _ghostRValid;
// Precomputed damping coefficient
private double _laminarCoeff;
private double _ambientEnergyReference; // internal energy density at ambient pressure
/// <summary> /// <summary>
/// Create a pipe with CFLstable automatic cell count. /// Initialise a pipe with a given cell count.
/// </summary> /// </summary>
/// <param name="length">Pipe length [m].</param> /// <param name="length">Pipe length (m).</param>
/// <param name="area">Crosssectional area [].</param> /// <param name="area">Crosssectional area ().</param>
/// <param name="sampleRate">Simulation step rate [Hz].</param> /// <param name="cellCount">Number of finitevolume cells (≥ 4).</param>
/// <param name="c0">Speed of sound [m/s] (default 343).</param> public Pipe1D(double length, double area, int cellCount)
/// <param name="frictionFactor">Darcy friction factor (0 = inviscid).</param>
/// <param name="cflSafety">CFL safety factor ≤1 (0.8 recommended).</param>
public Pipe1D(double length, double area, int sampleRate,
double c0 = 343.0, double frictionFactor = 0.02,
double cflSafety = 0.8)
{ {
if (area <= 0) throw new ArgumentException("Pipe area must be > 0"); if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
_area = area;
_dt = 1.0 / sampleRate;
FrictionFactor = frictionFactor;
// Nyquistbased cell count (wave resolution) _n = cellCount;
double nNyquist = Math.Ceiling(length * sampleRate / c0);
// CFLstable cell count: dx ≥ maxSpeed·dt / cflSafety, maxSpeed = 2·c0 (supersonic safe)
double maxSpeed = 2.0 * c0;
double dxMinStable = maxSpeed * _dt / cflSafety;
double nStable = Math.Floor(length / dxMinStable);
_n = Math.Max(2, (int)Math.Min(nNyquist, nStable));
_dx = length / _n; _dx = length / _n;
Area = area;
_diameter = 2.0 * Math.Sqrt(area / Math.PI);
_rho = new double[_n]; _rho = new double[_n];
_rhou = new double[_n]; _rhou = new double[_n];
_E = new double[_n]; _E = new double[_n];
_hydraulicDiameter = Math.Max(2.0 * Math.Sqrt(_area / Math.PI), 1e-9); _fluxM = new double[_n + 1];
_fluxP = new double[_n + 1];
_fluxE = new double[_n + 1];
PortA = new Port(); // Laminar damping coefficient for air at 20°C (multiplied by DampingMultiplier each step)
PortB = new Port(); 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);
} }
public void SetUniformState(double rho, double u, double p) 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)
{ {
double e = p / ((_gamma - 1) * rho); _rhoGhostL = rho;
double Etot = rho * e + 0.5 * rho * u * u; _uGhostL = u;
for (int i = 0; i < _n; i++) _pGhostL = p;
{ _ghostLValid = true;
_rho[i] = rho;
_rhou[i] = rho * u;
_E[i] = Etot;
}
} }
public double GetLeftPressure() => Pressure(0); public void SetGhostRight(double rho, double u, double p)
public double GetRightPressure() => Pressure(_n - 1);
public double GetLeftDensity() => _rho[0];
public double GetRightDensity() => _rho[_n - 1];
public void SetLeftVolumeState(double rhoVol, double pVol)
{ {
_rhoLeft = rhoVol; _rhoGhostR = rho;
_pLeft = pVol; _uGhostR = u;
_leftBCSet = true; _pGhostR = p;
_ghostRValid = true;
} }
public void SetRightVolumeState(double rhoVol, double pVol) public void ClearGhostFlags()
{ {
_rhoRight = rhoVol; _ghostLValid = false;
_pRight = pVol; _ghostRValid = false;
_rightBCSet = true;
} }
private double GetCellTotalSpecificEnthalpy(int i) 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)
{ {
double rho = Math.Max(_rho[i], 1e-12); double rho = Math.Max(_rho[i], 1e-12);
double u = _rhou[i] / rho; return _rhou[i] / rho;
double p = Pressure(i); }
double h = _gamma / (_gamma - 1.0) * p / rho; public double GetCellPressure(int i) => PressureScalar(i);
return h + 0.5 * u * u;
// ---------- Substepping ----------
public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8)
{
double maxW = 0.0;
for (int i = 0; i < _n; i++)
{
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-8);
return Math.Max(1, (int)Math.Ceiling(dtGlobal * maxW / (cflTarget * _dx)));
} }
public void Simulate() // ---------- Main simulation step (per substep) ----------
public void SimulateSingleStep(double 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; int n = _n;
double[] Fm = new double[n + 1], Fp = new double[n + 1], Fe = new double[n + 1];
// --- Left boundary (face 0) --- // Left boundary face (index 0)
if (_leftBCSet) HLLCFlux(_rhoGhostL, _uGhostL, _pGhostL, _rho[0], _rhou[0] / _rho[0], PressureScalar(0),
out _fluxM[0], out _fluxP[0], out _fluxE[0]);
// Internal faces (1 .. n-1)
for (int f = 1; f < n; f++)
{ {
double rhoL = _rhoLeft, uL = 0.0, pL = _pLeft; double rhoL = Math.Max(_rho[f - 1], 1e-12);
double rhoR = _rho[0], uR = _rhou[0] / Math.Max(rhoR, 1e-12), pR = Pressure(0); double uL = _rhou[f - 1] / rhoL;
HLLCFlux(rhoL, uL, pL, rhoR, uR, pR, out Fm[0], out Fp[0], out Fe[0]); double pL = PressureScalar(f - 1);
} double rhoR = Math.Max(_rho[f], 1e-12);
else double uR = _rhou[f] / rhoR;
{ double pR = PressureScalar(f);
Fm[0] = 0; Fp[0] = Pressure(0); Fe[0] = 0; HLLCFlux(rhoL, uL, pL, rhoR, uR, pR, out _fluxM[f], out _fluxP[f], out _fluxE[f]);
} }
// --- Internal faces --- // Right boundary face (index n)
for (int i = 0; i < n - 1; i++) HLLCFlux(_rho[_n - 1], _rhou[_n - 1] / _rho[_n - 1], PressureScalar(_n - 1),
{ _rhoGhostR, _uGhostR, _pGhostR,
double uL = _rhou[i] / Math.Max(_rho[i], 1e-12); out _fluxM[n], out _fluxP[n], out _fluxE[n]);
double uR = _rhou[i + 1] / Math.Max(_rho[i + 1], 1e-12);
HLLCFlux(_rho[i], uL, Pressure(i), _rho[i + 1], uR, Pressure(i + 1),
out Fm[i + 1], out Fp[i + 1], out Fe[i + 1]);
}
// --- Right boundary (face n) --- // Cell update
if (_rightBCSet) double dt_dx = dt / _dx;
{ double coeff = _laminarCoeff * DampingMultiplier;
double rhoL = _rho[n - 1], uL = _rhou[n - 1] / Math.Max(rhoL, 1e-12), pL = Pressure(n - 1); double relaxRate = EnergyRelaxationRate;
double rhoR = _rhoRight, uR = 0.0, pR = _pRight;
HLLCFlux(rhoL, uL, pL, rhoR, uR, pR, out Fm[n], out Fp[n], out Fe[n]);
}
else
{
Fm[n] = 0; Fp[n] = Pressure(n - 1); Fe[n] = 0;
}
// --- Cell update (inviscid fluxes) ---
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
double dM = (Fm[i + 1] - Fm[i]) / _dx; double r = _rho[i];
double dP = (Fp[i + 1] - Fp[i]) / _dx; double ru = _rhou[i];
double dE = (Fe[i + 1] - Fe[i]) / _dx; double E = _E[i];
_rho[i] -= _dt * dM;
_rhou[i] -= _dt * dP;
_E[i] -= _dt * dE;
if (_rho[i] < 1e-12) _rho[i] = 1e-12; double dM = _fluxM[i + 1] - _fluxM[i];
double kinetic = 0.5 * _rhou[i] * _rhou[i] / _rho[i]; double dP = _fluxP[i + 1] - _fluxP[i];
if (_E[i] < kinetic) _E[i] = kinetic; double dE_flux = _fluxE[i + 1] - _fluxE[i];
// Emergency reset if NaN double newR = r - dt_dx * dM;
if (double.IsNaN(_rho[i]) || double.IsNaN(_rhou[i]) || double.IsNaN(_E[i])) double newRu = ru - dt_dx * dP;
{ double newE = E - dt_dx * dE_flux;
_rho[i] = 1.225; // reset to atmospheric air at 300K
_rhou[i] = 0.0; // Wall friction damping (laminar)
_E[i] = 101325.0 / (_gamma - 1.0); // internal energy at 1atm double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
} newRu *= dampingFactor;
// Newtonian cooling toward ambient energy
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
// 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;
} }
// --- Friction (DarcyWeisbach, energyconserving) --- // Update port states to reflect the current interior state (for audio / monitoring)
if (FrictionFactor > 0) (double rhoA, double uA, double pA) = GetInteriorStateLeft();
{ PortA.Pressure = pA;
double D = _hydraulicDiameter; PortA.Density = rhoA;
double twoD = 2.0 * D; PortA.Temperature = pA / (rhoA * 287.0);
for (int i = 0; i < n; i++) PortA.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pA / rhoA;
{
double rho = _rho[i];
double u = _rhou[i] / rho;
double absU = Math.Abs(u);
double src = FrictionFactor * rho * absU * u / twoD;
double kinOld = 0.5 * rho * u * u;
_rhou[i] -= _dt * src;
double uNew = _rhou[i] / rho;
double kinNew = 0.5 * rho * uNew * uNew;
_E[i] += (kinOld - kinNew);
}
}
// --- Publish to ports --- (double rhoB, double uB, double pB) = GetInteriorStateRight();
PortA.Pressure = Pressure(0); PortB.Pressure = pB;
PortA.Density = _rho[0]; PortB.Density = rhoB;
PortB.Pressure = Pressure(_n - 1); PortB.Temperature = pB / (rhoB * 287.0);
PortB.Density = _rho[_n - 1]; PortB.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pB / rhoB;
PortA.MassFlowRate = _leftBCSet ? Fm[0] * _area : 0.0;
PortB.MassFlowRate = _rightBCSet ? -Fm[n] * _area : 0.0;
PortA.SpecificEnthalpy = GetCellTotalSpecificEnthalpy(0);
PortB.SpecificEnthalpy = GetCellTotalSpecificEnthalpy(_n - 1);
_leftBCSet = _rightBCSet = false;
} }
double Pressure(int i) => // ---------- Private helpers ----------
(_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / Math.Max(_rho[i], 1e-12)); private double PressureScalar(int i)
void HLLCFlux(double rL, double uL, double pL, double rR, double uR, double pR,
out double fm, out double fp, out double fe)
{ {
const double eps = 1e-12; double rho = Math.Max(_rho[i], 1e-12);
pL = Math.Max(pL, eps); return (_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho);
pR = Math.Max(pR, eps); }
double cL = Math.Sqrt(_gamma * pL / Math.Max(rL, eps)); /// <summary>
double cR = Math.Sqrt(_gamma * pR / Math.Max(rR, eps)); /// HLLC approximate Riemann solver (Toro, 1997).
double EL = pL / ((_gamma - 1) * rL) + 0.5 * uL * uL; /// Computes the numerical flux at a face given left and right states.
double ER = pR / ((_gamma - 1) * rR) + 0.5 * uR * uR; /// </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 SL = Math.Min(uL - cL, uR - cR);
double SR = Math.Max(uL + cL, uR + cR); double SR = Math.Max(uL + cL, uR + cR);
double denom = rL * (SL - uL) - rR * (SR - uR); double denom = rL * (SL - uL) - rR * (SR - uR);
double Ss; double Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) / denom;
if (Math.Abs(denom) < eps)
Ss = 0.5 * (uL + uR);
else
Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) / denom;
double FrL_m = rL * uL, FrL_p = rL * uL * uL + pL, FrL_e = (rL * EL + pL) * uL; double Fm_L = rL * uL;
double FrR_m = rR * uR, FrR_p = rR * uR * uR + pR, FrR_e = (rR * ER + pR) * uR; double Fp_L = rL * uL * uL + pL;
double Fe_L = (rL * EL + pL) * uL;
if (SL >= 0) { fm = FrL_m; fp = FrL_p; fe = FrL_e; } double Fm_R = rR * uR;
else if (SR <= 0) { fm = FrR_m; fp = FrR_p; fe = FrR_e; } 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) else if (Ss >= 0)
{ {
double diffSL = SL - uL; double rsL = rL * (SL - uL) / (SL - Ss);
if (Math.Abs(diffSL) < eps) diffSL = eps; double ps = pL + rL * (SL - uL) * (Ss - uL);
double rsL = rL * diffSL / (SL - Ss); double EsL = EL + (Ss - uL) * (Ss + pL / (rL * (SL - uL)));
double ps = pL + rL * diffSL * (Ss - uL); fm = rsL * Ss;
double EsL = EL + (Ss - uL) * (Ss + pL / (rL * diffSL)); fp = rsL * Ss * Ss + ps;
fm = rsL * Ss; fp = rsL * Ss * Ss + ps; fe = (rsL * EsL + ps) * Ss; fe = (rsL * EsL + ps) * Ss;
} }
else else
{ {
double diffSR = SR - uR; double rsR = rR * (SR - uR) / (SR - Ss);
if (Math.Abs(diffSR) < eps) diffSR = eps; double ps = pR + rR * (SR - uR) * (Ss - uR);
double rsR = rR * diffSR / (SR - Ss); double EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR)));
double ps = pL + rL * (SL - uL) * (Ss - uL); fm = rsR * Ss;
double EsR = ER + (Ss - uR) * (Ss + pR / (rR * diffSR)); fp = rsR * Ss * Ss + ps;
fm = rsR * Ss; fp = rsR * Ss * Ss + ps; fe = (rsR * EsR + ps) * Ss; 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,9 +0,0 @@
using FluidSim.Interfaces;
namespace FluidSim.Components
{
public class SoundConnection : Connection
{
public SoundConnection(Port a, Port b) : base(a, b) { }
}
}

View File

@@ -1,69 +1,109 @@
using FluidSim.Interfaces; using System;
using System.Collections.Generic;
using FluidSim.Interfaces;
namespace FluidSim.Components 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 Port Port { get; private set; } public List<Port> Ports { get; } = new List<Port>();
public double Mass { get; private set; } public double Mass { get; private set; }
public double InternalEnergy { 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 Gamma { get; set; } = 1.4;
public double GasConstant { get; set; } = 287.0; public double GasConstant { get; set; } = 287.0;
public double Volume { get; set; } // Ambient pressure used for emergency refill default 101325 Pa
public double dVdt { get; set; } public double AmbientPressure { get; set; } = 101325.0;
private double _dt; // Derived quantities
public double Density => Mass / Math.Max(Volume, 1e-12);
public double Density => Mass / Volume; public double Pressure => (Gamma - 1.0) * InternalEnergy / Math.Max(Volume, 1e-12);
public double Pressure => (Gamma - 1.0) * InternalEnergy / Volume; public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double Temperature => Pressure / (Density * GasConstant); public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Math.Max(Density, 1e-12);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density;
public Volume0D(double initialVolume, double initialPressure, public Volume0D(double initialVolume, double initialPressure,
double initialTemperature, int sampleRate, double initialTemperature, double gasConstant = 287.0, double gamma = 1.4)
double gasConstant = 287.0, double gamma = 1.4)
{ {
GasConstant = gasConstant; GasConstant = gasConstant;
Gamma = gamma; Gamma = gamma;
Volume = initialVolume; Volume = initialVolume;
dVdt = 0.0; Dvdt = 0.0;
_dt = 1.0 / sampleRate;
double rho0 = initialPressure / (GasConstant * initialTemperature); double rho0 = initialPressure / (GasConstant * initialTemperature);
Mass = rho0 * Volume; Mass = rho0 * Volume;
InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0); InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0);
Port = new Port();
PushStateToPort();
} }
public void PushStateToPort() /// <summary>Add a new port and initialise it to the volume's current state.</summary>
public Port CreatePort()
{ {
Port.Pressure = Pressure; var port = new Port { Owner = this };
Port.Density = Density; // Set the port state immediately to avoid a mismatch before the first integration
Port.Temperature = Temperature; port.Pressure = Pressure;
Port.SpecificEnthalpy = SpecificEnthalpy; port.Density = Density;
port.Temperature = Temperature;
port.SpecificEnthalpy = SpecificEnthalpy;
Ports.Add(port);
return port;
} }
public void Integrate() /// <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 mdot = Port.MassFlowRate; double totalMdot = 0.0;
double h_in = Port.SpecificEnthalpy; double totalEdot = 0.0;
double dm = mdot * _dt; foreach (var port in Ports)
double dE = (mdot * h_in) * _dt - Pressure * dVdt * _dt; {
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; Mass += dm;
InternalEnergy += dE; InternalEnergy += dE;
// Hard physical bounds prevent NaN and unphysical states // Safeguards: keep at least 1 µg of gas at a small pressure
if (Mass < 1e-12) Mass = 1e-12; double V = Math.Max(Volume, 1e-12);
if (InternalEnergy < 1e-12) InternalEnergy = 1e-12; if (Mass < 1e-9)
{
Mass = 1e-9;
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
}
else if (InternalEnergy < 0.0)
{
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
}
PushStateToPort(); // 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
}
} }
IReadOnlyList<Port> IComponent.Ports => Ports;
} }
} }

11
Core/Constants.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace FluidSim.Core
{
public static class Constants
{
public const double Gamma = 1.4;
public const double R_gas = 287.0; // J/(kg·K)
public const double P_amb = 101325.0; // Pa
public const double T_amb = 300.0; // K
public static readonly double Rho_amb = P_amb / (R_gas * T_amb); // ≈ 1.177 kg/m³
}
}

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;
}
}
}

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,99 +0,0 @@
using FluidSim.Components;
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

@@ -0,0 +1,187 @@
using System;
namespace FluidSim.Core
{
public class OutdoorExhaustReverb
{
// ========== Early reflection delays (stereo: left/right) ==========
private readonly DelayLine groundL, groundR;
private readonly DelayLine wall1L, wall1R;
private readonly DelayLine wall2L, wall2R;
// ========== Diffuse tail FDNs (left/right each with 8 channels) ==========
private const int FDN_CHANNELS = 8;
private readonly DelayLine[] fdnL, fdnR;
private readonly float[] stateL, stateR;
private readonly OrthonormalMixer mixerL, mixerR;
private readonly LowPassFilter[] filterL, filterR;
public float DryMix { get; set; } = 1.0f;
public float EarlyMix { get; set; } = 0.5f;
public float TailMix { get; set; } = 0.9f;
public float Feedback { get; set; } = 0.75f; // safe range 0.70.9
public float DampingFreq { get; set; } = 6000f; // Hz
public OutdoorExhaustReverb(int sampleRate)
{
// Early reflections left/right offset by ~12 ms for stereo width
groundL = new DelayLine((int)(sampleRate * 0.008)); // 8 ms
groundR = new DelayLine((int)(sampleRate * 0.010)); // 10 ms
wall1L = new DelayLine((int)(sampleRate * 0.045));
wall1R = new DelayLine((int)(sampleRate * 0.047));
wall2L = new DelayLine((int)(sampleRate * 0.080));
wall2R = new DelayLine((int)(sampleRate * 0.082));
// FDN delay lengths prime numbers, offset between L/R
int[] lengthsL = { 3203, 4027, 5521, 7027, 8521, 10007, 11503, 13009 };
int[] lengthsR = { 3217, 4049, 5531, 7043, 8537, 10037, 11519, 13033 };
fdnL = new DelayLine[FDN_CHANNELS];
fdnR = new DelayLine[FDN_CHANNELS];
for (int i = 0; i < FDN_CHANNELS; i++)
{
int lenL = Math.Min(lengthsL[i], (int)(sampleRate * 0.25));
int lenR = Math.Min(lengthsR[i], (int)(sampleRate * 0.25));
fdnL[i] = new DelayLine(lenL);
fdnR[i] = new DelayLine(lenR);
}
stateL = new float[FDN_CHANNELS];
stateR = new float[FDN_CHANNELS];
mixerL = new OrthonormalMixer(FDN_CHANNELS);
mixerR = new OrthonormalMixer(FDN_CHANNELS);
filterL = new LowPassFilter[FDN_CHANNELS];
filterR = new LowPassFilter[FDN_CHANNELS];
for (int i = 0; i < FDN_CHANNELS; i++)
{
filterL[i] = new LowPassFilter(sampleRate, DampingFreq);
filterR[i] = new LowPassFilter(sampleRate, DampingFreq);
}
}
/// <summary>Stereo reverb returns (left, right) sample pair.</summary>
public (float left, float right) ProcessStereo(float drySample)
{
// ---- Early reflections ----
float gL = groundL.ReadWrite(drySample * 0.8f);
float gR = groundR.ReadWrite(drySample * 0.8f);
float w1L = wall1L.ReadWrite(drySample * 0.5f);
float w1R = wall1R.ReadWrite(drySample * 0.5f);
float w2L = wall2L.ReadWrite(drySample * 0.4f);
float w2R = wall2R.ReadWrite(drySample * 0.4f);
float earlyL = (gL + w1L + w2L) * EarlyMix;
float earlyR = (gR + w1R + w2R) * EarlyMix;
// ---- Read diffuse tail ----
float[] delOutL = new float[FDN_CHANNELS];
float[] delOutR = new float[FDN_CHANNELS];
for (int i = 0; i < FDN_CHANNELS; i++)
{
delOutL[i] = fdnL[i].Read();
delOutR[i] = fdnR[i].Read();
}
// Mix via orthonormal matrix
float[] mixL = new float[FDN_CHANNELS];
float[] mixR = new float[FDN_CHANNELS];
mixerL.Process(delOutL, mixL);
mixerR.Process(delOutR, mixR);
// Feedback + air absorption
for (int i = 0; i < FDN_CHANNELS; i++)
{
stateL[i] = drySample * 0.15f + Feedback * mixL[i];
stateL[i] = filterL[i].Process(stateL[i]);
fdnL[i].Write(stateL[i]);
stateR[i] = drySample * 0.15f + Feedback * mixR[i];
stateR[i] = filterR[i].Process(stateR[i]);
fdnR[i].Write(stateR[i]);
}
float tailL = 0.0f, tailR = 0.0f;
for (int i = 0; i < FDN_CHANNELS; i++)
{
tailL += delOutL[i];
tailR += delOutR[i];
}
tailL *= TailMix;
tailR *= TailMix;
float left = drySample * DryMix + earlyL + tailL;
float right = drySample * DryMix + earlyR + tailR;
return (left, right);
}
/// <summary>Mono fallback sums left+right / 2.</summary>
public float Process(float drySample)
{
var (l, r) = ProcessStereo(drySample);
return (l + r) * 0.5f;
}
// ========== Helper classes ==========
private class DelayLine
{
private float[] buffer;
private int writePos;
public DelayLine(int length)
{
buffer = new float[Math.Max(length, 1)];
}
public float Read() => buffer[writePos];
public void Write(float value)
{
buffer[writePos] = value;
writePos = (writePos + 1) % buffer.Length;
}
public float ReadWrite(float value)
{
float outVal = buffer[writePos];
buffer[writePos] = value;
writePos = (writePos + 1) % buffer.Length;
return outVal;
}
}
private class LowPassFilter
{
private float b0, a1, y1;
private float sampleRate;
public LowPassFilter(int sampleRate, float cutoff)
{
this.sampleRate = sampleRate;
SetCutoff(cutoff);
}
public void SetCutoff(float cutoff)
{
float w = 2 * (float)Math.PI * cutoff / sampleRate;
float a0 = 1 + w;
b0 = w / a0;
a1 = (1 - w) / a0;
}
public float Process(float x)
{
float y = b0 * x - a1 * y1;
y1 = y;
return y;
}
}
private class OrthonormalMixer
{
private int size;
public OrthonormalMixer(int size) => this.size = size;
public void Process(float[] input, float[] output)
{
float sum = 0;
for (int i = 0; i < size; i++) sum += input[i];
float factor = 2.0f / size;
for (int i = 0; i < size; i++)
output[i] = factor * sum - input[i];
}
}
}
}

View File

@@ -1,125 +1,84 @@
using FluidSim.Audio; using System;
using System.Collections.Generic;
using System.Linq;
using FluidSim.Components; using FluidSim.Components;
using FluidSim.Interfaces; using FluidSim.Interfaces;
namespace FluidSim.Core namespace FluidSim.Core
{ {
/// <summary>
/// Toplevel solver that owns all components and couplings,
/// orchestrates substepping, and exposes states for audio.
/// </summary>
public class Solver public class Solver
{ {
private readonly List<Volume0D> _volumes = new(); private readonly List<IComponent> _components = new();
private readonly List<Pipe1D> _pipes = new(); private readonly List<OrificeLink> _orificeLinks = new();
private readonly List<Connection> _connections = new(); private readonly List<Junction> _junctions = new();
private readonly List<OpenEndLink> _openEndLinks = new();
public float LastSample { get; private set; } private double _dt;
public void AddVolume(Volume0D v) => _volumes.Add(v); public void SetTimeStep(double dt) => _dt = dt;
public void AddPipe(Pipe1D p) => _pipes.Add(p);
public void AddConnection(Connection c) => _connections.Add(c);
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
{
get
{
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count > 0)
return Math.Abs(pipes[0].PortB.MassFlowRate);
return 0.0;
}
}
/// <summary>
/// Advance the whole system by one global time step.
/// </summary>
public void Step() public void Step()
{ {
// 1. Publish volume states to their own ports var pipes = _components.OfType<Pipe1D>().ToList();
foreach (var v in _volumes) if (pipes.Count == 0) return;
v.PushStateToPort();
// 2. Handle direct volumetovolume connections // 1. Determine substep count (max CFL over all pipes)
foreach (var conn in _connections) int nSub = 1;
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++)
{ {
if (IsVolumePort(conn.PortA) && IsVolumePort(conn.PortB)) // a) Resolve all orifice links (volume ↔ pipe)
{ foreach (var link in _orificeLinks)
Volume0D volA = _volumes.Find(v => v.Port == conn.PortA); link.Resolve(dtSub);
Volume0D volB = _volumes.Find(v => v.Port == conn.PortB);
if (volA == null || volB == null) continue; // b) Resolve all openend links (pipe → atmosphere)
foreach (var link in _openEndLinks)
link.Resolve(dtSub);
double pA = volA.Pressure, rhoA = volA.Density; // c) Resolve all junctions (pipe ↔ pipe)
double pB = volB.Pressure, rhoB = volB.Density; foreach (var junc in _junctions)
junc.Resolve(dtSub);
double mdot = OrificeBoundary.MassFlow(pA, rhoA, pB, rhoB, conn); // d) Advance all pipes
foreach (var p in pipes)
if (mdot > 0) // A → B p.SimulateSingleStep(dtSub);
{
volA.Port.MassFlowRate = -mdot;
volB.Port.MassFlowRate = mdot;
volB.Port.SpecificEnthalpy = volA.SpecificEnthalpy; // fluid from A
volA.Port.SpecificEnthalpy = volA.SpecificEnthalpy; // outflow carries its own enthalpy
}
else // B → A (mdot negative)
{
volA.Port.MassFlowRate = -mdot; // positive
volB.Port.MassFlowRate = mdot; // negative
volA.Port.SpecificEnthalpy = volB.SpecificEnthalpy; // fluid from B
volB.Port.SpecificEnthalpy = volB.SpecificEnthalpy; // outflow carries its own
}
}
} }
// 3. Pipevolume boundary conditions unchanged // 3. Clear ghost flags
foreach (var conn in _connections) foreach (var p in pipes)
{ p.ClearGhostFlags();
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
SetVolumeBC(conn.PortA, conn.PortB);
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
SetVolumeBC(conn.PortB, conn.PortA);
}
// 4. Run pipe simulations // 4. Integrate nonpipe components (volumes, atmosphere, etc.)
foreach (var p in _pipes) foreach (var comp in _components)
p.Simulate(); comp.UpdateState(_dt);
// 5. Transfer pipetovolume flows unchanged
foreach (var conn in _connections)
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
TransferPipeToVolume(conn.PortA, conn.PortB);
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
TransferPipeToVolume(conn.PortB, conn.PortA);
}
// 6. Integrate volumes
foreach (var v in _volumes)
v.Integrate();
// 7. COMPUTE AUDIO SAMPLE from all sound connections
double sample = 0f;
foreach (var conn in _connections)
{
if (conn is SoundConnection)
{
// Both ports have the same absolute mass flow; either works.
sample += SoundProcessor.ComputeSample(conn);
}
}
LastSample = (float)Math.Tanh(sample);
}
bool IsVolumePort(Port p) => _volumes.Exists(v => v.Port == p);
bool IsPipePort(Port p) => _pipes.Exists(pp => pp.PortA == p || pp.PortB == p);
Pipe1D GetPipe(Port p) => _pipes.Find(pp => pp.PortA == p || pp.PortB == p);
void SetVolumeBC(Port pipePort, Port volPort)
{
Pipe1D pipe = GetPipe(pipePort);
if (pipe == null) return;
bool isLeft = pipe.PortA == pipePort;
if (isLeft)
pipe.SetLeftVolumeState(volPort.Density, volPort.Pressure);
else
pipe.SetRightVolumeState(volPort.Density, volPort.Pressure);
}
void TransferPipeToVolume(Port pipePort, Port volPort)
{
double mdot = pipePort.MassFlowRate;
volPort.MassFlowRate = -mdot;
if (mdot < 0) // pipe → volume
{
// pipePort.SpecificEnthalpy is already total (h + ½u²)
volPort.SpecificEnthalpy = pipePort.SpecificEnthalpy;
}
// else: volume → pipe, volumes own static enthalpy is used (already set)
} }
} }
} }

131
Core/SoundEngine.cs Normal file
View File

@@ -0,0 +1,131 @@
using SFML.Audio;
using SFML.System;
namespace FluidSim;
#region Lockfree ring buffer (unchanged)
internal class RingBuffer
{
private readonly float[] buffer;
private volatile int readPos;
private volatile int writePos;
public RingBuffer(int capacity)
{
if ((capacity & (capacity - 1)) != 0)
throw new ArgumentException("Capacity must be a power of two.");
buffer = new float[capacity];
}
public int Count => (writePos - readPos) & (buffer.Length - 1);
public int Space => (readPos - writePos - 1) & (buffer.Length - 1);
public int Write(float[] data, int count)
{
int space = Space;
int toWrite = Math.Min(count, space);
int mask = buffer.Length - 1;
for (int i = 0; i < toWrite; i++)
buffer[(writePos + i) & mask] = data[i];
writePos = (writePos + toWrite) & mask;
return toWrite;
}
public int Read(float[] destination, int count)
{
int available = Count;
int toRead = Math.Min(count, available);
int mask = buffer.Length - 1;
for (int i = 0; i < toRead; i++)
destination[i] = buffer[(readPos + i) & mask];
readPos = (readPos + toRead) & mask;
return toRead;
}
}
#endregion
#region Stereo stream that consumes the ring buffer
internal class RingBufferStream : SoundStream
{
private readonly RingBuffer ringBuffer;
public RingBufferStream(RingBuffer buffer)
{
ringBuffer = buffer;
// 2 channels, 44.1 kHz, standard stereo mapping
Initialize(2, 44100, new[] { SoundChannel.FrontLeft, SoundChannel.FrontRight });
}
protected override bool OnGetData(out short[] samples)
{
const int monoBlockSize = 512; // number of mono samples we'll read
float[] temp = new float[monoBlockSize];
int read = ringBuffer.Read(temp, monoBlockSize);
samples = new short[monoBlockSize * 2];
if (read > 0)
{
for (int i = 0; i < read; i++)
{
float clamped = Math.Clamp(temp[i], -1f, 1f);
short final = (short)(clamped * short.MaxValue);
samples[i * 2] = final; // left
samples[i * 2 + 1] = final; // right
}
}
for (int i = read * 2; i < samples.Length; i++)
samples[i] = 0;
return true;
}
protected override void OnSeek(Time timeOffset) =>
throw new NotSupportedException();
}
#endregion
#region Public sound engine API (unchanged)
public class SoundEngine : IDisposable
{
private readonly RingBuffer ringBuffer;
private readonly RingBufferStream stream;
private bool isPlaying;
public SoundEngine(int bufferCapacity = 16384)
{
ringBuffer = new RingBuffer(bufferCapacity);
stream = new RingBufferStream(ringBuffer);
}
public void Start()
{
if (isPlaying) return;
stream.Play();
isPlaying = true;
}
public void Stop()
{
if (!isPlaying) return;
stream.Stop();
isPlaying = false;
float[] drain = new float[ringBuffer.Count];
ringBuffer.Read(drain, drain.Length);
}
public int WriteSamples(float[] data, int count) =>
ringBuffer.Write(data, count);
public float Volume
{
get => stream.Volume;
set => stream.Volume = value;
}
public void Dispose()
{
Stop();
stream.Dispose();
}
}
#endregion

48
Core/SoundProcessor.cs Normal file
View File

@@ -0,0 +1,48 @@
using System;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
public class SoundProcessor
{
private readonly double dt;
private readonly double scaleFactor; // 1 / (4π r) and a user gain
private double prevMassFlowOut;
// Simple lowpass for derivative smoothing (≈ 23 ms)
private double smoothDMdt;
private readonly double alpha;
public float Gain { get; set; } = 1.0f;
public SoundProcessor(int sampleRate, double listenerDistanceMeters = 1.0)
{
dt = 1.0 / sampleRate;
scaleFactor = 1.0 / (4.0 * Math.PI * listenerDistanceMeters);
// Smoothing time constant ~ 2 ms, blocks singlesample spikes
double tau = 0.002;
alpha = Math.Exp(-dt / tau);
}
public float Process(Port port)
{
// Outflow mass flow (positive = leaving pipe)
double flowOut = -port.MassFlowRate;
// Derivative
double rawDerivative = (flowOut - prevMassFlowOut) / dt;
prevMassFlowOut = flowOut;
// Smooth the derivative to kill isolated spikes
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
// Farfield monopole pressure
double pressure = smoothDMdt * scaleFactor * Gain;
// Soft clip to ±1 for audio output (safe limit)
float sample = (float)Math.Tanh(pressure);
return sample;
}
}
}

View File

@@ -5,7 +5,7 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PublishAot>true</PublishAot> <PublishAot>false</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup> </PropertyGroup>

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 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 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 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 Density; // kg/m³
public double Temperature; // K public double Temperature; // K
// Link back to owner (optional, but handy for debugging)
public object? Owner { get; set; }
public Port() public Port()
{ {
Pressure = 101325.0;
MassFlowRate = 0.0; MassFlowRate = 0.0;
SpecificEnthalpy = 0.0; SpecificEnthalpy = 0.0;
Pressure = 101325.0;
Density = 1.225; Density = 1.225;
Temperature = 300.0; Temperature = 300.0;
} }

View File

@@ -2,95 +2,167 @@
using SFML.Window; using SFML.Window;
using SFML.System; using SFML.System;
using System.Diagnostics; using System.Diagnostics;
using FluidSim.Scenarios; using FluidSim.Core;
using FluidSim.Audio; using FluidSim.Tests;
namespace FluidSim; namespace FluidSim;
public class Program public class Program
{ {
private const int SampleRate = 44100; private const int SampleRate = 44100;
private const double DrawFrequency = 60.0;
private static Scenario scenario;
// Speed control (existing + new throttle)
private static double desiredSpeed = 0.01;
private static double currentSpeed = desiredSpeed;
private const double MinSpeed = 0.0001;
private const double MaxSpeed = 1.0;
private const double ScrollFactor = 1.1;
private static double lastDesiredSpeed = 0.1;
private static bool isRealTime = false;
// Throttle smoothing
private static double targetThrottle = 0.0; // 1.0 when W is pressed, 0.0 otherwise
private static double currentThrottle = 0.0;
private const double ThrottleSmoothing = 20.0; // rate of change
private static volatile bool running = true; private static volatile bool running = true;
// Global step counter incremented every simulation step
private static long stepCount = 0;
public static void Main() public static void Main()
{ {
var mode = new VideoMode(new Vector2u(1280, 720)); var mode = new VideoMode(new Vector2u(1280, 720));
var window = new RenderWindow(mode, "Fluid Simulation"); var window = new RenderWindow(mode, "FluidSim - Engine (W = throttle)");
window.SetVerticalSyncEnabled(true); window.SetVerticalSyncEnabled(true);
window.Closed += (_, _) => { running = false; window.Close(); }; window.Closed += (_, _) => { running = false; window.Close(); };
window.MouseWheelScrolled += OnMouseWheel;
window.KeyPressed += OnKeyPressed;
var soundEngine = new SoundEngine(bufferCapacity: 2048); var soundEngine = new SoundEngine(bufferCapacity: 16384);
soundEngine.Volume = 70; soundEngine.Volume = 100;
soundEngine.Start(); soundEngine.Start();
scenario = new TestScenario();
scenario.Initialize(SampleRate);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
double lastDrawTime = 0.0;
double drawInterval = 1.0 / DrawFrequency;
double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds;
// --- Warmup: fill audio buffer with silence --- var simBuffer = new List<float>(4096);
int warmupSamples = SampleRate / 2; // 0.5 s double readIndex = 0.0;
float[] warmup = new float[warmupSamples]; for (int i = 0; i < 4; i++)
for (int i = 0; i < warmupSamples; i++) simBuffer.Add(scenario.Process());
warmup[i] = 0;
soundEngine.WriteSamples(warmup, warmupSamples);
// Reset timer after warmup this is the “realtime zero” long totalSimSteps = simBuffer.Count;
stopwatch.Restart(); long totalOutputSamples = 0;
stepCount = 0; // simulation steps start now const int outputChunk = 256;
float[] outputBuf = new float[outputChunk];
// --- Initialise the simulation scenario ---
Simulation.Initialize(SampleRate);
const int chunkSize = 2048;
float[] buffer = new float[chunkSize];
double lastLogTime = 0.0; // for periodic speed printout
while (window.IsOpen) while (window.IsOpen)
{ {
window.DispatchEvents(); window.DispatchEvents();
// --- Compute how many audio samples are needed since last frame --- double currentRealTime = stopwatch.Elapsed.TotalSeconds;
double currentTime = stopwatch.Elapsed.TotalSeconds; double dtClock = currentRealTime - lastSpeedUpdateTime;
double elapsed = currentTime; // since stopwatch was reset lastSpeedUpdateTime = currentRealTime;
int samplesNeeded = (int)(elapsed * SampleRate) - (int)(stepCount);
// (stepCount is total generated samples, so we just need the remainder)
// --- Generate the required number of simulation steps --- // Smooth simulation speed
while (samplesNeeded > 0 && running) double speedSmoothing = 8.0;
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
// Generate audio
double targetAudioClock = currentRealTime + 0.05;
while (totalOutputSamples < targetAudioClock * SampleRate && running)
{ {
int toGenerate = Math.Min(samplesNeeded, chunkSize); int toGenerate = (int)Math.Min(
(long)outputChunk,
(long)(targetAudioClock * SampleRate) - totalOutputSamples
);
if (toGenerate <= 0) break;
double maxIndex = readIndex + (toGenerate - 1) * currentSpeed + 2;
int requiredSimIndex = (int)Math.Ceiling(maxIndex);
while (simBuffer.Count - 1 < requiredSimIndex)
{
simBuffer.Add(scenario.Process());
totalSimSteps++;
}
for (int i = 0; i < toGenerate; i++) for (int i = 0; i < toGenerate; i++)
{ {
buffer[i] = Simulation.Process(); int i0 = (int)readIndex;
stepCount++; int i1 = i0 + 1;
double frac = readIndex - i0;
float y0 = simBuffer[Math.Clamp(i0, 0, simBuffer.Count - 1)];
float y1 = simBuffer[Math.Clamp(i1, 0, simBuffer.Count - 1)];
outputBuf[i] = (float)(y0 + (y1 - y0) * frac);
readIndex += currentSpeed;
while (readIndex >= 1.0 && simBuffer.Count > 2)
{
simBuffer.RemoveAt(0);
readIndex -= 1.0;
}
} }
soundEngine.WriteSamples(buffer, toGenerate);
samplesNeeded -= toGenerate; int accepted = soundEngine.WriteSamples(outputBuf, toGenerate);
totalOutputSamples += accepted;
if (accepted < toGenerate)
break;
} }
// --- Display speed --- // Drawing & title
double simTime = stepCount / (double)SampleRate; if (currentRealTime - lastDrawTime >= drawInterval)
double wallTime = stopwatch.Elapsed.TotalSeconds;
double speed = (wallTime > 0) ? simTime / wallTime : 0.0;
// Update window title with instant speed
window.SetTitle($"FluidSim | Speed: {speed:F3}× | Steps: {stepCount}");
// Console log once per second
if (wallTime - lastLogTime >= 1.0)
{ {
Console.WriteLine($"Speed: {speed:F3}× ({stepCount} steps, {wallTime:F2}s wall)"); double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
lastLogTime = wallTime; double simTime = totalSimSteps / (double)SampleRate;
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
window.SetTitle(
$"{toggleHint} {throttleHint} " +
$"Thr: {currentThrottle:F2} " +
$"Speed: {currentSpeed:F3}x → {desiredSpeed:F3}x " +
$"Act: {actualSpeed:F2}x"
);
window.Clear(Color.Black);
scenario.Draw(window);
window.Display();
lastDrawTime = currentRealTime;
} }
// --- Rendering (placeholder) ---
window.Clear(Color.Black);
window.Display();
} }
// --- Cleanup ---
soundEngine.Dispose(); soundEngine.Dispose();
window.Dispose(); window.Dispose();
} }
// (Mouse wheel, space toggle unchanged)
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
{
bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
if (e.Delta > 0)
desiredSpeed *= ScrollFactor;
else if (e.Delta < 0)
desiredSpeed /= ScrollFactor;
desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed);
if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6)
lastDesiredSpeed = desiredSpeed;
isRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
}
private static void OnKeyPressed(object? sender, KeyEventArgs e)
{
if (e.Code == Keyboard.Key.Space)
{
if (isRealTime)
desiredSpeed = lastDesiredSpeed;
else
desiredSpeed = 1.0;
isRealTime = !isRealTime;
}
}
} }

121
Scenarios/Scenario.cs Normal file
View File

@@ -0,0 +1,121 @@
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
namespace FluidSim.Tests
{
public abstract class Scenario
{
/// <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.</summary>
public abstract float Process();
/// <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,93 +0,0 @@
using FluidSim.Audio;
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Utilities;
namespace FluidSim.Scenarios
{
public static class Simulation
{
private static Solver solver;
private static Volume0D cavity, ambient;
private static Pipe1D neck;
private static Connection connNeckCavity, connNeckAmbient;
private static int stepCount;
private static double time;
private static double dt;
public static void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// --- Cavity (the “bottle”) ---
double V_cav = 1.0 * Units.L; // 1 litre
cavity = new Volume0D(V_cav, 10 * Units.atm, Units.Celsius(20), sampleRate);
// --- Ambient (a huge “room”) ---
double V_amb = 1000.0; // 1000 m³ ≈ constant pressure
ambient = new Volume0D(V_amb, 1.0 * Units.atm, Units.Celsius(20), sampleRate);
// --- Neck (short pipe) ---
double length_neck = 80.0 * Units.mm;
double diam_neck = 10 * Units.mm;
double area_neck = Units.AreaFromRadius(diam_neck, Units.mm); // few cells enough for a short neck
neck = new Pipe1D(length_neck, area_neck, sampleRate);
neck.SetUniformState(ambient.Density, 0.0, ambient.Pressure);
neck.FrictionFactor = 0.02; // slight damping
// --- Connections ---
// Cavity-to-neck (full area)
connNeckCavity = new Connection(cavity.Port, neck.PortA)
{
Area = area_neck,
DischargeCoefficient = 1.0,
Gamma = 1.4
};
// Neck-to-ambient (SoundConnection to capture the radiated tone)
connNeckAmbient = new SoundConnection(neck.PortB, ambient.Port)
{
Area = area_neck,
DischargeCoefficient = 1.0,
Gamma = 1.4
};
// --- Solver ---
solver = new Solver();
solver.AddVolume(cavity);
solver.AddVolume(ambient);
solver.AddPipe(neck);
solver.AddConnection(connNeckCavity);
solver.AddConnection(connNeckAmbient);
// --- Sound tuning ---
SoundProcessor.MaxDeltaP = 0.1f * (float)Units.atm; // small Δp expected
SoundProcessor.MaxArea = (float)area_neck;
SoundProcessor.MaxVelocity = 343f;
SoundProcessor.ReferenceDensity = 1.2f;
SoundProcessor.ReferenceSpeedOfSound = 343f;
SoundProcessor.Gain = 10.0f; // amplify because Δp is small
}
public static float Process()
{
solver.Step();
time += dt;
stepCount++;
Log();
return solver.LastSample;
}
public static void Log()
{
if (stepCount <= 200 || stepCount % (int)(0.5 / dt) == 0)
{
Console.WriteLine(
$"t = {time * 1e3:F3} ms " +
$"Sample = {solver.LastSample:F6}, " +
$"P_cav = {cavity.Pressure / 1e5:F6} bar, " +
$"flow_cav = {cavity.Port.MassFlowRate / 1e3:F4} g/s, " +
$"flow_neck = {neck.PortB.MassFlowRate * 1e3:F4} g/s");
}
}
}
}

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,4 +1,6 @@
namespace FluidSim.Utilities using System;
namespace FluidSim.Utils
{ {
public static class Units public static class Units
{ {
@@ -19,10 +21,10 @@
public static double Celsius(double tC) => tC + 273.15; public static double Celsius(double tC) => tC + 273.15;
public static double AreaFromRadius(double radius, double unit = mm) => public static double AreaFromRadius(double radius) =>
Math.PI * (radius * unit) * (radius * unit); Math.PI * (radius) * (radius);
public static double AreaFromDiameter(double diameter, double unit = mm) => public static double AreaFromDiameter(double diameter) =>
Math.PI * 0.25 * (diameter * unit) * (diameter * unit); Math.PI * 0.25 * (diameter) * (diameter);
} }
} }