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