Helmholtz testing (no decay bug)

This commit is contained in:
max
2026-05-09 01:44:35 +02:00
parent 9c9e23147a
commit 77ef4753a3
23 changed files with 1811 additions and 2118 deletions

330
Core/BoundarySystem.cs Normal file
View 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;
}
}
}
}