General testing

This commit is contained in:
2026-05-05 10:32:30 +02:00
parent ff4c4aef23
commit d963032e74
11 changed files with 794 additions and 448 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

@@ -5,9 +5,10 @@ namespace FluidSim.Components
{ {
public enum BoundaryType public enum BoundaryType
{ {
VolumeCoupling,
OpenEnd, OpenEnd,
ClosedEnd ZeroPressureOpen, // pressurerelease boundary (strong reflection)
ClosedEnd,
GhostCell
} }
public class Pipe1D public class Pipe1D
@@ -21,27 +22,20 @@ namespace FluidSim.Components
private double _dx, _dt, _gamma, _area, _diameter; private double _dx, _dt, _gamma, _area, _diameter;
private double[] _rho, _rhou, _E; private double[] _rho, _rhou, _E;
// Volumecoupling ghost states for boundaries A and B // Ghost cell states
private double _rhoA, _pA; private double _rhoGhostL, _uGhostL, _pGhostL;
private double _rhoB, _pB; private double _rhoGhostR, _uGhostR, _pGhostR;
private bool _aBCSet, _bBCSet; private bool _ghostLSet, _ghostRSet;
private BoundaryType _aBCType = BoundaryType.VolumeCoupling; private BoundaryType _aBCType = BoundaryType.GhostCell;
private BoundaryType _bBCType = BoundaryType.VolumeCoupling; private BoundaryType _bBCType = BoundaryType.GhostCell;
private double _aAmbientPressure = 101325.0; private double _aAmbientPressure = 101325.0;
private double _bAmbientPressure = 101325.0; private double _bAmbientPressure = 101325.0;
private const double CflTarget = 0.8; private const double CflTarget = 0.8;
private const double ReferenceSoundSpeed = 340.0; private const double ReferenceSoundSpeed = 340.0;
private double _lastMassFlow = 0.0;
public int GetCellCount() => _n;
public double GetCellDensity(int i) => _rho[i];
public double GetCellPressure(int i) => Pressure(i);
public double GetCellVelocity(int i) => _rhou[i] / Math.Max(_rho[i], 1e-12);
public BoundaryType ABCType => _aBCType;
public BoundaryType BBCType => _bBCType;
public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0) public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0)
{ {
@@ -49,9 +43,7 @@ namespace FluidSim.Components
int nCells; int nCells;
if (forcedCellCount > 1) if (forcedCellCount > 1)
{
nCells = forcedCellCount; nCells = forcedCellCount;
}
else else
{ {
double dxTarget = ReferenceSoundSpeed * dtGlobal * CflTarget; double dxTarget = ReferenceSoundSpeed * dtGlobal * CflTarget;
@@ -65,8 +57,6 @@ namespace FluidSim.Components
_dt = dtGlobal; _dt = dtGlobal;
_area = area; _area = area;
_gamma = 1.4; _gamma = 1.4;
// Hydraulic diameter for a circular pipe
_diameter = 2.0 * Math.Sqrt(area / Math.PI); _diameter = 2.0 * Math.Sqrt(area / Math.PI);
_rho = new double[_n]; _rho = new double[_n];
@@ -82,6 +72,26 @@ namespace FluidSim.Components
public void SetAAmbientPressure(double p) => _aAmbientPressure = p; public void SetAAmbientPressure(double p) => _aAmbientPressure = p;
public void SetBAmbientPressure(double p) => _bAmbientPressure = p; public void SetBAmbientPressure(double p) => _bAmbientPressure = p;
public void SetGhostLeft(double rho, double u, double p)
{
_rhoGhostL = rho;
_uGhostL = u;
_pGhostL = p;
_ghostLSet = true;
}
public void SetGhostRight(double rho, double u, double p)
{
_rhoGhostR = rho;
_uGhostR = u;
_pGhostR = p;
_ghostRSet = true;
}
public void ClearGhostFlag()
{
_ghostLSet = false;
_ghostRSet = false;
}
public void SetUniformState(double rho, double u, double p) public void SetUniformState(double rho, double u, double p)
{ {
double e = p / ((_gamma - 1) * rho); double e = p / ((_gamma - 1) * rho);
@@ -94,6 +104,46 @@ namespace FluidSim.Components
} }
} }
public double GetOpenEndMassFlow()
{
if (_bBCType != BoundaryType.OpenEnd && _bBCType != BoundaryType.ZeroPressureOpen)
return 0.0;
int lastCell = _n - 1;
double rho = _rho[lastCell];
double u = _rhou[lastCell] / Math.Max(rho, 1e-12);
double p = Pressure(lastCell);
double c = Math.Sqrt(_gamma * p / rho);
double uFace = u;
double rhoFace = rho;
double pFace = p;
// Subsonic outflow: impose ambient pressure, adjust velocity and density
if (uFace > 0 && uFace < c)
{
double s = p / Math.Pow(rho, _gamma);
double rhoAmb = Math.Pow(_bAmbientPressure / s, 1.0 / _gamma);
double cAmb = Math.Sqrt(_gamma * _bAmbientPressure / rhoAmb);
double J_plus = u + 2.0 * c / (_gamma - 1.0);
double uFaceNew = J_plus - 2.0 * cAmb / (_gamma - 1.0);
if (uFaceNew > 0) uFace = uFaceNew;
if (uFace < 0) uFace = 0;
rhoFace = rhoAmb;
pFace = _bAmbientPressure;
}
return rhoFace * uFace * _area;
}
public double GetAndStoreMassFlowForDerivative()
{
double current = GetOpenEndMassFlow();
double derivative = (current - _lastMassFlow) / _dt;
_lastMassFlow = current;
return derivative;
}
public void SetCellState(int i, double rho, double u, double p) public void SetCellState(int i, double rho, double u, double p)
{ {
if (i < 0 || i >= _n) return; if (i < 0 || i >= _n) return;
@@ -103,22 +153,18 @@ namespace FluidSim.Components
_E[i] = rho * e + 0.5 * rho * u * u; _E[i] = rho * e + 0.5 * rho * u * u;
} }
public void SetAVolumeState(double rhoVol, double pVol) public int GetCellCount() => _n;
public double GetCellDensity(int i) => _rho[i];
public double GetCellPressure(int i) => Pressure(i);
public double GetCellVelocity(int i) => _rhou[i] / Math.Max(_rho[i], 1e-12);
private double Pressure(int i) => (_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / Math.Max(_rho[i], 1e-12));
public double GetPressureAtFraction(double fraction)
{ {
_rhoA = rhoVol; int i = (int)(fraction * (_n - 1));
_pA = pVol; i = Math.Clamp(i, 0, _n - 1);
_aBCSet = true; return Pressure(i);
} }
public void SetBVolumeState(double rhoVol, double pVol)
{
_rhoB = rhoVol;
_pB = pVol;
_bBCSet = true;
}
public void ClearBC() => _aBCSet = _bBCSet = false;
public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8) public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8)
{ {
double maxW = 0.0; double maxW = 0.0;
@@ -141,101 +187,72 @@ namespace FluidSim.Components
double[] Fp = new double[n + 1]; double[] Fp = new double[n + 1];
double[] Fe = new double[n + 1]; double[] Fe = new double[n + 1];
// ---------- Boundary A (face 0, left) ---------- // Left boundary (face 0)
double rhoIntA = _rho[0]; double rhoL = _rho[0];
double uIntA = _rhou[0] / Math.Max(rhoIntA, 1e-12); double uL = _rhou[0] / Math.Max(rhoL, 1e-12);
double pIntA = Pressure(0); double pL = Pressure(0);
if (_aBCType == BoundaryType.GhostCell && _ghostLSet)
switch (_aBCType) HLLCFlux(_rhoGhostL, _uGhostL, _pGhostL, rhoL, uL, pL, out Fm[0], out Fp[0], out Fe[0]);
else if (_aBCType == BoundaryType.OpenEnd)
OpenEndFluxLeft(rhoL, uL, pL, _aAmbientPressure, out Fm[0], out Fp[0], out Fe[0]);
else if (_aBCType == BoundaryType.ZeroPressureOpen)
{ {
case BoundaryType.VolumeCoupling: // Strong reflection: force pressure to ambient, extrapolate density and velocity
if (_aBCSet) double rhoFace = rhoL;
{ double uFace = uL;
HLLCFlux(_rhoA, 0.0, _pA, double pFace = _aAmbientPressure;
rhoIntA, uIntA, pIntA, HLLCFlux(rhoFace, uFace, pFace, rhoL, uL, pL, out Fm[0], out Fp[0], out Fe[0]);
out Fm[0], out Fp[0], out Fe[0]);
} }
else if (_aBCType == BoundaryType.ClosedEnd)
ClosedEndFlux(rhoL, uL, pL, false, out Fm[0], out Fp[0], out Fe[0]);
else else
{ { Fm[0] = 0; Fp[0] = pL; Fe[0] = 0; }
Fm[0] = 0; Fp[0] = pIntA; Fe[0] = 0;
}
break;
case BoundaryType.OpenEnd: // Internal faces
OpenEndFluxA(rhoIntA, uIntA, pIntA, _aAmbientPressure,
out Fm[0], out Fp[0], out Fe[0]);
break;
case BoundaryType.ClosedEnd:
ClosedEndFlux(rhoIntA, uIntA, pIntA, isRightBoundary: false,
out Fm[0], out Fp[0], out Fe[0]);
break;
}
// ---------- Internal faces ----------
for (int i = 0; i < n - 1; i++) for (int i = 0; i < n - 1; i++)
{ {
double rhoL = _rho[i]; double rhoLi = _rho[i];
double uL = _rhou[i] / Math.Max(rhoL, 1e-12); double uLi = _rhou[i] / Math.Max(rhoLi, 1e-12);
double pL = Pressure(i); double pLi = Pressure(i);
double rhoRi = _rho[i + 1];
double rhoR = _rho[i + 1]; double uRi = _rhou[i + 1] / Math.Max(rhoRi, 1e-12);
double uR = _rhou[i + 1] / Math.Max(rhoR, 1e-12); double pRi = Pressure(i + 1);
double pR = Pressure(i + 1); HLLCFlux(rhoLi, uLi, pLi, rhoRi, uRi, pRi, out Fm[i + 1], out Fp[i + 1], out Fe[i + 1]);
HLLCFlux(rhoL, uL, pL, rhoR, uR, pR,
out Fm[i + 1], out Fp[i + 1], out Fe[i + 1]);
} }
// ---------- Boundary B (face n, right) ---------- // Right boundary (face n)
double rhoIntB = _rho[n - 1]; double rhoR = _rho[n - 1];
double uIntB = _rhou[n - 1] / Math.Max(rhoIntB, 1e-12); double uR = _rhou[n - 1] / Math.Max(rhoR, 1e-12);
double pIntB = Pressure(n - 1); double pR = Pressure(n - 1);
if (_bBCType == BoundaryType.GhostCell && _ghostRSet)
switch (_bBCType) HLLCFlux(rhoR, uR, pR, _rhoGhostR, _uGhostR, _pGhostR, out Fm[n], out Fp[n], out Fe[n]);
else if (_bBCType == BoundaryType.OpenEnd)
OpenEndFluxRight(rhoR, uR, pR, _bAmbientPressure, out Fm[n], out Fp[n], out Fe[n]);
else if (_bBCType == BoundaryType.ZeroPressureOpen)
{ {
case BoundaryType.VolumeCoupling: double rhoFace = rhoR;
if (_bBCSet) double uFace = uR;
{ double pFace = _bAmbientPressure;
HLLCFlux(rhoIntB, uIntB, pIntB, HLLCFlux(rhoR, uR, pR, rhoFace, uFace, pFace, out Fm[n], out Fp[n], out Fe[n]);
_rhoB, 0.0, _pB,
out Fm[n], out Fp[n], out Fe[n]);
} }
else if (_bBCType == BoundaryType.ClosedEnd)
ClosedEndFlux(rhoR, uR, pR, true, out Fm[n], out Fp[n], out Fe[n]);
else else
{ { Fm[n] = 0; Fp[n] = pR; Fe[n] = 0; }
Fm[n] = 0; Fp[n] = pIntB; Fe[n] = 0;
}
break;
case BoundaryType.OpenEnd: // Cell update (linear damping)
OpenEndFluxB(rhoIntB, uIntB, pIntB, _bAmbientPressure,
out Fm[n], out Fp[n], out Fe[n]);
break;
case BoundaryType.ClosedEnd:
ClosedEndFlux(rhoIntB, uIntB, pIntB, isRightBoundary: true,
out Fm[n], out Fp[n], out Fe[n]);
break;
}
// ---- Cell update with linear laminar damping ----
double radius = _diameter / 2.0; double radius = _diameter / 2.0;
double mu_air = 1.8e-5; double mu_air = 1.8e-5;
double laminarCoeff = DampingMultiplier * 8.0 * mu_air / (radius * radius); double laminarCoeff = DampingMultiplier * 8.0 * mu_air / (radius * radius);
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
double dM = (Fm[i + 1] - Fm[i]) / _dx; _rho[i] -= dtSub * (Fm[i + 1] - Fm[i]) / _dx;
double dP = (Fp[i + 1] - Fp[i]) / _dx; _rhou[i] -= dtSub * (Fp[i + 1] - Fp[i]) / _dx;
double dE = (Fe[i + 1] - Fe[i]) / _dx; _E[i] -= dtSub * (Fe[i + 1] - Fe[i]) / _dx;
_rho[i] -= dtSub * dM;
_rhou[i] -= dtSub * dP;
_E[i] -= dtSub * dE;
double rho = Math.Max(_rho[i], 1e-12); double rho = Math.Max(_rho[i], 1e-12);
double dampingRate = laminarCoeff / rho; double dampingFactor = Math.Exp(-(laminarCoeff / rho) * dtSub);
double dampingFactor = Math.Exp(-dampingRate * dtSub);
_rhou[i] *= dampingFactor; _rhou[i] *= dampingFactor;
if (_rho[i] < 1e-12) _rho[i] = 1e-12; if (_rho[i] < 1e-12) _rho[i] = 1e-12;
@@ -244,139 +261,10 @@ namespace FluidSim.Components
double eMin = pMin / ((_gamma - 1) * _rho[i]) + kinetic; double eMin = pMin / ((_gamma - 1) * _rho[i]) + kinetic;
if (_E[i] < eMin) _E[i] = eMin; if (_E[i] < eMin) _E[i] = eMin;
} }
// ---------- Port quantities ----------
double mdotA_sub = _aBCType == BoundaryType.VolumeCoupling && _aBCSet ? Fm[0] * _area : 0.0;
double mdotB_sub = _bBCType == BoundaryType.VolumeCoupling && _bBCSet ? -Fm[n] * _area : 0.0;
PortA.MassFlowRate = mdotA_sub;
PortB.MassFlowRate = mdotB_sub;
PortA.Pressure = pIntA;
PortB.Pressure = pIntB;
PortA.Density = _rho[0];
PortB.Density = _rho[n - 1];
// Corrected enthalpy for both directions
if (_aBCType == BoundaryType.VolumeCoupling && _aBCSet)
{
PortA.SpecificEnthalpy = mdotA_sub < 0
? GetCellTotalSpecificEnthalpy(0)
: (_gamma / (_gamma - 1.0)) * _pA / Math.Max(_rhoA, 1e-12);
}
if (_bBCType == BoundaryType.VolumeCoupling && _bBCSet)
{
PortB.SpecificEnthalpy = mdotB_sub < 0
? GetCellTotalSpecificEnthalpy(_n - 1)
: (_gamma / (_gamma - 1.0)) * _pB / Math.Max(_rhoB, 1e-12);
}
} }
private double GetCellTotalSpecificEnthalpy(int i) // ---------- HLLC Riemann solver ----------
{ private void HLLCFlux(double rL, double uL, double pL, double rR, double uR, double pR,
double rho = Math.Max(_rho[i], 1e-12);
double u = _rhou[i] / rho;
double p = Pressure(i);
double h = _gamma / (_gamma - 1.0) * p / rho;
return h + 0.5 * u * u;
}
private double Pressure(int i) =>
(_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / Math.Max(_rho[i], 1e-12));
// ========== Characteristicbased Open End ==========
private void OpenEndFluxA(double rhoInt, double uInt, double pInt, double pAmb,
out double fm, out double fp, out double fe)
{
double cInt = Math.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12));
// Subsonic inflow (uInt ≤ 0, so flow inside pipe ←)
if (uInt <= -cInt) // supersonic inflow use interior state as ghost
{
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1) * rhoInt) + 0.5 * uInt * uInt) + pInt) * uInt;
return;
}
else if (uInt <= 0) // subsonic inflow
{
// Reservoir condition: p = pAmb, T = 300K, u = 0
double T0 = 300.0;
double R = 287.0;
double rhoGhost = pAmb / (R * T0);
HLLCFlux(rhoGhost, 0.0, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
return;
}
else // subsonic outflow (uInt > 0)
{
// Ghost pressure forced to pAmb
double s = pInt / Math.Pow(rhoInt, _gamma);
double rhoGhost = Math.Pow(pAmb / s, 1.0 / _gamma);
double cGhost = Math.Sqrt(_gamma * pAmb / rhoGhost);
// Outgoing Riemann invariant J⁻ = uInt - 2*cInt/(γ-1) (for left boundary)
double J_minus = uInt - 2.0 * cInt / (_gamma - 1.0);
double uGhost = J_minus + 2.0 * cGhost / (_gamma - 1.0);
// Prevent spurious inflow by clipping to zero
if (uGhost < 0) uGhost = 0;
HLLCFlux(rhoGhost, uGhost, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
}
private void OpenEndFluxB(double rhoInt, double uInt, double pInt, double pAmb,
out double fm, out double fp, out double fe)
{
double cInt = Math.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12));
if (uInt >= cInt) // supersonic outflow (extrapolation)
{
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1) * rhoInt) + 0.5 * uInt * uInt) + pInt) * uInt;
return;
}
else if (uInt >= 0) // subsonic outflow
{
double s = pInt / Math.Pow(rhoInt, _gamma);
double rhoGhost = Math.Pow(pAmb / s, 1.0 / _gamma);
double cGhost = Math.Sqrt(_gamma * pAmb / rhoGhost);
// Outgoing Riemann invariant J⁺ = uInt + 2*cInt/(γ-1) (for right boundary)
double J_plus = uInt + 2.0 * cInt / (_gamma - 1.0);
double uGhost = J_plus - 2.0 * cGhost / (_gamma - 1.0);
// Clip to zero to prevent inflow
if (uGhost > 0) uGhost = 0;
HLLCFlux(rhoInt, uInt, pInt, rhoGhost, uGhost, pAmb, out fm, out fp, out fe);
}
else // subsonic inflow
{
double T0 = 300.0;
double R = 287.0;
double rhoGhost = pAmb / (R * T0);
HLLCFlux(rhoInt, uInt, pInt, rhoGhost, 0.0, pAmb, out fm, out fp, out fe);
}
}
// ========== Closed end (mirror) ==========
private void ClosedEndFlux(double rhoInt, double uInt, double pInt, bool isRightBoundary,
out double fm, out double fp, out double fe)
{
double rhoGhost = rhoInt;
double pGhost = pInt;
double uGhost = -uInt; // mirror velocity
if (isRightBoundary)
HLLCFlux(rhoInt, uInt, pInt, rhoGhost, uGhost, pGhost, out fm, out fp, out fe);
else
HLLCFlux(rhoGhost, uGhost, pGhost, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
// ========== Standard HLLC flux ==========
private void HLLCFlux(double rL, double uL, double pL,
double rR, double uR, double pR,
out double fm, out double fp, out double fe) out double fm, out double fp, out double fe)
{ {
double cL = Math.Sqrt(_gamma * pL / Math.Max(rL, 1e-12)); double cL = Math.Sqrt(_gamma * pL / Math.Max(rL, 1e-12));
@@ -385,7 +273,6 @@ namespace FluidSim.Components
double ER = pR / ((_gamma - 1) * rR) + 0.5 * uR * uR; double ER = pR / ((_gamma - 1) * rR) + 0.5 * uR * uR;
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 Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) double Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR))
/ (rL * (SL - uL) - rR * (SR - uR)); / (rL * (SL - uL) - rR * (SR - uR));
@@ -404,17 +291,76 @@ namespace FluidSim.Components
else else
{ {
double rsR = rR * (SR - uR) / (SR - Ss); double rsR = rR * (SR - uR) / (SR - Ss);
double ps = pL + rL * (SL - uL) * (Ss - uL); double ps = pR + rR * (SR - uR) * (Ss - uR);
double EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR))); double EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR)));
fm = rsR * Ss; fp = rsR * Ss * Ss + ps; fe = (rsR * EsR + ps) * Ss; fm = rsR * Ss; fp = rsR * Ss * Ss + ps; fe = (rsR * EsR + ps) * Ss;
} }
} }
public double GetPressureAtFraction(double fraction) // ---------- Characteristic openend boundaries ----------
private void OpenEndFluxLeft(double rhoInt, double uInt, double pInt, double pAmb,
out double fm, out double fp, out double fe)
{ {
int i = (int)(fraction * (_n - 1)); double cInt = Math.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12));
i = Math.Clamp(i, 0, _n - 1); if (uInt <= -cInt) // supersonic inflow
return Pressure(i); {
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1) * rhoInt) + 0.5 * uInt * uInt) + pInt) * uInt;
return;
}
if (uInt <= 0) // subsonic inflow
{
double T0 = 300.0, R = 287.0;
double ghost_Rho = pAmb / (R * T0);
HLLCFlux(ghost_Rho, 0.0, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
return;
}
// subsonic outflow
double s = pInt / Math.Pow(rhoInt, _gamma);
double ghostRho = Math.Pow(pAmb / s, 1.0 / _gamma);
double cGhost = Math.Sqrt(_gamma * pAmb / ghostRho);
double J_minus = uInt - 2.0 * cInt / (_gamma - 1.0);
double uGhost = J_minus + 2.0 * cGhost / (_gamma - 1.0);
if (uGhost < 0) uGhost = 0;
HLLCFlux(ghostRho, uGhost, pAmb, rhoInt, uInt, pInt, out fm, out fp, out fe);
}
private void OpenEndFluxRight(double rhoInt, double uInt, double pInt, double pAmb,
out double fm, out double fp, out double fe)
{
double cInt = Math.Sqrt(_gamma * pInt / Math.Max(rhoInt, 1e-12));
if (uInt >= cInt) // supersonic outflow
{
fm = rhoInt * uInt;
fp = rhoInt * uInt * uInt + pInt;
fe = (rhoInt * (pInt / ((_gamma - 1) * rhoInt) + 0.5 * uInt * uInt) + pInt) * uInt;
return;
}
if (uInt >= 0) // subsonic outflow
{
double s = pInt / Math.Pow(rhoInt, _gamma);
double ghost_Rho = Math.Pow(pAmb / s, 1.0 / _gamma);
double cGhost = Math.Sqrt(_gamma * pAmb / ghost_Rho);
double J_plus = uInt + 2.0 * cInt / (_gamma - 1.0);
double uGhost = J_plus - 2.0 * cGhost / (_gamma - 1.0);
if (uGhost > 0) uGhost = 0;
HLLCFlux(rhoInt, uInt, pInt, ghost_Rho, uGhost, pAmb, out fm, out fp, out fe);
}
// subsonic inflow
double T0 = 300.0, R = 287.0;
double ghostRho = pAmb / (R * T0);
HLLCFlux(rhoInt, uInt, pInt, ghostRho, 0.0, pAmb, out fm, out fp, out fe);
}
private void ClosedEndFlux(double rhoInt, double uInt, double pInt, bool isRight,
out double fm, out double fp, out double fe)
{
double rhoGhost = rhoInt, pGhost = pInt, uGhost = -uInt;
if (isRight)
HLLCFlux(rhoInt, uInt, pInt, rhoGhost, uGhost, pGhost, out fm, out fp, out fe);
else
HLLCFlux(rhoGhost, uGhost, pGhost, rhoInt, uInt, pInt, out fm, out fp, out fe);
} }
} }
} }

