General testing
This commit is contained in:
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal 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
41
.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ namespace FluidSim.Components
|
|||||||
{
|
{
|
||||||
public enum BoundaryType
|
public enum BoundaryType
|
||||||
{
|
{
|
||||||
VolumeCoupling,
|
|
||||||
OpenEnd,
|
OpenEnd,
|
||||||
ClosedEnd
|
ZeroPressureOpen, // pressure‑release 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;
|
||||||
|
|
||||||
// Volume‑coupling 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));
|
|
||||||
|
|
||||||
// ========== Characteristic‑based 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 open‑end 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 constructor’s 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 sub‑steps)
|
|
||||||
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
46
Core/NozzleFlow.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Core/PipeVolumeConnection.cs
Normal file
20
Core/PipeVolumeConnection.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Core/Solver.cs
157
Core/Solver.cs
@@ -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 volume‑coupled 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. Sub‑steps
|
// 2. Determine required sub‑steps
|
||||||
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. Sub‑step 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 pipe’s 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 volume’s own enthalpy (from PushStateToPort) is used
|
|
||||||
|
|
||||||
GetVolume(volPort)?.Integrate(dtSub);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,155 @@
|
|||||||
namespace FluidSim.Core
|
using System;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Mixes multiple audio samples and applies a soft‑clipping 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 band‑pass filter (second‑order)
|
||||||
|
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 (low‑frequency 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 band‑pass 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
22
Program.cs
22
Program.cs
@@ -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;
|
||||||
|
|
||||||
// Space‑toggle state
|
// Space‑toggle state
|
||||||
private static double lastDesiredSpeed = 0.1; // remembers the last non‑1.0 scroll speed
|
private static double lastDesiredSpeed = 0.1; // remembers the last non‑1.0 speed
|
||||||
private static bool isRealTime = true; // true when desiredSpeed == 1.0
|
private static bool isRealTime = false; // starts in slow‑mo (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 catch‑up
|
double smoothingRate = 8.0; // higher = faster catch‑up
|
||||||
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 slow‑mo 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
238
Scenarios/EngineScenario.cs
Normal 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;
|
||||||
|
|
||||||
|
// Low‑pass 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);
|
||||||
|
|
||||||
|
// Low‑pass 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 volume‑coupled 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user