Helmholtz testing (no decay bug)
This commit is contained in:
330
Core/BoundarySystem.cs
Normal file
330
Core/BoundarySystem.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class BoundarySystem
|
||||
{
|
||||
public struct OrificeDesc
|
||||
{
|
||||
public Port VolumePort;
|
||||
public int PipeIndex;
|
||||
public bool IsLeftEnd;
|
||||
public int AreaIndex;
|
||||
public float DischargeCoeff;
|
||||
|
||||
// --- Inertance support ---
|
||||
public bool UseInertance;
|
||||
public float EffectiveLength;
|
||||
public float CurrentMdot; // kg/s, positive = volume → pipe
|
||||
|
||||
// --- Dissipative loss ---
|
||||
public float LossCoefficient; // K factor for pressure drop = K * 0.5*rho*u^2
|
||||
}
|
||||
|
||||
public struct OpenEndDesc
|
||||
{
|
||||
public int PipeIndex;
|
||||
public bool IsLeftEnd;
|
||||
public float AmbientPressure;
|
||||
public float Gamma;
|
||||
public float PipeArea;
|
||||
public float LastMassFlowRate;
|
||||
public float LastFacePressure;
|
||||
}
|
||||
|
||||
private OrificeDesc[] _orifices;
|
||||
private OpenEndDesc[] _openEnds;
|
||||
private float[] _orificeAreas;
|
||||
private PipeSystem _pipeSystem;
|
||||
|
||||
public BoundarySystem(PipeSystem pipeSystem, int maxOrifices, int maxOpenEnds)
|
||||
{
|
||||
_pipeSystem = pipeSystem;
|
||||
_orifices = new OrificeDesc[maxOrifices];
|
||||
_openEnds = new OpenEndDesc[maxOpenEnds];
|
||||
_orificeAreas = new float[maxOrifices];
|
||||
}
|
||||
|
||||
public int OrificeCount { get; private set; }
|
||||
public int OpenEndCount { get; private set; }
|
||||
|
||||
public void AddOrifice(Port volumePort, int pipeIndex, bool isLeftEnd,
|
||||
int areaIndex, float dischargeCoeff = 1f,
|
||||
float lossCoefficient = 0f)
|
||||
{
|
||||
_orifices[OrificeCount] = new OrificeDesc
|
||||
{
|
||||
VolumePort = volumePort,
|
||||
PipeIndex = pipeIndex,
|
||||
IsLeftEnd = isLeftEnd,
|
||||
AreaIndex = areaIndex,
|
||||
DischargeCoeff = dischargeCoeff,
|
||||
UseInertance = false,
|
||||
EffectiveLength = 0f,
|
||||
CurrentMdot = 0f,
|
||||
LossCoefficient = lossCoefficient
|
||||
};
|
||||
OrificeCount++;
|
||||
}
|
||||
|
||||
public void AddOrificeWithInertance(Port volumePort, int pipeIndex, bool isLeftEnd,
|
||||
int areaIndex, float dischargeCoeff,
|
||||
float effectiveLength, float lossCoefficient = 0f)
|
||||
{
|
||||
AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff, lossCoefficient);
|
||||
ref var d = ref _orifices[OrificeCount - 1];
|
||||
d.UseInertance = true;
|
||||
d.EffectiveLength = effectiveLength;
|
||||
}
|
||||
|
||||
public void AddOpenEnd(int pipeIndex, bool isLeftEnd,
|
||||
float ambientPressure, float pipeArea, float gamma = 1.4f)
|
||||
{
|
||||
int idx = OpenEndCount;
|
||||
_openEnds[idx] = new OpenEndDesc
|
||||
{
|
||||
PipeIndex = pipeIndex,
|
||||
IsLeftEnd = isLeftEnd,
|
||||
AmbientPressure = ambientPressure,
|
||||
Gamma = gamma,
|
||||
PipeArea = pipeArea
|
||||
};
|
||||
OpenEndCount++;
|
||||
}
|
||||
|
||||
public void SetOrificeAreas(float[] areas)
|
||||
{
|
||||
for (int i = 0; i < OrificeCount; i++)
|
||||
_orificeAreas[i] = areas[i];
|
||||
}
|
||||
|
||||
public float GetOpenEndMassFlow(int openEndIndex)
|
||||
{
|
||||
if (openEndIndex < 0 || openEndIndex >= OpenEndCount) return 0f;
|
||||
return _openEnds[openEndIndex].LastMassFlowRate;
|
||||
}
|
||||
|
||||
public float GetOpenEndPressure(int openEndIndex)
|
||||
{
|
||||
if (openEndIndex < 0 || openEndIndex >= OpenEndCount) return 101325f;
|
||||
return _openEnds[openEndIndex].LastFacePressure;
|
||||
}
|
||||
|
||||
public void ResolveOrifices(float dt)
|
||||
{
|
||||
for (int i = 0; i < OrificeCount; i++)
|
||||
{
|
||||
ref var d = ref _orifices[i];
|
||||
float area = _orificeAreas[d.AreaIndex];
|
||||
if (area < 1e-12f || d.VolumePort == null)
|
||||
{
|
||||
// Closed wall – reflect interior state
|
||||
var (rInt, uInt, pInt) = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
|
||||
float afInt = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
|
||||
|
||||
if (d.IsLeftEnd)
|
||||
_pipeSystem.SetGhostLeft(d.PipeIndex, rInt, -uInt, pInt, afInt);
|
||||
else
|
||||
_pipeSystem.SetGhostRight(d.PipeIndex, rInt, -uInt, pInt, afInt);
|
||||
|
||||
if (d.VolumePort != null) d.VolumePort.MassFlowRate = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gather states
|
||||
float volP = d.VolumePort.Pressure;
|
||||
float volRho = d.VolumePort.Density;
|
||||
float volT = d.VolumePort.Temperature;
|
||||
float volH = d.VolumePort.SpecificEnthalpy;
|
||||
float volAF = d.VolumePort.AirFraction;
|
||||
|
||||
var (pipeRho, pipeU, pipeP) = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
|
||||
float pipeT = pipeP / MathF.Max(pipeRho * 287f, 1e-12f);
|
||||
float pipeAF = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
|
||||
|
||||
float gamma = 1.4f, R = 287f, Cd = d.DischargeCoeff;
|
||||
|
||||
// --- Preliminary nozzle solution (no loss) to estimate flow direction and velocity ---
|
||||
float mdotEst, rhoFaceEst, uFaceEst, pFaceEst;
|
||||
if (volP >= pipeP)
|
||||
{
|
||||
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, Cd,
|
||||
out mdotEst, out rhoFaceEst, out uFaceEst, out pFaceEst);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, Cd,
|
||||
out mdotEst, out rhoFaceEst, out uFaceEst, out pFaceEst);
|
||||
mdotEst = -mdotEst;
|
||||
}
|
||||
|
||||
// --- Apply symmetric loss if LossCoefficient > 0 ---
|
||||
float volP_eff = volP;
|
||||
float pipeP_eff = pipeP;
|
||||
if (d.LossCoefficient > 0f && MathF.Abs(mdotEst) > 1e-12f)
|
||||
{
|
||||
float rhoRef = mdotEst >= 0 ? rhoFaceEst : rhoFaceEst; // rhoFaceEst already reflects the correct side
|
||||
float uRef = uFaceEst;
|
||||
float dynP = 0.5f * rhoRef * uRef * uRef * d.LossCoefficient;
|
||||
|
||||
// Clamp the loss to avoid overshoot (max 80% of pressure difference)
|
||||
float dp = MathF.Abs(volP - pipeP);
|
||||
dynP = MathF.Min(dynP, 0.8f * dp);
|
||||
|
||||
// Apply symmetrically: loss reduces the higher pressure and increases the lower pressure
|
||||
if (mdotEst >= 0) // volume → pipe
|
||||
{
|
||||
volP_eff -= dynP;
|
||||
pipeP_eff += dynP;
|
||||
}
|
||||
else // pipe → volume
|
||||
{
|
||||
pipeP_eff -= dynP;
|
||||
volP_eff += dynP;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Final nozzle solution with corrected pressures ---
|
||||
float mdotSS, rhoFace0, uFace0, pFace0;
|
||||
if (volP_eff >= pipeP_eff)
|
||||
{
|
||||
IsentropicOrifice.Compute(volP_eff, volRho, volT, pipeP_eff, gamma, R, area, Cd,
|
||||
out float mUp, out rhoFace0, out uFace0, out pFace0);
|
||||
mdotSS = mUp;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsentropicOrifice.Compute(pipeP_eff, pipeRho, pipeT, volP_eff, gamma, R, area, Cd,
|
||||
out float mUp, out rhoFace0, out uFace0, out pFace0);
|
||||
mdotSS = -mUp;
|
||||
}
|
||||
|
||||
float mdot;
|
||||
if (d.UseInertance)
|
||||
{
|
||||
float rhoUp = d.CurrentMdot >= 0 ? volRho : pipeRho;
|
||||
float inertance = rhoUp * d.EffectiveLength / area;
|
||||
float dp = volP_eff - pipeP_eff;
|
||||
float resistance = MathF.Abs(dp) / MathF.Max(MathF.Abs(mdotSS), 1e-12f);
|
||||
float dmdot_dt = (dp - resistance * d.CurrentMdot) / inertance;
|
||||
mdot = d.CurrentMdot + dmdot_dt * dt;
|
||||
|
||||
if (d.VolumePort.Owner is Volume0D vol0)
|
||||
{
|
||||
float maxOut = vol0.Mass / dt;
|
||||
if (mdot > maxOut) mdot = maxOut;
|
||||
}
|
||||
if (float.IsNaN(mdot)) mdot = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mdot = mdotSS;
|
||||
if (d.VolumePort.Owner is Volume0D vol0)
|
||||
{
|
||||
float maxOut = vol0.Mass / dt;
|
||||
if (mdot > maxOut) mdot = maxOut;
|
||||
}
|
||||
}
|
||||
|
||||
d.CurrentMdot = mdot; // stored for future steps (inertance or loss)
|
||||
|
||||
// Ghost state construction
|
||||
float rhoFace = mdot >= 0 ? volRho : pipeRho;
|
||||
float pFace = pFace0;
|
||||
float uFace = MathF.Abs(mdot) / MathF.Max(rhoFace * area, 1e-12f);
|
||||
float airFracGhost;
|
||||
if (mdot >= 0)
|
||||
airFracGhost = volAF;
|
||||
else
|
||||
{
|
||||
airFracGhost = pipeAF;
|
||||
d.VolumePort.AirFraction = pipeAF;
|
||||
}
|
||||
|
||||
if (mdot >= 0 && d.IsLeftEnd) uFace = +uFace;
|
||||
else if (mdot >= 0 && !d.IsLeftEnd) uFace = -uFace;
|
||||
else if (mdot < 0 && d.IsLeftEnd) uFace = -uFace;
|
||||
else if (mdot < 0 && !d.IsLeftEnd) uFace = +uFace;
|
||||
|
||||
if (d.IsLeftEnd)
|
||||
_pipeSystem.SetGhostLeft(d.PipeIndex, rhoFace, uFace, pFace, airFracGhost);
|
||||
else
|
||||
_pipeSystem.SetGhostRight(d.PipeIndex, rhoFace, uFace, pFace, airFracGhost);
|
||||
|
||||
d.VolumePort.MassFlowRate = -mdot;
|
||||
if (-mdot >= 0)
|
||||
{
|
||||
float pipeH = gamma / (gamma - 1f) * pipeP / MathF.Max(pipeRho, 1e-12f);
|
||||
d.VolumePort.SpecificEnthalpy = pipeH;
|
||||
}
|
||||
else
|
||||
{
|
||||
d.VolumePort.SpecificEnthalpy = volH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ResolveOpenEnds(float dt)
|
||||
{
|
||||
for (int i = 0; i < OpenEndCount; i++)
|
||||
{
|
||||
ref var d = ref _openEnds[i];
|
||||
|
||||
var (rhoInt, uInt, pInt) = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
|
||||
float afInt = d.IsLeftEnd
|
||||
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
|
||||
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
|
||||
|
||||
float gamma = d.Gamma;
|
||||
float gm1 = gamma - 1f;
|
||||
float cInt = MathF.Sqrt(gamma * pInt / MathF.Max(rhoInt, 1e-12f));
|
||||
float pAmb = d.AmbientPressure;
|
||||
|
||||
float Jplus = uInt + 2f * cInt / gm1;
|
||||
float Jminus = uInt - 2f * cInt / gm1;
|
||||
float s = pInt / MathF.Pow(rhoInt, gamma);
|
||||
float rhoIso = MathF.Pow(pAmb / s, 1f / gamma);
|
||||
float cIso = MathF.Sqrt(gamma * pAmb / MathF.Max(rhoIso, 1e-12f));
|
||||
float uIso = d.IsLeftEnd
|
||||
? (Jminus + 2f * cIso / gm1)
|
||||
: (Jplus - 2f * cIso / gm1);
|
||||
|
||||
bool supersonic = d.IsLeftEnd ? (uInt <= -cInt) : (uInt >= cInt);
|
||||
float rhoGhost, uGhost, pGhost, afGhost;
|
||||
if (supersonic)
|
||||
{
|
||||
rhoGhost = rhoInt; uGhost = uInt; pGhost = pInt; afGhost = afInt;
|
||||
}
|
||||
else
|
||||
{
|
||||
rhoGhost = rhoIso; uGhost = uIso; pGhost = pAmb;
|
||||
bool inflow = d.IsLeftEnd ? (uIso >= 0f) : (uIso <= 0f);
|
||||
afGhost = inflow ? 1f : afInt;
|
||||
}
|
||||
|
||||
if (d.IsLeftEnd)
|
||||
_pipeSystem.SetGhostLeft(d.PipeIndex, rhoGhost, uGhost, pGhost, afGhost);
|
||||
else
|
||||
_pipeSystem.SetGhostRight(d.PipeIndex, rhoGhost, uGhost, pGhost, afGhost);
|
||||
|
||||
float area = d.PipeArea;
|
||||
float mdot = rhoGhost * uGhost * area;
|
||||
if (d.IsLeftEnd) mdot = -mdot;
|
||||
d.LastMassFlowRate = mdot;
|
||||
d.LastFacePressure = pGhost;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@ namespace FluidSim.Core
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const double Gamma = 1.4;
|
||||
public const double R_gas = 287.0; // J/(kg·K)
|
||||
public const double P_amb = 101325.0; // Pa
|
||||
public const double T_amb = 300.0; // K
|
||||
public static readonly double Rho_amb = P_amb / (R_gas * T_amb); // ≈ 1.177 kg/m³
|
||||
public const float Gamma = 1.4f;
|
||||
public const float R_gas = 287f;
|
||||
public const float P_amb = 101325f;
|
||||
public const float T_amb = 300f;
|
||||
public static readonly float Rho_amb = P_amb / (R_gas * T_amb);
|
||||
}
|
||||
}
|
||||
27
Core/GhostBuffer.cs
Normal file
27
Core/GhostBuffer.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class GhostBuffer
|
||||
{
|
||||
public float[] Rho, U, P, Y;
|
||||
public int PipeCount { get; }
|
||||
|
||||
public GhostBuffer(int pipeCount)
|
||||
{
|
||||
PipeCount = pipeCount;
|
||||
int size = pipeCount * 2;
|
||||
Rho = new float[size];
|
||||
U = new float[size];
|
||||
P = new float[size];
|
||||
Y = new float[size];
|
||||
}
|
||||
|
||||
public void Set(int pipeIndex, bool isLeftEnd, float rho, float u, float p, float y)
|
||||
{
|
||||
int idx = pipeIndex * 2 + (isLeftEnd ? 0 : 1);
|
||||
Rho[idx] = rho;
|
||||
U[idx] = u;
|
||||
P[idx] = p;
|
||||
Y[idx] = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,30 @@ using System;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Compressible flow through an orifice, modelled as an isentropic nozzle.
|
||||
/// The caller provides the upstream stagnation state (pUp, rhoUp, TUp),
|
||||
/// downstream pressure, orifice area, discharge coefficient, and gas properties.
|
||||
/// Returns the face state and mass flow from upstream to downstream.
|
||||
/// </summary>
|
||||
public static class IsentropicOrifice
|
||||
{
|
||||
public static void Compute(
|
||||
double pUp, double rhoUp, double TUp, // upstream stagnation
|
||||
double pDown, // downstream back pressure
|
||||
double gamma, double R, double area, double Cd,
|
||||
out double mdot, out double rhoFace, out double uFace, out double pFace)
|
||||
float pUp, float rhoUp, float TUp,
|
||||
float pDown, float gamma, float R, float area, float Cd,
|
||||
out float mdot, out float rhoFace, out float uFace, out float pFace)
|
||||
{
|
||||
mdot = 0; rhoFace = rhoUp; uFace = 0; pFace = pUp;
|
||||
mdot = 0f; rhoFace = rhoUp; uFace = 0f; pFace = pUp;
|
||||
if (area <= 0f || pUp <= 0f || rhoUp <= 0f || TUp <= 0f) return;
|
||||
|
||||
if (area <= 0 || pUp <= 0 || rhoUp <= 0 || TUp <= 0)
|
||||
return;
|
||||
float pr = MathF.Min(pDown / pUp, 1f);
|
||||
if (pr < 1e-6f) pr = 1e-6f;
|
||||
float prCrit = MathF.Pow(2f / (gamma + 1f), gamma / (gamma - 1f));
|
||||
if (pr < prCrit) pr = prCrit;
|
||||
|
||||
double pr = pDown / pUp;
|
||||
if (pr < 1e-6) pr = 1e-6;
|
||||
float exponent = (gamma - 1f) / gamma;
|
||||
float M = MathF.Sqrt((2f / (gamma - 1f)) * (MathF.Pow(pr, -exponent) - 1f));
|
||||
if (float.IsNaN(M)) M = 0f;
|
||||
|
||||
double prCrit = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
|
||||
if (pr < prCrit) pr = prCrit; // choked flow
|
||||
|
||||
double exponent = (gamma - 1.0) / gamma;
|
||||
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -exponent) - 1.0));
|
||||
if (double.IsNaN(M)) M = 0;
|
||||
|
||||
double aUp = Math.Sqrt(gamma * R * TUp);
|
||||
float aUp = MathF.Sqrt(gamma * R * TUp);
|
||||
uFace = M * aUp;
|
||||
rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma);
|
||||
rhoFace = rhoUp * MathF.Pow(pr, 1f / gamma);
|
||||
pFace = pUp * pr;
|
||||
mdot = rhoFace * uFace * area * Cd; // positive from upstream to downstream
|
||||
mdot = rhoFace * uFace * area * Cd;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Characteristic open‑end boundary condition after Jones (1978).
|
||||
/// For all subsonic flow (outflow and inflow), the ghost state is derived
|
||||
/// from the isentropic expansion to ambient pressure, using the pipe's entropy,
|
||||
/// and the outgoing Riemann invariant. This avoids a density jump at flow reversal.
|
||||
/// Supersonic outflow extrapolates the interior state.
|
||||
/// Now includes air fraction tracking: incoming air is fresh (AF=1), outgoing uses interior pipe AF.
|
||||
/// </summary>
|
||||
public class OpenEndLink
|
||||
{
|
||||
public Pipe1D Pipe { get; }
|
||||
public bool IsLeftEnd { get; }
|
||||
public double AmbientPressure { get; set; } = 101325.0;
|
||||
public double Gamma { get; set; } = 1.4;
|
||||
|
||||
public double LastMassFlowRate { get; private set; }
|
||||
public double LastFaceDensity { get; private set; }
|
||||
public double LastFaceVelocity { get; private set; }
|
||||
public double LastFacePressure { get; private set; }
|
||||
|
||||
public OpenEndLink(Pipe1D pipe, bool isLeftEnd)
|
||||
{
|
||||
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
|
||||
IsLeftEnd = isLeftEnd;
|
||||
}
|
||||
|
||||
public void Resolve(double dtSub)
|
||||
{
|
||||
(double rhoInt, double uInt, double pInt) = IsLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
double airFracInt = IsLeftEnd
|
||||
? Pipe.GetInteriorAirFractionLeft()
|
||||
: Pipe.GetInteriorAirFractionRight();
|
||||
|
||||
double gamma = Gamma;
|
||||
double gm1 = gamma - 1.0;
|
||||
double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12));
|
||||
double pAmb = AmbientPressure;
|
||||
|
||||
// Riemann invariants
|
||||
double J_plus = uInt + 2.0 * cInt / gm1;
|
||||
double J_minus = uInt - 2.0 * cInt / gm1;
|
||||
|
||||
double rhoGhost, uGhost, pGhost, airFracGhost;
|
||||
|
||||
// ---- Subsonic branch (used for both outflow and inflow) ----
|
||||
double s = pInt / Math.Pow(rhoInt, gamma); // entropy constant
|
||||
double rhoIso = Math.Pow(pAmb / s, 1.0 / gamma);
|
||||
double cIso = Math.Sqrt(gamma * pAmb / Math.Max(rhoIso, 1e-12));
|
||||
double uIso = IsLeftEnd
|
||||
? (J_minus + 2.0 * cIso / gm1)
|
||||
: (J_plus - 2.0 * cIso / gm1);
|
||||
|
||||
// Check for supersonic outflow
|
||||
bool supersonic = IsLeftEnd
|
||||
? (uInt <= -cInt)
|
||||
: (uInt >= cInt);
|
||||
|
||||
if (!supersonic)
|
||||
{
|
||||
if (IsLeftEnd)
|
||||
supersonic = uIso <= -cIso;
|
||||
else
|
||||
supersonic = uIso >= cIso;
|
||||
}
|
||||
|
||||
if (supersonic)
|
||||
{
|
||||
// Supersonic outflow – extrapolate interior
|
||||
rhoGhost = rhoInt;
|
||||
uGhost = uInt;
|
||||
pGhost = pInt;
|
||||
airFracGhost = airFracInt; // whatever is leaving
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subsonic flow – use isentropic state to ambient
|
||||
rhoGhost = rhoIso;
|
||||
uGhost = uIso;
|
||||
pGhost = pAmb;
|
||||
|
||||
// Determine if inflow or outflow
|
||||
bool isInflow = IsLeftEnd ? (uIso >= 0) : (uIso <= 0); // positive u means into pipe from left end? Wait: left end: u>0 means flow to the right, into pipe. Right end: u>0 means flow to the right, out of pipe. Let's use mass flow sign later.
|
||||
// More straightforward: if using the isentropic state, the ghost velocity direction indicates flow. For inflow (ambient to pipe), airFraction = 1.0; for outflow, airFraction = interior's AF.
|
||||
if ((IsLeftEnd && uIso >= 0) || (!IsLeftEnd && uIso <= 0))
|
||||
{
|
||||
// Inflow (ambient enters pipe)
|
||||
airFracGhost = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Outflow (pipe exits to ambient)
|
||||
airFracGhost = airFracInt;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ghost to pipe
|
||||
if (IsLeftEnd)
|
||||
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost, airFracGhost);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost, airFracGhost);
|
||||
|
||||
// Mass flow out of the pipe (positive = leaving)
|
||||
double area = Pipe.Area;
|
||||
double mdot = rhoGhost * uGhost * area;
|
||||
if (IsLeftEnd) mdot = -mdot; // left end: positive u is into pipe, outward flow is -u
|
||||
LastMassFlowRate = mdot;
|
||||
LastFaceDensity = rhoGhost;
|
||||
LastFaceVelocity = uGhost;
|
||||
LastFacePressure = pGhost;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Interfaces;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class OrificeLink
|
||||
{
|
||||
public Port? VolumePort { get; }
|
||||
public Pipe1D Pipe { get; }
|
||||
public bool IsPipeLeftEnd { get; }
|
||||
public Func<double> AreaProvider { get; set; }
|
||||
public double DischargeCoefficient { get; set; } = 0.62;
|
||||
|
||||
public double EffectiveLength { get; set; } = 0.001;
|
||||
public bool UseInertance { get; set; } = false;
|
||||
|
||||
// Current mass flow (kg/s, positive = volume → pipe)
|
||||
private double _mdot;
|
||||
|
||||
public double LastMassFlowRate { get; private set; } // positive = into volume
|
||||
public double LastFaceDensity { get; private set; }
|
||||
public double LastFaceVelocity { get; private set; }
|
||||
public double LastFacePressure { get; private set; }
|
||||
|
||||
public OrificeLink(Port? volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func<double> areaProvider)
|
||||
{
|
||||
VolumePort = volumePort;
|
||||
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
|
||||
IsPipeLeftEnd = isPipeLeftEnd;
|
||||
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
|
||||
_mdot = 0.0;
|
||||
}
|
||||
|
||||
public void Resolve(double dtSub)
|
||||
{
|
||||
double area = AreaProvider();
|
||||
if (area < 1e-12 || VolumePort == null)
|
||||
{
|
||||
SetClosedWall();
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather states
|
||||
double volP = VolumePort.Pressure;
|
||||
double volRho = VolumePort.Density;
|
||||
double volT = VolumePort.Temperature;
|
||||
double volH = VolumePort.SpecificEnthalpy;
|
||||
double volAF = VolumePort.AirFraction;
|
||||
|
||||
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
double pipeT = pipeP / Math.Max(pipeRho * 287.0, 1e-12);
|
||||
double pipeAF = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorAirFractionLeft()
|
||||
: Pipe.GetInteriorAirFractionRight();
|
||||
|
||||
double gamma = 1.4;
|
||||
double R = 287.0;
|
||||
|
||||
// ---- Steady‑state nozzle solution ----
|
||||
double mdotSS; // positive = volume → pipe
|
||||
double rhoFace0, uFace0, pFace0;
|
||||
if (volP >= pipeP)
|
||||
{
|
||||
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
|
||||
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
||||
mdotSS = mdotUpToDown;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
|
||||
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
||||
mdotSS = -mdotUpToDown;
|
||||
}
|
||||
|
||||
// ---- Dynamic update ----
|
||||
if (UseInertance)
|
||||
{
|
||||
double rhoUp = _mdot >= 0 ? volRho : pipeRho;
|
||||
double inertance = rhoUp * EffectiveLength / area;
|
||||
double dp = volP - pipeP;
|
||||
double resistance = Math.Abs(dp) / Math.Max(Math.Abs(mdotSS), 1e-12);
|
||||
double dmdot_dt = (dp - resistance * _mdot) / inertance;
|
||||
_mdot += dmdot_dt * dtSub;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mdot = mdotSS;
|
||||
}
|
||||
|
||||
// Clamp outflow to available mass (if finite volume)
|
||||
if (VolumePort.Owner is Volume0D vol)
|
||||
{
|
||||
double maxOut = vol.Mass / dtSub;
|
||||
if (_mdot > maxOut) _mdot = maxOut;
|
||||
}
|
||||
|
||||
// ---- Ghost state with air fraction ----
|
||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho;
|
||||
double pFace = pFace0;
|
||||
double mdotMag = Math.Abs(_mdot);
|
||||
double uFace = mdotMag / (rhoFace * area);
|
||||
|
||||
// Determine air fraction for ghost and for volume port
|
||||
double airFracGhost; // air fraction of ghost cell (at pipe end)
|
||||
double airFracForVolume; // if flow reverses into volume, this is the air fraction entering volume
|
||||
|
||||
if (_mdot >= 0) // volume → pipe
|
||||
{
|
||||
airFracGhost = volAF;
|
||||
// Flow enters pipe; no need to set volume's air fraction (port already has its own)
|
||||
airFracForVolume = volAF; // unused
|
||||
}
|
||||
else // pipe → volume
|
||||
{
|
||||
airFracGhost = pipeAF;
|
||||
airFracForVolume = pipeAF;
|
||||
VolumePort.AirFraction = airFracForVolume;
|
||||
}
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
uFace = _mdot >= 0 ? uFace : -uFace;
|
||||
else
|
||||
uFace = _mdot >= 0 ? -uFace : uFace;
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace, airFracGhost);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoFace, uFace, pFace, airFracGhost);
|
||||
|
||||
// Store results (positive = into volume)
|
||||
LastMassFlowRate = -_mdot;
|
||||
LastFaceDensity = rhoFace;
|
||||
LastFaceVelocity = uFace;
|
||||
LastFacePressure = pFace;
|
||||
|
||||
VolumePort.MassFlowRate = -_mdot;
|
||||
|
||||
// Enthalpy transport
|
||||
if (-_mdot >= 0) // inflow → pipe enthalpy
|
||||
{
|
||||
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
|
||||
VolumePort.SpecificEnthalpy = hPipe;
|
||||
}
|
||||
else
|
||||
{
|
||||
VolumePort.SpecificEnthalpy = volH;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetClosedWall()
|
||||
{
|
||||
var (rInt, uInt, pInt) = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
Pipe.SetGhostLeft(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
|
||||
else
|
||||
Pipe.SetGhostRight(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
|
||||
|
||||
LastMassFlowRate = 0.0;
|
||||
LastFaceDensity = rInt;
|
||||
LastFaceVelocity = 0.0;
|
||||
LastFacePressure = pInt;
|
||||
if (VolumePort != null)
|
||||
VolumePort.MassFlowRate = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
560
Core/Pipesystem.cs
Normal file
560
Core/Pipesystem.cs
Normal file
@@ -0,0 +1,560 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
public class PipeSystem
|
||||
{
|
||||
// ---------- Master arrays ----------
|
||||
private float[] _rho, _rhou, _E, _Y;
|
||||
private readonly float[] _area;
|
||||
private readonly float[] _dx;
|
||||
private readonly int[] _pipeStart;
|
||||
private readonly int[] _pipeEnd;
|
||||
private readonly int _totalCells; // original cell count (visible)
|
||||
private readonly int _allCells; // total allocated (padded to Vector<float>.Count)
|
||||
private readonly int _pipeCount;
|
||||
|
||||
// Derived state – _p is kept for visualization, _c is gone
|
||||
private float[] _p;
|
||||
|
||||
// Flux arrays (size = _allCells + 1)
|
||||
private float[] _fluxM, _fluxP, _fluxE, _fluxY;
|
||||
|
||||
// Damping and relaxation (computed on‑the‑fly only if used)
|
||||
private float[] _dampingFactors;
|
||||
private float[] _relaxFactors;
|
||||
private bool _applyDamping;
|
||||
private bool _applyRelax;
|
||||
|
||||
// Ghost buffer
|
||||
private readonly GhostBuffer _ghost;
|
||||
|
||||
// Wall mask – precomputed once
|
||||
private readonly bool[] _isWallFace;
|
||||
|
||||
// ---------- Physical constants ----------
|
||||
private const float Gamma = 1.4f;
|
||||
private const float Gm1 = 0.4f;
|
||||
private const float Gm1Inv = 1f / Gm1; // 2.5
|
||||
private const float GammaOverGm1 = Gamma / Gm1; // 3.5
|
||||
private float _coeffBase;
|
||||
private float _relaxRate;
|
||||
private float _ambientPressure = 101325f;
|
||||
private float _ambientEnergyRef;
|
||||
|
||||
public float DampingMultiplier
|
||||
{
|
||||
set
|
||||
{
|
||||
_coeffBase = 0.1f * value;
|
||||
_applyDamping = _coeffBase != 0f;
|
||||
}
|
||||
}
|
||||
public float EnergyRelaxationRate
|
||||
{
|
||||
set
|
||||
{
|
||||
_relaxRate = value;
|
||||
_applyRelax = _relaxRate != 0f;
|
||||
}
|
||||
}
|
||||
public float AmbientPressure
|
||||
{
|
||||
set
|
||||
{
|
||||
_ambientPressure = value;
|
||||
_ambientEnergyRef = value * Gm1Inv;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Profiling ----------
|
||||
public bool EnableProfiling { get; set; }
|
||||
private long _profFluxTicks;
|
||||
private long _profUpdateTicks;
|
||||
private long _profCallCount;
|
||||
|
||||
// ---------- Construction ----------
|
||||
public PipeSystem(int totalCells, int[] pipeStart, int[] pipeEnd,
|
||||
float[] area, float[] dx,
|
||||
float initialRho, float initialU, float initialP)
|
||||
{
|
||||
_pipeStart = pipeStart;
|
||||
_pipeEnd = pipeEnd;
|
||||
_pipeCount = pipeStart.Length;
|
||||
_totalCells = totalCells;
|
||||
_area = area;
|
||||
_dx = dx;
|
||||
|
||||
// Pad to SIMD width so all vectorized loops cover the whole data
|
||||
int vecSize = Vector<float>.Count;
|
||||
_allCells = totalCells % vecSize == 0 ? totalCells : totalCells + vecSize - (totalCells % vecSize);
|
||||
|
||||
_rho = new float[_allCells];
|
||||
_rhou = new float[_allCells];
|
||||
_E = new float[_allCells];
|
||||
_Y = new float[_allCells];
|
||||
_p = new float[_allCells]; // pressure for drawing
|
||||
int faceCount = _allCells + 1;
|
||||
_fluxM = new float[faceCount];
|
||||
_fluxP = new float[faceCount];
|
||||
_fluxE = new float[faceCount];
|
||||
_fluxY = new float[faceCount];
|
||||
|
||||
_dampingFactors = new float[_allCells];
|
||||
_relaxFactors = new float[_allCells];
|
||||
_applyDamping = _coeffBase != 0f;
|
||||
_applyRelax = _relaxRate != 0f;
|
||||
|
||||
_ghost = new GhostBuffer(_pipeCount);
|
||||
_ambientEnergyRef = initialP * Gm1Inv;
|
||||
|
||||
// Pre‑compute wall face flags: each face that sits between two different pipes is a wall
|
||||
_isWallFace = new bool[faceCount];
|
||||
for (int f = 1; f < _totalCells; f++)
|
||||
{
|
||||
for (int p = 0; p < _pipeCount; p++)
|
||||
{
|
||||
if (f == _pipeEnd[p] && f < _totalCells)
|
||||
{
|
||||
_isWallFace[f] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize uniform state
|
||||
float initE = initialP / (Gm1 * initialRho);
|
||||
float rhoE = initialRho * initE + 0.5f * initialRho * initialU * initialU;
|
||||
for (int i = 0; i < totalCells; i++)
|
||||
{
|
||||
_rho[i] = initialRho;
|
||||
_rhou[i] = initialRho * initialU;
|
||||
_E[i] = rhoE;
|
||||
_Y[i] = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Ghost setters (for BoundarySystem) ----------
|
||||
public void SetGhostLeft(int pipeIndex, float rho, float u, float p, float y)
|
||||
=> _ghost.Set(pipeIndex, true, rho, u, p, y);
|
||||
public void SetGhostRight(int pipeIndex, float rho, float u, float p, float y)
|
||||
=> _ghost.Set(pipeIndex, false, rho, u, p, y);
|
||||
|
||||
// ---------- Public read methods ----------
|
||||
public int TotalCells => _totalCells;
|
||||
public int PipeCount => _pipeCount;
|
||||
public int GetPipeStart(int pipeIdx) => _pipeStart[pipeIdx];
|
||||
public int GetPipeEnd(int pipeIdx) => _pipeEnd[pipeIdx];
|
||||
public float GetCellPressure(int i) => _p[i];
|
||||
public float GetCellDensity(int i) => _rho[i];
|
||||
public float GetCellVelocity(int i)
|
||||
{
|
||||
float rho = _rho[i];
|
||||
return rho > 1e-12f ? _rhou[i] / rho : 0f;
|
||||
}
|
||||
public float GetCellAirFraction(int i) => _Y[i];
|
||||
|
||||
public (float rho, float u, float p) GetInteriorStateLeft(int pipeIdx)
|
||||
{
|
||||
int i = _pipeStart[pipeIdx];
|
||||
float rho = _rho[i];
|
||||
float rhou = _rhou[i];
|
||||
float u = rhou / MathF.Max(rho, 1e-12f);
|
||||
float p = Gm1 * (_E[i] - 0.5f * rhou * u);
|
||||
return (rho, u, p);
|
||||
}
|
||||
public (float rho, float u, float p) GetInteriorStateRight(int pipeIdx)
|
||||
{
|
||||
int i = _pipeEnd[pipeIdx] - 1;
|
||||
float rho = _rho[i];
|
||||
float rhou = _rhou[i];
|
||||
float u = rhou / MathF.Max(rho, 1e-12f);
|
||||
float p = Gm1 * (_E[i] - 0.5f * rhou * u);
|
||||
return (rho, u, p);
|
||||
}
|
||||
public float GetInteriorAirFractionLeft(int pipeIdx) => _Y[_pipeStart[pipeIdx]];
|
||||
public float GetInteriorAirFractionRight(int pipeIdx) => _Y[_pipeEnd[pipeIdx] - 1];
|
||||
|
||||
public void SetCellState(int i, float rho, float u, float p, float y = 1f)
|
||||
{
|
||||
if (i < 0 || i >= _totalCells) return;
|
||||
_rho[i] = rho;
|
||||
_rhou[i] = rho * u;
|
||||
_E[i] = p * Gm1Inv + 0.5f * rho * u * u;
|
||||
_Y[i] = y;
|
||||
}
|
||||
|
||||
// ---------- Main step ----------
|
||||
public void SimulateStep(float dt)
|
||||
{
|
||||
long t0 = 0, t1 = 0;
|
||||
if (EnableProfiling)
|
||||
{
|
||||
_profCallCount++;
|
||||
t0 = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
ComputeFluxes(dt);
|
||||
|
||||
if (EnableProfiling)
|
||||
{
|
||||
t1 = Stopwatch.GetTimestamp();
|
||||
_profFluxTicks += (t1 - t0);
|
||||
t0 = t1;
|
||||
}
|
||||
|
||||
UpdateCells(dt);
|
||||
|
||||
if (EnableProfiling)
|
||||
{
|
||||
t1 = Stopwatch.GetTimestamp();
|
||||
_profUpdateTicks += (t1 - t0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Flux computation: fuses primitive calculation and flux evaluation ----------
|
||||
private void ComputeFluxes(float dt)
|
||||
{
|
||||
float fm, fp, fe;
|
||||
int vecSize = Vector<float>.Count;
|
||||
|
||||
// ---- 1. Left ghost boundaries ----
|
||||
for (int p = 0; p < _pipeCount; p++)
|
||||
{
|
||||
int idx = _pipeStart[p];
|
||||
int ghostIdx = p * 2;
|
||||
float rL = _ghost.Rho[ghostIdx];
|
||||
float uL = _ghost.U[ghostIdx];
|
||||
float pL = _ghost.P[ghostIdx];
|
||||
float YL = _ghost.Y[ghostIdx];
|
||||
float cL = MathF.Sqrt(Gamma * pL / MathF.Max(rL, 1e-12f));
|
||||
|
||||
float rR = _rho[idx], rhouR = _rhou[idx];
|
||||
float invRhoR = MathF.ReciprocalEstimate(MathF.Max(rR, 1e-12f));
|
||||
float uR = rhouR * invRhoR;
|
||||
float pR = Gm1 * (_E[idx] - 0.5f * rhouR * uR);
|
||||
float cR = MathF.Sqrt(Gamma * pR * invRhoR);
|
||||
float YR = _Y[idx];
|
||||
|
||||
// store pressure for cell idx
|
||||
_p[idx] = pR;
|
||||
|
||||
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR, out fm, out fp, out fe);
|
||||
_fluxM[idx] = fm; _fluxP[idx] = fp; _fluxE[idx] = fe;
|
||||
|
||||
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
|
||||
ScalarFlux(rL, uL, YL, rR, uR, YR, alpha, out float fy);
|
||||
_fluxY[idx] = fy;
|
||||
}
|
||||
|
||||
// ---- 2. Right ghost boundaries ----
|
||||
for (int p = 0; p < _pipeCount; p++)
|
||||
{
|
||||
int idx = _pipeEnd[p] - 1;
|
||||
int face = idx + 1;
|
||||
int ghostIdx = p * 2 + 1;
|
||||
float rR = _ghost.Rho[ghostIdx];
|
||||
float uR = _ghost.U[ghostIdx];
|
||||
float pR = _ghost.P[ghostIdx];
|
||||
float YR = _ghost.Y[ghostIdx];
|
||||
float cR = MathF.Sqrt(Gamma * pR / MathF.Max(rR, 1e-12f));
|
||||
|
||||
float rL = _rho[idx], rhouL = _rhou[idx];
|
||||
float invRhoL = MathF.ReciprocalEstimate(MathF.Max(rL, 1e-12f));
|
||||
float uL = rhouL * invRhoL;
|
||||
float pL = Gm1 * (_E[idx] - 0.5f * rhouL * uL);
|
||||
float cL = MathF.Sqrt(Gamma * pL * invRhoL);
|
||||
float YL = _Y[idx];
|
||||
|
||||
// store pressure for cell idx
|
||||
_p[idx] = pL;
|
||||
|
||||
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR, out fm, out fp, out fe);
|
||||
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
|
||||
|
||||
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
|
||||
ScalarFlux(rL, uL, YL, rR, uR, YR, alpha, out float fy);
|
||||
_fluxY[face] = fy;
|
||||
}
|
||||
|
||||
// ---- 3. Interior faces – vectorised SIMD ----
|
||||
for (int face = 1; face < _totalCells; face++)
|
||||
{
|
||||
// Handle walls (rare) with scalar code
|
||||
if (_isWallFace[face])
|
||||
{
|
||||
int iL = face - 1;
|
||||
float rL = _rho[iL], rhouL = _rhou[iL];
|
||||
float invRhoL = MathF.ReciprocalEstimate(MathF.Max(rL, 1e-12f));
|
||||
float uL = rhouL * invRhoL;
|
||||
float pL = Gm1 * (_E[iL] - 0.5f * rhouL * uL);
|
||||
float cL = MathF.Sqrt(Gamma * pL * invRhoL);
|
||||
_p[iL] = pL;
|
||||
|
||||
LaxFlux(rL, uL, pL, cL, rL, -uL, pL, cL, out fm, out fp, out fe);
|
||||
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
|
||||
_fluxY[face] = 0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the next vecSize faces contain a wall, fall back to scalar for this block
|
||||
if (face + vecSize - 1 < _totalCells)
|
||||
{
|
||||
bool hasWall = false;
|
||||
for (int f = face; f < face + vecSize; f++)
|
||||
if (_isWallFace[f]) { hasWall = true; break; }
|
||||
|
||||
if (!hasWall)
|
||||
{
|
||||
// --- Vectorised block ---
|
||||
var rhoL = new Vector<float>(_rho, face - 1);
|
||||
var rhouL = new Vector<float>(_rhou, face - 1);
|
||||
var EL = new Vector<float>(_E, face - 1);
|
||||
var YL = new Vector<float>(_Y, face - 1);
|
||||
var rhoR = new Vector<float>(_rho, face);
|
||||
var rhouR = new Vector<float>(_rhou, face);
|
||||
var ER = new Vector<float>(_E, face);
|
||||
var YR = new Vector<float>(_Y, face);
|
||||
|
||||
var invRhoL = Vector<float>.One / Vector.Max(rhoL, new Vector<float>(1e-12f));
|
||||
var invRhoR = Vector<float>.One / Vector.Max(rhoR, new Vector<float>(1e-12f));
|
||||
var uL = rhouL * invRhoL;
|
||||
var uR = rhouR * invRhoR;
|
||||
var kinL = 0.5f * rhouL * uL;
|
||||
var kinR = 0.5f * rhouR * uR;
|
||||
var pL = Gm1 * (EL - kinL);
|
||||
var pR = Gm1 * (ER - kinR);
|
||||
var cL = Vector.SquareRoot(Gamma * pL * invRhoL);
|
||||
var cR = Vector.SquareRoot(Gamma * pR * invRhoR);
|
||||
|
||||
// Store pressures for visualisation (left cell of each face)
|
||||
pL.CopyTo(_p, face - 1);
|
||||
|
||||
// Lax‑Friedrichs fluxes
|
||||
var ELs = pL * Gm1Inv * invRhoL + 0.5f * uL * uL; // energy per mass
|
||||
var ERs = pR * Gm1Inv * invRhoR + 0.5f * uR * uR;
|
||||
|
||||
var FmL = rhoL * uL;
|
||||
var FpL = rhoL * uL * uL + pL;
|
||||
var FeL = (rhoL * ELs + pL) * uL;
|
||||
|
||||
var FmR = rhoR * uR;
|
||||
var FpR = rhoR * uR * uR + pR;
|
||||
var FeR = (rhoR * ERs + pR) * uR;
|
||||
|
||||
var absUL = Vector.Abs(uL);
|
||||
var absUR = Vector.Abs(uR);
|
||||
var alpha = Vector.Max(absUL + cL, absUR + cR);
|
||||
|
||||
var fmVec = 0.5f * (FmL + FmR) - 0.5f * alpha * (rhoR - rhoL);
|
||||
var fpVec = 0.5f * (FpL + FpR) - 0.5f * alpha * (rhouR - rhouL);
|
||||
var feVec = 0.5f * (FeL + FeR) - 0.5f * alpha * (rhoR * ERs - rhoL * ELs);
|
||||
|
||||
var fyL = FmL * YL;
|
||||
var fyR = FmR * YR;
|
||||
var fyVec = 0.5f * (fyL + fyR) - 0.5f * alpha * (rhoR * YR - rhoL * YL);
|
||||
|
||||
fmVec.CopyTo(_fluxM, face);
|
||||
fpVec.CopyTo(_fluxP, face);
|
||||
feVec.CopyTo(_fluxE, face);
|
||||
fyVec.CopyTo(_fluxY, face);
|
||||
|
||||
face += vecSize - 1; // loop increment will add 1, so we advance vecSize faces
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Scalar interior face (fallback) ---
|
||||
{
|
||||
int iLf = face - 1, iRf = face;
|
||||
float rLf = _rho[iLf], rhouLf = _rhou[iLf];
|
||||
float invRhoLf = MathF.ReciprocalEstimate(MathF.Max(rLf, 1e-12f));
|
||||
float uLf = rhouLf * invRhoLf;
|
||||
float pLf = Gm1 * (_E[iLf] - 0.5f * rhouLf * uLf);
|
||||
float cLf = MathF.Sqrt(Gamma * pLf * invRhoLf);
|
||||
float YLf = _Y[iLf];
|
||||
_p[iLf] = pLf;
|
||||
|
||||
float rRf = _rho[iRf], rhouRf = _rhou[iRf];
|
||||
float invRhoRf = MathF.ReciprocalEstimate(MathF.Max(rRf, 1e-12f));
|
||||
float uRf = rhouRf * invRhoRf;
|
||||
float pRf = Gm1 * (_E[iRf] - 0.5f * rhouRf * uRf);
|
||||
float cRf = MathF.Sqrt(Gamma * pRf * invRhoRf);
|
||||
float YRf = _Y[iRf];
|
||||
|
||||
LaxFlux(rLf, uLf, pLf, cLf, rRf, uRf, pRf, cRf, out fm, out fp, out fe);
|
||||
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
|
||||
|
||||
float alpha = MathF.Max(MathF.Abs(uLf) + cLf, MathF.Abs(uRf) + cRf);
|
||||
ScalarFlux(rLf, uLf, YLf, rRf, uRf, YRf, alpha, out float fy);
|
||||
_fluxY[face] = fy;
|
||||
}
|
||||
}
|
||||
|
||||
// If damping/relaxation are active, compute the factors here (re-uses _dampingFactors/_relaxFactors arrays,
|
||||
// but we no longer have a separate precompute pass). We compute them on demand in UpdateCells anyway?
|
||||
// Actually UpdateCells multiplies by these factors; we can compute them there if needed.
|
||||
}
|
||||
|
||||
// ---------- Cell update (unchanged core, but skips relaxation/damping when not needed) ----------
|
||||
private void UpdateCells(float dt)
|
||||
{
|
||||
int vecSize = Vector<float>.Count;
|
||||
float dtRelax = -_relaxRate * dt;
|
||||
|
||||
// Compute damping and relaxation factors if needed
|
||||
if (_applyDamping)
|
||||
{
|
||||
for (int i = 0; i < _totalCells; i++)
|
||||
{
|
||||
float rho = _rho[i];
|
||||
_dampingFactors[i] = rho > 1e-12f
|
||||
? MathF.Exp(-_coeffBase * dt / rho)
|
||||
: 1f;
|
||||
}
|
||||
}
|
||||
if (_applyRelax)
|
||||
{
|
||||
var relaxVal = MathF.Exp(dtRelax);
|
||||
for (int i = 0; i < _totalCells; i++)
|
||||
_relaxFactors[i] = relaxVal;
|
||||
}
|
||||
|
||||
int iCell = 0;
|
||||
for (; iCell <= _totalCells - vecSize; iCell += vecSize)
|
||||
{
|
||||
var rhoOld = new Vector<float>(_rho, iCell);
|
||||
var rhouOld = new Vector<float>(_rhou, iCell);
|
||||
var EOld = new Vector<float>(_E, iCell);
|
||||
var YOld = new Vector<float>(_Y, iCell);
|
||||
|
||||
var fluxM_L = new Vector<float>(_fluxM, iCell);
|
||||
var fluxP_L = new Vector<float>(_fluxP, iCell);
|
||||
var fluxE_L = new Vector<float>(_fluxE, iCell);
|
||||
var fluxY_L = new Vector<float>(_fluxY, iCell);
|
||||
|
||||
var fluxM_R = new Vector<float>(_fluxM, iCell + 1);
|
||||
var fluxP_R = new Vector<float>(_fluxP, iCell + 1);
|
||||
var fluxE_R = new Vector<float>(_fluxE, iCell + 1);
|
||||
var fluxY_R = new Vector<float>(_fluxY, iCell + 1);
|
||||
|
||||
var dtdx = new Vector<float>(dt) / new Vector<float>(_dx, iCell);
|
||||
|
||||
var rhoNew = rhoOld - dtdx * (fluxM_R - fluxM_L);
|
||||
var rhouNew = rhouOld - dtdx * (fluxP_R - fluxP_L);
|
||||
var ENew = EOld - dtdx * (fluxE_R - fluxE_L);
|
||||
var rhoYOld = rhoOld * YOld;
|
||||
var rhoYNew = rhoYOld - dtdx * (fluxY_R - fluxY_L);
|
||||
|
||||
if (_applyDamping)
|
||||
rhouNew *= new Vector<float>(_dampingFactors, iCell);
|
||||
if (_applyRelax)
|
||||
{
|
||||
var ambRef = new Vector<float>(_ambientEnergyRef);
|
||||
var relax = new Vector<float>(_relaxFactors, iCell);
|
||||
ENew = ambRef + (ENew - ambRef) * relax;
|
||||
}
|
||||
|
||||
rhoNew = Vector.Max(rhoNew, new Vector<float>(1e-12f));
|
||||
var kinNew = 0.5f * rhouNew * rhouNew / rhoNew;
|
||||
var eMin = new Vector<float>(100f * Gm1Inv) + kinNew;
|
||||
ENew = Vector.Max(ENew, eMin);
|
||||
|
||||
rhoNew.CopyTo(_rho, iCell);
|
||||
rhouNew.CopyTo(_rhou, iCell);
|
||||
ENew.CopyTo(_E, iCell);
|
||||
var yNew = rhoYNew / rhoNew;
|
||||
yNew = Vector.Min(Vector.Max(yNew, Vector<float>.Zero), Vector<float>.One);
|
||||
yNew.CopyTo(_Y, iCell);
|
||||
}
|
||||
|
||||
// Scalar remainder (only a few cells)
|
||||
for (; iCell < _totalCells; iCell++)
|
||||
{
|
||||
float rhoOld = _rho[iCell], rhouOld = _rhou[iCell], EOld = _E[iCell], YOld = _Y[iCell];
|
||||
float fluxM_L = _fluxM[iCell], fluxP_L = _fluxP[iCell], fluxE_L = _fluxE[iCell], fluxY_L = _fluxY[iCell];
|
||||
float fluxM_R = _fluxM[iCell + 1], fluxP_R = _fluxP[iCell + 1], fluxE_R = _fluxE[iCell + 1], fluxY_R = _fluxY[iCell + 1];
|
||||
float dtdx = dt / _dx[iCell];
|
||||
|
||||
float rhoNew = rhoOld - dtdx * (fluxM_R - fluxM_L);
|
||||
float rhouNew = rhouOld - dtdx * (fluxP_R - fluxP_L);
|
||||
float ENew = EOld - dtdx * (fluxE_R - fluxE_L);
|
||||
float rhoYOld = rhoOld * YOld;
|
||||
float rhoYNew = rhoYOld - dtdx * (fluxY_R - fluxY_L);
|
||||
|
||||
if (_applyDamping) rhouNew *= _dampingFactors[iCell];
|
||||
if (_applyRelax) ENew = _ambientEnergyRef + (ENew - _ambientEnergyRef) * _relaxFactors[iCell];
|
||||
|
||||
rhoNew = MathF.Max(rhoNew, 1e-12f);
|
||||
float kin = 0.5f * rhouNew * rhouNew / rhoNew;
|
||||
float eMin = 100f * Gm1Inv + kin;
|
||||
ENew = MathF.Max(ENew, eMin);
|
||||
|
||||
_rho[iCell] = rhoNew;
|
||||
_rhou[iCell] = rhouNew;
|
||||
_E[iCell] = ENew;
|
||||
_Y[iCell] = Math.Clamp(rhoYNew / rhoNew, 0f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Scalar flux helpers (used in boundaries and scalar fallback) ----------
|
||||
private static void LaxFlux(float rL, float uL, float pL, float cL,
|
||||
float rR, float uR, float pR, float cR,
|
||||
out float fm, out float fp, out float fe)
|
||||
{
|
||||
float EL = pL * Gm1Inv / rL + 0.5f * uL * uL;
|
||||
float ER = pR * Gm1Inv / rR + 0.5f * uR * uR;
|
||||
float FmL = rL * uL;
|
||||
float FpL = rL * uL * uL + pL;
|
||||
float FeL = (rL * EL + pL) * uL;
|
||||
float FmR = rR * uR;
|
||||
float FpR = rR * uR * uR + pR;
|
||||
float FeR = (rR * ER + pR) * uR;
|
||||
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
|
||||
fm = 0.5f * (FmL + FmR) - 0.5f * alpha * (rR - rL);
|
||||
fp = 0.5f * (FpL + FpR) - 0.5f * alpha * (rR * uR - rL * uL);
|
||||
fe = 0.5f * (FeL + FeR) - 0.5f * alpha * (rR * ER - rL * EL);
|
||||
}
|
||||
|
||||
private static void ScalarFlux(float rL, float uL, float YL,
|
||||
float rR, float uR, float YR,
|
||||
float alpha, out float fy)
|
||||
{
|
||||
float FyL = rL * uL * YL;
|
||||
float FyR = rR * uR * YR;
|
||||
fy = 0.5f * (FyL + FyR) - 0.5f * alpha * (rR * YR - rL * YL);
|
||||
}
|
||||
|
||||
// ---------- Profiling report ----------
|
||||
public string GetProfileReport()
|
||||
{
|
||||
if (!EnableProfiling || _profCallCount == 0)
|
||||
return "Pipe profiling disabled or no data.";
|
||||
|
||||
double freq = Stopwatch.Frequency;
|
||||
long totalTicks = _profFluxTicks + _profUpdateTicks;
|
||||
if (totalTicks == 0) return "No pipe profile data collected.";
|
||||
|
||||
double totalMs = totalTicks * 1000.0 / freq;
|
||||
double avgCallUs = totalMs * 1000.0 / _profCallCount;
|
||||
|
||||
double fluxMs = _profFluxTicks * 1000.0 / freq;
|
||||
double updateMs = _profUpdateTicks * 1000.0 / freq;
|
||||
|
||||
double fluxAvgUs = fluxMs * 1000.0 / _profCallCount;
|
||||
double updateAvgUs = updateMs * 1000.0 / _profCallCount;
|
||||
|
||||
string report = $" Pipe kernel (over {_profCallCount} calls, total {totalMs:F2} ms, avg {avgCallUs:F2} µs/call):\n";
|
||||
report += $" Fluxes (incl. primitives): {fluxMs:F2} ms ({_profFluxTicks * 100.0 / totalTicks:F1}%), avg {fluxAvgUs:F2} µs/call\n";
|
||||
report += $" Update cells: {updateMs:F2} ms ({_profUpdateTicks * 100.0 / totalTicks:F1}%), avg {updateAvgUs:F2} µs/call\n";
|
||||
|
||||
_profFluxTicks = 0;
|
||||
_profUpdateTicks = 0;
|
||||
_profCallCount = 0;
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
}
|
||||
163
Core/Solver.cs
163
Core/Solver.cs
@@ -10,136 +10,91 @@ namespace FluidSim.Core
|
||||
public class Solver
|
||||
{
|
||||
private readonly List<IComponent> _components = new();
|
||||
private readonly List<OrificeLink> _orificeLinks = new();
|
||||
private readonly List<OpenEndLink> _openEndLinks = new();
|
||||
|
||||
private PipeSystem _pipeSystem;
|
||||
private BoundarySystem _boundarySystem;
|
||||
private double _dt;
|
||||
|
||||
/// <summary>CFL target for sub‑stepping (0.3‑0.8). Lower values are safer for shocks.</summary>
|
||||
public double CflTarget { get; set; } = 0.9;
|
||||
public int SubStepCount { get; set; } = 4;
|
||||
public bool EnableProfiling { get; set; } = false;
|
||||
|
||||
// ---------- Timing accumulators (reset every LogInterval steps) ----------
|
||||
private long _stepCount;
|
||||
private double _timeTotal, _timeCFL, _timeOrifice, _timeOpenEnd,
|
||||
_timePipe, _timeClearGhosts, _timeUpdateState;
|
||||
|
||||
private const int LogInterval = 5000;
|
||||
private const bool EnableLogging = false; // temporarily ON for debugging
|
||||
private long _ticksOrifice, _ticksOpenEnd, _ticksPipe, _ticksUpdate;
|
||||
|
||||
public void SetTimeStep(double dt) => _dt = dt;
|
||||
|
||||
public void AddComponent(IComponent component) => _components.Add(component);
|
||||
public void AddOrificeLink(OrificeLink link) => _orificeLinks.Add(link);
|
||||
public void AddOpenEndLink(OpenEndLink link) => _openEndLinks.Add(link);
|
||||
|
||||
public void SetPipeSystem(PipeSystem pipeSystem)
|
||||
{
|
||||
_pipeSystem = pipeSystem;
|
||||
}
|
||||
public void SetBoundarySystem(BoundarySystem boundarySystem)
|
||||
{
|
||||
_boundarySystem = boundarySystem;
|
||||
}
|
||||
|
||||
public void Step()
|
||||
{
|
||||
var pipes = _components.OfType<Pipe1D>().ToList();
|
||||
if (pipes.Count == 0) return;
|
||||
if (_pipeSystem == null || _boundarySystem == null) return;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// CFL count – track which pipe demands the most sub‑steps
|
||||
int nSub = 1;
|
||||
Pipe1D worstPipe = pipes[0];
|
||||
foreach (var p in pipes)
|
||||
{
|
||||
int n = p.GetRequiredSubSteps(_dt, CflTarget);
|
||||
if (n > nSub)
|
||||
{
|
||||
nSub = n;
|
||||
worstPipe = p;
|
||||
}
|
||||
}
|
||||
double dtSub = _dt / nSub;
|
||||
|
||||
// ----- Diagnostic: warn if nSub is high -----
|
||||
if (nSub > 50)
|
||||
{
|
||||
double maxW = 0;
|
||||
for (int i = 0; i < worstPipe.CellCount; i++)
|
||||
{
|
||||
double rho = worstPipe.GetCellDensity(i);
|
||||
double u = Math.Abs(worstPipe.GetCellVelocity(i));
|
||||
double p = worstPipe.GetCellPressure(i);
|
||||
double c = Math.Sqrt(1.4 * p / Math.Max(rho, 1e-12));
|
||||
if (u + c > maxW) maxW = u + c;
|
||||
}
|
||||
Console.WriteLine($"nSub = {nSub} (worst pipe: {worstPipe.Name}, maxW = {maxW:F0} m/s)");
|
||||
}
|
||||
|
||||
_timeCFL += sw.Elapsed.TotalSeconds;
|
||||
|
||||
// ----- Safety cap – prevent the solver from hanging -----
|
||||
const int maxSubSteps = 10000;
|
||||
const int hardLimit = 500; // temporary low cap for debugging
|
||||
|
||||
if (nSub > hardLimit)
|
||||
{
|
||||
Console.WriteLine($"nSub ({nSub}) exceeds hard limit {hardLimit}. Simulation step skipped.");
|
||||
return;
|
||||
}
|
||||
int nSub = SubStepCount;
|
||||
float dtSub = (float)(_dt / nSub);
|
||||
|
||||
for (int sub = 0; sub < nSub; sub++)
|
||||
{
|
||||
double t0;
|
||||
long t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var link in _orificeLinks)
|
||||
link.Resolve(dtSub);
|
||||
_timeOrifice += sw.Elapsed.TotalSeconds - t0;
|
||||
t0 = Stopwatch.GetTimestamp();
|
||||
_boundarySystem.ResolveOrifices(dtSub);
|
||||
_ticksOrifice += Stopwatch.GetTimestamp() - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var link in _openEndLinks)
|
||||
link.Resolve(dtSub);
|
||||
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
|
||||
t0 = Stopwatch.GetTimestamp();
|
||||
_boundarySystem.ResolveOpenEnds(dtSub);
|
||||
_ticksOpenEnd += Stopwatch.GetTimestamp() - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var p in pipes)
|
||||
p.SimulateSingleStep(dtSub);
|
||||
_timePipe += sw.Elapsed.TotalSeconds - t0;
|
||||
t0 = Stopwatch.GetTimestamp();
|
||||
_pipeSystem.SimulateStep(dtSub);
|
||||
_ticksPipe += Stopwatch.GetTimestamp() - t0;
|
||||
}
|
||||
|
||||
double tCG = sw.Elapsed.TotalSeconds;
|
||||
foreach (var p in pipes)
|
||||
p.ClearGhostFlags();
|
||||
_timeClearGhosts += sw.Elapsed.TotalSeconds - tCG;
|
||||
|
||||
double tUS = sw.Elapsed.TotalSeconds;
|
||||
long tUS = Stopwatch.GetTimestamp();
|
||||
foreach (var comp in _components)
|
||||
comp.UpdateState(_dt);
|
||||
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS;
|
||||
|
||||
_timeTotal += sw.Elapsed.TotalSeconds;
|
||||
comp.UpdateState((float)_dt);
|
||||
_ticksUpdate += Stopwatch.GetTimestamp() - tUS;
|
||||
|
||||
_stepCount++;
|
||||
if (_stepCount % LogInterval == 0 && EnableLogging)
|
||||
if (_stepCount % 5000 == 0 && EnableProfiling)
|
||||
{
|
||||
if (_timeTotal > 0)
|
||||
{
|
||||
double stepsPerSec = LogInterval / _timeTotal;
|
||||
double avgUs = (_timeTotal / LogInterval) * 1e6;
|
||||
double freq = Stopwatch.Frequency;
|
||||
double total = _ticksOrifice + _ticksOpenEnd + _ticksPipe + _ticksUpdate;
|
||||
double avgStepUs = (total / freq) * 1e6 / 5000.0;
|
||||
|
||||
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---");
|
||||
Console.WriteLine($" Steps per second: {stepsPerSec:F1}");
|
||||
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
|
||||
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine($" Sub‑step loop:");
|
||||
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} %");
|
||||
Console.WriteLine();
|
||||
int orificeCalls = 5000 * nSub;
|
||||
int updateCalls = 5000;
|
||||
|
||||
double orificeMs = _ticksOrifice * 1000.0 / freq;
|
||||
double openEndMs = _ticksOpenEnd * 1000.0 / freq;
|
||||
double pipeMs = _ticksPipe * 1000.0 / freq;
|
||||
double updateMs = _ticksUpdate * 1000.0 / freq;
|
||||
|
||||
double orificeAvgUs = orificeMs * 1000.0 / orificeCalls;
|
||||
double openEndAvgUs = openEndMs * 1000.0 / orificeCalls;
|
||||
double pipeAvgUs = pipeMs * 1000.0 / orificeCalls;
|
||||
double updateAvgUs = updateMs * 1000.0 / updateCalls;
|
||||
|
||||
Console.WriteLine($"--- Solver ({5000} steps, nSub={nSub}) ---");
|
||||
Console.WriteLine($" Average step: {avgStepUs:F2} µs");
|
||||
Console.WriteLine($" Orifice: {orificeMs:F2} ms ({(double)_ticksOrifice / total * 100:F1}%), avg {orificeAvgUs:F2} µs/call");
|
||||
Console.WriteLine($" OpenEnd: {openEndMs:F2} ms ({(double)_ticksOpenEnd / total * 100:F1}%), avg {openEndAvgUs:F2} µs/call");
|
||||
Console.WriteLine($" Pipe: {pipeMs:F2} ms ({(double)_ticksPipe / total * 100:F1}%), avg {pipeAvgUs:F2} µs/call");
|
||||
Console.WriteLine($" Update: {updateMs:F2} ms ({(double)_ticksUpdate / total * 100:F1}%), avg {updateAvgUs:F2} µs/call");
|
||||
|
||||
// Pipe internal breakdown (with per-phase averages)
|
||||
if (_pipeSystem.EnableProfiling)
|
||||
{
|
||||
Console.WriteLine(_pipeSystem.GetProfileReport());
|
||||
}
|
||||
|
||||
_timeTotal = 0;
|
||||
_timeCFL = 0;
|
||||
_timeOrifice = 0;
|
||||
_timeOpenEnd = 0;
|
||||
_timePipe = 0;
|
||||
_timeClearGhosts = 0;
|
||||
_timeUpdateState = 0;
|
||||
_ticksOrifice = _ticksOpenEnd = _ticksPipe = _ticksUpdate = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,34 @@
|
||||
using System;
|
||||
using FluidSim.Core;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Synthesises far‑field exhaust sound using the monopole model
|
||||
/// of Jones (1978). The radiated pressure is proportional to the
|
||||
/// time derivative of the mass flow at the pipe exit.
|
||||
///
|
||||
/// Reference:
|
||||
/// Jones, A.D. (1978) "Noise characteristics and exhaust process
|
||||
/// gas dynamics of a small 2-stroke engine", PhD thesis, Univ. Adelaide.
|
||||
/// </summary>
|
||||
public class SoundProcessor
|
||||
{
|
||||
private readonly double dt;
|
||||
private readonly double r; // listener distance (m)
|
||||
private readonly double scaleFactor; // 1 / (4π r) (free-field monopole)
|
||||
private readonly float dt;
|
||||
private readonly float scaleFactor; // 1 / (4π r)
|
||||
private float flowLP, prevMassFlowOut, smoothDMdt;
|
||||
private readonly float lpAlpha, alpha;
|
||||
|
||||
// ---------- Mass‑flow derivative (identical to original) ----------
|
||||
private double flowLP;
|
||||
private readonly double lpAlpha;
|
||||
private double prevMassFlowOut;
|
||||
private double smoothDMdt;
|
||||
private readonly double alpha;
|
||||
public float Gain = 1f;
|
||||
|
||||
public float Gain { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="sampleRate">Audio sample rate (Hz).</param>
|
||||
/// <param name="listenerDistanceMeters">Listener distance (m).</param>
|
||||
/// <param name="pipeDiameterMeters">Ignored in this model; kept for compatibility.</param>
|
||||
public SoundProcessor(int sampleRate,
|
||||
double listenerDistanceMeters = 1.0,
|
||||
double pipeDiameterMeters = 0.0217)
|
||||
public SoundProcessor(int sampleRate, float listenerDistance = 1f)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
r = listenerDistanceMeters;
|
||||
scaleFactor = 1.0 / (4.0 * Math.PI * r); // free‑field monopole
|
||||
|
||||
// ---- Smoothing time constants (unchanged) ----
|
||||
double tau = 0.02; // 2 ms for derivative
|
||||
alpha = Math.Exp(-dt / tau);
|
||||
|
||||
double tauLP = 0.00001; // 5 ms low‑pass on mass flow
|
||||
lpAlpha = Math.Exp(-dt / tauLP);
|
||||
dt = 1f / sampleRate;
|
||||
scaleFactor = 1f / (4f * MathF.PI * listenerDistance);
|
||||
float tau = 0.02f;
|
||||
alpha = MathF.Exp(-dt / tau);
|
||||
float tauLP = 0.005f;
|
||||
lpAlpha = MathF.Exp(-dt / tauLP);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process one sample. The OpenEndLink provides the instantaneous
|
||||
/// exit‑plane mass flow.
|
||||
/// </summary>
|
||||
public float Process(OpenEndLink openEnd)
|
||||
public float Process(float massFlowOut)
|
||||
{
|
||||
double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe
|
||||
|
||||
// Low‑pass the mass flow signal
|
||||
flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut;
|
||||
|
||||
// Derivative of the smoothed mass flow
|
||||
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
|
||||
flowLP = lpAlpha * flowLP + (1f - lpAlpha) * massFlowOut;
|
||||
float rawDerivative = (flowLP - prevMassFlowOut) / dt;
|
||||
prevMassFlowOut = flowLP;
|
||||
|
||||
// Smooth the derivative
|
||||
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
|
||||
|
||||
// Far‑field monopole pressure (free‑field, Jones eq. 2.15 adapted)
|
||||
double pressure = smoothDMdt * scaleFactor * Gain;
|
||||
|
||||
// Soft clip to ±1
|
||||
return (float)pressure;
|
||||
smoothDMdt = alpha * smoothDMdt + (1f - alpha) * rawDerivative;
|
||||
float pressure = smoothDMdt * scaleFactor * Gain;
|
||||
return MathF.Tanh(pressure);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user