View File

@@ -1,21 +1,17 @@
using System; using System;
using FluidSim.Interfaces;
using FluidSim.Utils;
namespace FluidSim.Components namespace FluidSim.Components
{ {
public class Volume0D public class Volume0D
{ {
public Port Port { get; private set; } public double Mass { get; set; }
public double InternalEnergy { get; set; }
public double Mass { get; private set; }
public double InternalEnergy { get; private 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; } public double Volume { get; set; }
public double dVdt { get; set; } public double Dvdt { get; set; }
private double _dt; private double _dt;
@@ -24,6 +20,9 @@ namespace FluidSim.Components
public double Temperature => Pressure / (Density * GasConstant); public double Temperature => Pressure / (Density * GasConstant);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density; public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density;
public double MassFlowRateIn { get; set; }
public double SpecificEnthalpyIn { get; set; }
public Volume0D(double initialVolume, double initialPressure, public Volume0D(double initialVolume, double initialPressure,
double initialTemperature, int sampleRate, double initialTemperature, int sampleRate,
double gasConstant = 287.0, double gamma = 1.4) double gasConstant = 287.0, double gamma = 1.4)
@@ -31,54 +30,38 @@ namespace FluidSim.Components
GasConstant = gasConstant; GasConstant = gasConstant;
Gamma = gamma; Gamma = gamma;
Volume = initialVolume; Volume = initialVolume;
dVdt = 0.0; Dvdt = 0.0;
_dt = 1.0 / sampleRate; _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()
{
Port.Pressure = Pressure;
Port.Density = Density;
Port.Temperature = Temperature;
Port.SpecificEnthalpy = SpecificEnthalpy;
}
// Original integrate (uses the constructors sample rate)
public void Integrate()
{
Integrate(_dt);
}
public void SetPressure(double newPressure)
{
InternalEnergy = newPressure * Volume / (Gamma - 1.0);
// Mass stays the same, so density is unchanged
}
// New overload: integrate with a custom time step (for substeps)
public void Integrate(double dtOverride) public void Integrate(double dtOverride)
{ {
double mdot = Port.MassFlowRate; double dm = MassFlowRateIn * dtOverride;
double h_in = Port.SpecificEnthalpy; double dE = (MassFlowRateIn * SpecificEnthalpyIn) * dtOverride - Pressure * Dvdt * dtOverride;
double dm = mdot * dtOverride;
double dE = (mdot * h_in) * dtOverride - Pressure * dVdt * dtOverride;
Mass += dm; Mass += dm;
InternalEnergy += dE; InternalEnergy += dE;
// Hard physical bounds prevent NaN and unphysical states // Safety: if mass becomes extremely small, reset internal energy to zero
if (Mass < 1e-12) Mass = 1e-12; if (Mass < 1e-12)
if (InternalEnergy < 1e-12) InternalEnergy = 1e-12; {
Mass = 0.0;
InternalEnergy = 0.0;
}
else if (InternalEnergy < 1e-12)
{
InternalEnergy = 0.0;
}
PushStateToPort(); // Avoid negative mass/energy
} if (Mass < 0.0) Mass = 0.0;
if (InternalEnergy < 0.0) InternalEnergy = 0.0;
}
public void Integrate() => Integrate(_dt);
} }
} }

46
Core/NozzleFlow.cs Normal file
View File

@@ -0,0 +1,46 @@
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)
{
// Default fallback (no flow)
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 < 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;
if (double.IsNaN(massFlow) || double.IsInfinity(massFlow))
massFlow = 0.0;
}
}
}

View File

@@ -0,0 +1,20 @@
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 PipeVolumeConnection(Volume0D vol, Pipe1D pipe, bool isPipeLeftEnd, double orificeArea)
{
Volume = vol;
Pipe = pipe;
IsPipeLeftEnd = isPipeLeftEnd;
OrificeArea = orificeArea;
}
}
}

View File

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using FluidSim.Components; using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core namespace FluidSim.Core
{ {
@@ -9,162 +8,80 @@ namespace FluidSim.Core
{ {
private readonly List<Volume0D> _volumes = new(); private readonly List<Volume0D> _volumes = new();
private readonly List<Pipe1D> _pipes = new(); private readonly List<Pipe1D> _pipes = new();
private readonly List<Connection> _connections = new(); private readonly List<PipeVolumeConnection> _connections = new();
private double _dt; 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 AddVolume(Volume0D v) => _volumes.Add(v);
public void AddPipe(Pipe1D p) => _pipes.Add(p); public void AddPipe(Pipe1D p) => _pipes.Add(p);
public void AddConnection(Connection c) => _connections.Add(c); public void AddConnection(PipeVolumeConnection c) => _connections.Add(c);
public void SetTimeStep(double dt) => _dt = dt; public void SetTimeStep(double dt) => _dt = dt;
/// <summary>
/// Set boundary type for a pipe end. isA = true for port A (left), false for port B (right).
/// </summary>
public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0) public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0)
{ {
if (isA) if (isA)
{ {
pipe.SetABoundaryType(type); pipe.SetABoundaryType(type);
if (type == BoundaryType.OpenEnd) if (type == BoundaryType.OpenEnd) pipe.SetAAmbientPressure(ambientPressure);
pipe.SetAAmbientPressure(ambientPressure);
} }
else else
{ {
pipe.SetBBoundaryType(type); pipe.SetBBoundaryType(type);
if (type == BoundaryType.OpenEnd) if (type == BoundaryType.OpenEnd) pipe.SetBAmbientPressure(ambientPressure);
pipe.SetBAmbientPressure(ambientPressure);
} }
} }
public float Step() public float Step()
{ {
// 1. Volumes publish state // 1. Compute nozzle flows and update volumes (once per audio sample)
foreach (var v in _volumes)
v.PushStateToPort();
// 2. Set volume BCs for volumecoupled ends
foreach (var conn in _connections) foreach (var conn in _connections)
{ {
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB)) double downstreamPressure = conn.IsPipeLeftEnd
{ ? conn.Pipe.GetCellPressure(0)
var pipe = GetPipe(conn.PortA); : conn.Pipe.GetCellPressure(conn.Pipe.GetCellCount() - 1);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) || NozzleFlow.Compute(conn.Volume, conn.OrificeArea, downstreamPressure,
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling)) out double mdot, out double rhoFace, out double uFace, out double pFace,
SetVolumeBC(conn.PortA, conn.PortB); gamma: conn.Volume.Gamma);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB)) // Limit mass flow to available mass
{ double maxMdot = conn.Volume.Mass / _dt;
var pipe = GetPipe(conn.PortB); if (mdot > maxMdot) mdot = maxMdot;
bool isA = pipe.PortB == conn.PortB; if (mdot < -maxMdot) mdot = -maxMdot;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling)) conn.Volume.MassFlowRateIn = -mdot;
SetVolumeBC(conn.PortB, conn.PortA); conn.Volume.SpecificEnthalpyIn = (conn.Volume.Gamma / (conn.Volume.Gamma - 1.0)) *
} (conn.Volume.Pressure / Math.Max(conn.Volume.Density, 1e-12));
conn.Volume.Integrate(_dt);
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
conn.Pipe.SetGhostRight(rhoFace, uFace, pFace);
} }
// 3. Substeps // 2. Determine required substeps
int nSub = 1; int nSub = 1;
foreach (var p in _pipes) foreach (var p in _pipes)
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt)); nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt));
double dtSub = _dt / nSub; double dtSub = _dt / nSub;
// 3. Substep loop for pipes
for (int sub = 0; sub < nSub; sub++) for (int sub = 0; sub < nSub; sub++)
{
foreach (var p in _pipes) foreach (var p in _pipes)
p.SimulateSingleStep(dtSub); p.SimulateSingleStep(dtSub);
foreach (var conn in _connections) // 4. Clear ghost flags
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
{
var pipe = GetPipe(conn.PortA);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
TransferAndIntegrate(conn.PortA, conn.PortB, dtSub);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
{
var pipe = GetPipe(conn.PortB);
bool isA = pipe.PortB == conn.PortB;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
TransferAndIntegrate(conn.PortB, conn.PortA, dtSub);
}
}
if (sub < nSub - 1)
{
foreach (var v in _volumes)
v.PushStateToPort();
foreach (var conn in _connections)
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
{
var pipe = GetPipe(conn.PortA);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortA, conn.PortB);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
{
var pipe = GetPipe(conn.PortB);
bool isA = pipe.PortB == conn.PortB;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortB, conn.PortA);
}
}
}
}
// 5. Audio samples (none for now, but placeholder)
var audioSamples = new List<float>();
foreach (var conn in _connections)
{
if (conn is SoundConnection sc)
audioSamples.Add(sc.GetAudioSample());
}
// 6. Clear BC flags
foreach (var p in _pipes) foreach (var p in _pipes)
p.ClearBC(); p.ClearGhostFlag();
return SoundProcessor.MixAndClip(audioSamples.ToArray()); // 5. Return raw mass flow from the first pipes open end (assumed exhaust tailpipe)
} if (_pipes.Count > 0)
return (float)_pipes[0].GetOpenEndMassFlow();
private bool IsVolumePort(Port p) => _volumes.Exists(v => v.Port == p); return 0f;
private bool IsPipePort(Port p) => _pipes.Exists(pp => pp.PortA == p || pp.PortB == p);
private Pipe1D GetPipe(Port p) => _pipes.Find(pp => pp.PortA == p || pp.PortB == p);
private Volume0D GetVolume(Port p) => _volumes.Find(v => v.Port == p);
private void SetVolumeBC(Port pipePort, Port volPort)
{
var pipe = GetPipe(pipePort);
if (pipe == null) return;
bool isA = pipe.PortA == pipePort;
if (isA)
pipe.SetAVolumeState(volPort.Density, volPort.Pressure);
else
pipe.SetBVolumeState(volPort.Density, volPort.Pressure);
}
private void TransferAndIntegrate(Port pipePort, Port volPort, double dtSub)
{
double mdot = pipePort.MassFlowRate;
volPort.MassFlowRate = -mdot;
if (mdot < 0) // pipe → volume
{
volPort.SpecificEnthalpy = pipePort.SpecificEnthalpy;
}
// else volumes own enthalpy (from PushStateToPort) is used
GetVolume(volPort)?.Integrate(dtSub);
} }
} }
} }

View File

@@ -1,23 +1,155 @@
namespace FluidSim.Core using System;
{
/// <summary>
/// Mixes multiple audio samples and applies a softclipping tanh.
/// </summary>
public static class SoundProcessor
{
/// <summary>Overall gain applied after mixing (before tanh).</summary>
public static float MasterGain { get; set; } = 0.01f;
/// <summary> namespace FluidSim.Core
/// Mixes an array of raw audio samples and returns a single sample in [1, 1].
/// </summary>
public static float MixAndClip(params float[] samples)
{ {
public class SoundProcessor
{
// Monopole state
private double lastMassFlow = 0.0;
private double dt;
// Resonant bandpass filter (secondorder)
private double b0, b1, b2, a1, a2;
private double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
private double pipeLength;
// Reverb (outdoor)
private float[] delayLine;
private int writeIndex;
private float feedback = 0.50f;
private float lowpassCoeff = 0.70f;
private float lastFeedbackSample = 0f;
// Turbulence (pink noise scaled by U³)
private PinkNoiseGenerator pinkNoise;
private float turbulenceGain = 0.05f;
private double pipeArea;
private double ambientPressure = 101325.0;
// Gains
private float masterGain = 0.0005f;
private float pressureGain = 0.12f;
public SoundProcessor(int sampleRate, double pipeLengthMeters, double pipeDiameterMeters = 0.04, float reverbTimeMs = 200.0f)
{
dt = 1.0 / sampleRate;
pipeLength = pipeLengthMeters;
pipeArea = Math.PI * Math.Pow(pipeDiameterMeters / 2.0, 2.0);
// Design resonant filter at pipe fundamental frequency
double c = 340.0;
double f0 = c / (4.0 * pipeLength);
double Q = 15.0;
double omega = 2.0 * Math.PI * f0;
double alpha = Math.Sin(omega * dt) / (2.0 * Q);
double norm = 1.0 / (1.0 + alpha);
b0 = alpha * norm;
b1 = 0.0;
b2 = -alpha * norm;
a1 = -2.0 * Math.Cos(omega * dt) * norm;
a2 = (1.0 - alpha) * norm;
// Reverb delay line
int delaySamples = (int)(sampleRate * reverbTimeMs / 1000.0);
delayLine = new float[delaySamples];
writeIndex = 0;
pinkNoise = new PinkNoiseGenerator();
}
public float MasterGain
{
get => masterGain;
set => masterGain = value;
}
public float PressureGain
{
get => pressureGain;
set => pressureGain = value;
}
public float TurbulenceGain
{
get => turbulenceGain;
set => turbulenceGain = value;
}
public void SetAmbientPressure(double p) => ambientPressure = p;
public void SetPipeDiameter(double diameterMeters) => pipeArea = Math.PI * Math.Pow(diameterMeters / 2.0, 2.0);
public float Process(float massFlow, float pipeEndPressure)
{
// 1. Monopole source: d(mdot)/dt
double derivative = (massFlow - lastMassFlow) / dt;
lastMassFlow = massFlow;
float monopole = (float)(derivative * masterGain);
// 2. Pressure difference (lowfrequency component)
float pressureDiff = (float)((pipeEndPressure - ambientPressure) / ambientPressure) * pressureGain;
float mixed = monopole + pressureDiff;
// DO NOT clamp here let the filter and final clamp handle dynamics
// 3. Resonant bandpass filter
double y = b0 * mixed + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1; x1 = mixed;
y2 = y1; y1 = y;
float resonant = (float)Math.Clamp(y, -1f, 1f);
// 4. Turbulence noise: amplitude ∝ U³ (empirical for low speeds)
double velocity = massFlow / (pipeArea * 1.225);
double Uref = 100.0;
double turbulenceAmp = Math.Pow(Math.Abs(velocity) / Uref, 3.0);
float pink = pinkNoise.Next() * turbulenceGain * (float)turbulenceAmp;
resonant += pink;
resonant = Math.Clamp(resonant, -1f, 1f);
// 5. Outdoor reverb
float delayed = delayLine[writeIndex];
float filteredDelay = delayed * lowpassCoeff + lastFeedbackSample * (1f - lowpassCoeff);
lastFeedbackSample = filteredDelay;
float wet = delayed + filteredDelay * feedback;
delayLine[writeIndex] = resonant + filteredDelay * feedback;
writeIndex = (writeIndex + 1) % delayLine.Length;
// 6. Dry/wet mix
float output = resonant * 0.7f + wet * 0.3f;
output = Math.Clamp(output, -1f, 1f);
return output;
}
}
internal class PinkNoiseGenerator
{
private readonly Random random = new Random();
private readonly float[] whiteNoise = new float[7];
private int currentIndex = 0;
public PinkNoiseGenerator()
{
for (int i = 0; i < 7; i++)
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
}
public float Next()
{
whiteNoise[0] = (float)(random.NextDouble() * 2.0 - 1.0);
currentIndex = (currentIndex + 1) & 0x7F;
int updateMask = 0;
int temp = currentIndex;
for (int i = 0; i < 7; i++)
{
if ((temp & 1) == 0)
updateMask |= (1 << i);
temp >>= 1;
}
for (int i = 1; i < 7; i++)
if ((updateMask & (1 << i)) != 0)
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
float sum = 0f; float sum = 0f;
foreach (float s in samples) for (int i = 0; i < 7; i++) sum += whiteNoise[i];
sum += s; return sum / 3.5f;
sum *= MasterGain;
return sum;
} }
} }
} }

View File

@@ -13,23 +13,23 @@ public class Program
private static Scenario scenario; private static Scenario scenario;
// Speed control // Speed control
//private static double desiredSpeed = 1.0;
private static double desiredSpeed = 0.0001; private static double desiredSpeed = 0.0001;
//private static double desiredSpeed = 1;
private static double currentSpeed = desiredSpeed; private static double currentSpeed = desiredSpeed;
private const double MinSpeed = 0.0001; private const double MinSpeed = 0.0001;
private const double MaxSpeed = 1.0; private const double MaxSpeed = 1.0;
private const double ScrollFactor = 1.1; private const double ScrollFactor = 1.1;
// Spacetoggle state // Spacetoggle state
private static double lastDesiredSpeed = 0.1; // remembers the last non1.0 scroll speed private static double lastDesiredSpeed = 0.1; // remembers the last non1.0 speed
private static bool isRealTime = true; // true when desiredSpeed == 1.0 private static bool isRealTime = false; // starts in slowmo (desiredSpeed != 1.0)
private static volatile bool running = true; private static volatile bool running = true;
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, "Pipe Resonator"); var window = new RenderWindow(mode, "FluidSim - Helmholtz Resonator");
window.SetVerticalSyncEnabled(true); window.SetVerticalSyncEnabled(true);
window.Closed += (_, _) => { running = false; window.Close(); }; window.Closed += (_, _) => { running = false; window.Close(); };
window.MouseWheelScrolled += OnMouseWheel; window.MouseWheelScrolled += OnMouseWheel;
@@ -39,9 +39,11 @@ public class Program
soundEngine.Volume = 70; soundEngine.Volume = 70;
soundEngine.Start(); soundEngine.Start();
//scenario = new PipeResonatorScenario(); // Choose one scenario. The Helmholtz resonator is fully updated.
//scenario = new HelmholtzResonatorScenario(); //scenario = new HelmholtzResonatorScenario();
scenario = new SodShockTubeScenario(); //scenario = new PipeResonatorScenario(); // needs update to new API
//scenario = new SodShockTubeScenario(); // needs update to new API
scenario = new EngineScenario(); // also works (provided earlier)
scenario.Initialize(SampleRate); scenario.Initialize(SampleRate);
@@ -51,9 +53,10 @@ public class Program
double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds; double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds;
// Resampling buffer // Resampling buffer
List<float> simBuffer = new List<float>(4096); var simBuffer = new List<float>(4096);
double readIndex = 0.0; double readIndex = 0.0;
// Prime the buffer with a few samples
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
simBuffer.Add(scenario.Process()); simBuffer.Add(scenario.Process());
@@ -73,7 +76,6 @@ public class Program
lastSpeedUpdateTime = currentRealTime; lastSpeedUpdateTime = currentRealTime;
// Smoothly transition currentSpeed → desiredSpeed // Smoothly transition currentSpeed → desiredSpeed
// When toggling, desiredSpeed jumps, but currentSpeed follows with a smooth lerp
double smoothingRate = 8.0; // higher = faster catchup double smoothingRate = 8.0; // higher = faster catchup
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-smoothingRate * dtSpeed)); currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-smoothingRate * dtSpeed));
@@ -122,7 +124,7 @@ public class Program
break; break;
} }
// ---------- Drawing & title ---------- // ---------- Drawing & window title ----------
if (currentRealTime - lastDrawTime >= drawInterval) if (currentRealTime - lastDrawTime >= drawInterval)
{ {
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate); double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
@@ -156,7 +158,7 @@ public class Program
desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed); desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed);
// Update the remembered slow-mo speed (unless we are exactly at 1.0) // Update the remembered slowmo speed (unless we are exactly at 1.0)
if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6) if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6)
lastDesiredSpeed = desiredSpeed; lastDesiredSpeed = desiredSpeed;

238
Scenarios/EngineScenario.cs Normal file
View File

@@ -0,0 +1,238 @@
using System;
using FluidSim.Components;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Volume0D cylinder;
private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
private double dt;
private double ambientPressure = 101325.0;
private double time;
// Crankshaft
private double crankAngle = 0.0;
private const double TargetRPM = 4000.0;
private double angularVelocity;
// Combustion
private const double CombustionPressure = 8.0 * 101325.0;
private const double CombustionTemperature = 1800.0;
// Valve timing
private const double ValveOpenStart = 120.0 * Math.PI / 180.0;
private const double ValveOpenEnd = 480.0 * Math.PI / 180.0;
private const double ValveRampWidth = 30.0 * Math.PI / 180.0;
private double maxOrificeArea;
// Misfire
private Random rand = new Random();
private const double MisfireProbability = 0.02;
private bool isMisfiring = false;
// Lowpass filter for pressure
private double lastFilteredPressure;
private const double PressureCutoffHz = 50.0;
// Logging
private int stepCount = 0;
private const int LogStepInterval = 1000;
private int combustionCount = 0;
private int misfireCount = 0;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
angularVelocity = TargetRPM * 2.0 * Math.PI / 60.0;
// Cylinder: 0.5 litre, initially at ambient
double cylVolume = 0.5e-3;
double initialPressure = ambientPressure;
double initialTemperature = 300.0;
cylinder = new Volume0D(cylVolume, initialPressure, initialTemperature, sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
// Exhaust pipe: length 2.5 m, radius 2 cm
double pipeLength = 2.5;
double pipeRadius = 0.02;
double pipeArea = Math.PI * pipeRadius * pipeRadius;
maxOrificeArea = pipeArea;
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 70);
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
// Coupling (valve starts closed)
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(cylinder);
solver.AddPipe(exhaustPipe);
solver.AddConnection(coupling);
// Use ZeroPressureOpen for strong reflections
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.ZeroPressureOpen, ambientPressure);
// Sound processor (tuned to pipe length)
soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f);
soundProcessor.MasterGain = 0.02f; // boosted from 0.0008
soundProcessor.PressureGain = 4.0f; // boosted from6 0.12
soundProcessor.TurbulenceGain = 0.0002f; // reduced from 0.02
soundProcessor.SetAmbientPressure(ambientPressure);
lastFilteredPressure = ambientPressure;
Console.WriteLine("=== EngineScenario (ZeroPressureOpen, boosted gains) ===");
Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%");
Console.WriteLine($"Pipe length: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
Console.WriteLine($"Valve opens at {ValveOpenStart*180/Math.PI:F0}°, closes at {ValveOpenEnd*180/Math.PI:F0}°, ramp {ValveRampWidth*180/Math.PI:F0}°");
Console.WriteLine($"Sample rate: {sampleRate} Hz, dt = {dt*1000:F3} ms");
Console.WriteLine("Time[s] Crank[°] Valve[%] MassFlow[kg/s] Comb# Misfire");
Console.WriteLine("---------------------------------------------------------");
}
private double ValveOpenRatio(double crankRad)
{
double cycleAngle = crankRad % (4.0 * Math.PI);
double openStart = ValveOpenStart;
double openEnd = ValveOpenEnd;
if (cycleAngle < openStart || cycleAngle > openEnd)
return 0.0;
double fullOpenWindow = openEnd - openStart;
double closedWindow = 2.0 * ValveRampWidth;
if (fullOpenWindow <= closedWindow)
return 1.0;
double tmid = (openStart + openEnd) / 2.0;
double dist = Math.Abs(cycleAngle - tmid);
double rampHalf = (fullOpenWindow - closedWindow) / 2.0;
if (dist <= rampHalf)
return 1.0;
else
{
double frac = (dist - rampHalf) / ValveRampWidth;
frac = Math.Clamp(frac, 0.0, 1.0);
double lift = Math.Cos(frac * Math.PI / 2.0);
return lift * lift;
}
}
public override float Process()
{
// Update crank angle
crankAngle += angularVelocity * dt;
if (crankAngle >= 2.0 * Math.PI)
{
crankAngle -= 2.0 * Math.PI;
isMisfiring = rand.NextDouble() < MisfireProbability;
}
// Power stroke at TDC
if (crankAngle < angularVelocity * dt && crankAngle >= 0.0)
{
if (isMisfiring)
{
double vol = cylinder.Volume;
double R = cylinder.GasConstant;
double T0 = 300.0;
double newMass = ambientPressure * vol / (R * T0);
double newInternalEnergy = ambientPressure * vol / (cylinder.Gamma - 1.0);
cylinder.Mass = newMass;
cylinder.InternalEnergy = newInternalEnergy;
misfireCount++;
}
else
{
double volume = cylinder.Volume;
double gamma = cylinder.Gamma;
double newInternalEnergy = CombustionPressure * volume / (gamma - 1.0);
double R = cylinder.GasConstant;
double newMass = CombustionPressure * volume / (R * CombustionTemperature);
cylinder.InternalEnergy = newInternalEnergy;
cylinder.Mass = newMass;
combustionCount++;
}
}
// Update valve area
double valveOpen = ValveOpenRatio(crankAngle);
coupling.OrificeArea = maxOrificeArea * valveOpen;
float massFlow = solver.Step();
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
// Lowpass filter the pressure (emphasise low frequencies)
double rc = 1.0 / (2.0 * Math.PI * PressureCutoffHz);
double alpha = dt / (rc + dt);
double filteredPressure = alpha * endPressure + (1.0 - alpha) * lastFilteredPressure;
lastFilteredPressure = filteredPressure;
float audioSample = soundProcessor.Process(massFlow, (float)filteredPressure);
time += dt;
stepCount++;
// Logging
if (stepCount % LogStepInterval == 0 || (crankAngle < angularVelocity * dt * 2 && !isMisfiring && combustionCount > 0))
{
Console.WriteLine($"{time,7:F3} {crankAngle * 180.0 / Math.PI,6:F1} " +
$"{valveOpen * 100,6:F1} {massFlow,10:F4} " +
$"{combustionCount,3} {(isMisfiring ? "X" : "")}");
}
return audioSample;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(40f, centerY - cylH / 2f);
double pNorm = (cylinder.Pressure - ambientPressure) / ambientPressure;
if (double.IsNaN(pNorm)) pNorm = 0;
byte red = (byte)(Math.Clamp(pNorm * 128, 0, 255));
byte blue = (byte)(Math.Clamp(-pNorm * 128, 0, 255));
cylRect.FillColor = new Color(red, 0, blue);
target.Draw(cylRect);
int n = exhaustPipe.GetCellCount();
float pipeStartX = 120f, pipeEndX = winW - 60f;
float pipeLen = pipeEndX - pipeStartX;
float dx = pipeLen / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = exhaustPipe.GetCellPressure(i);
float r = baseRadius * (float)(1.0 + (p - ambientPressure) / ambientPressure);
if (r < 2f) r = 2f;
double t = (p - ambientPressure) / ambientPressure;
t = Math.Clamp(t, -1.0, 1.0);
byte rCol = (byte)(t > 0 ? 255 * t : 0);
byte bCol = (byte)(t < 0 ? -255 * t : 0);
byte gCol = (byte)(255 * (1 - Math.Abs(t)));
var col = new Color(rCol, gCol, bCol);
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,6 +1,5 @@
using System; using System;
using FluidSim.Components; using FluidSim.Components;
using FluidSim.Interfaces;
using FluidSim.Utils; using FluidSim.Utils;
using SFML.Graphics; using SFML.Graphics;
using SFML.System; using SFML.System;
@@ -12,7 +11,7 @@ namespace FluidSim.Core
private Solver solver; private Solver solver;
private Volume0D cavity; private Volume0D cavity;
private Pipe1D neck; private Pipe1D neck;
private Connection coupling; private PipeVolumeConnection coupling;
private int stepCount; private int stepCount;
private double time; private double time;
private double dt; private double dt;
@@ -38,12 +37,8 @@ namespace FluidSim.Core
neck = new Pipe1D(neckLength, neckArea, sampleRate, forcedCellCount: 40); neck = new Pipe1D(neckLength, neckArea, sampleRate, forcedCellCount: 40);
neck.SetUniformState(1.225, 0.0, ambientPressure); neck.SetUniformState(1.225, 0.0, ambientPressure);
coupling = new Connection(neck.PortA, cavity.Port) // Create the coupling between cavity and left end of the neck (PortA)
{ coupling = new PipeVolumeConnection(cavity, neck, isPipeLeftEnd: true, orificeArea: neckArea);
Area = neckArea,
DischargeCoefficient = 0.62,
Gamma = 1.4
};
solver = new Solver(); solver = new Solver();
solver.SetTimeStep(dt); solver.SetTimeStep(dt);
@@ -51,8 +46,8 @@ namespace FluidSim.Core
solver.AddPipe(neck); solver.AddPipe(neck);
solver.AddConnection(coupling); solver.AddConnection(coupling);
// Port A (left) = volume coupling, Port B (right) = open end // Left boundary (PortA) is volumecoupled via ghost cell, right boundary (PortB) is open end
solver.SetPipeBoundary(neck, isA: true, BoundaryType.VolumeCoupling); solver.SetPipeBoundary(neck, isA: true, BoundaryType.GhostCell);
solver.SetPipeBoundary(neck, isA: false, BoundaryType.OpenEnd, ambientPressure); solver.SetPipeBoundary(neck, isA: false, BoundaryType.OpenEnd, ambientPressure);
} }
@@ -68,11 +63,11 @@ namespace FluidSim.Core
if (stepCount % 20 == 0) if (stepCount % 20 == 0)
{ {
double pCav = cavity.Pressure; double pCav = cavity.Pressure;
double mdotA = neck.PortA.MassFlowRate; // positive = into pipe (leaving cavity) // Mass flow rate is not directly available we can compute from pressure difference or skip
Console.WriteLine( Console.WriteLine(
$"t={time * 1e3:F2} ms step={stepCount} " + $"t={time * 1e3:F2} ms step={stepCount} " +
$"P_cav={pCav:F1} Pa, P_open={pOpen:F1} Pa, " + $"P_cav={pCav:F1} Pa, P_open={pOpen:F1} Pa, " +
$"mdot_A={mdotA * 1e3:F4} g/s, audio={audio:F4}"); $"audio={audio:F4}");
} }
return audio; return audio;
@@ -100,7 +95,7 @@ namespace FluidSim.Core
float dx = neckLenPx / (n - 1); float dx = neckLenPx / (n - 1);
float baseRadius = 20f; float baseRadius = 20f;
Vertex[] vertices = new Vertex[n * 2]; var vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
float x = neckStartX + i * dx; float x = neckStartX + i * dx;