refactoring (broken right now)

This commit is contained in:
2026-05-06 15:24:39 +02:00
parent bc4e077924
commit bc0df51ddb
25 changed files with 1184 additions and 1983 deletions

41
Core/IsentropicOrifice.cs Normal file
View File

@@ -0,0 +1,41 @@
using System;
namespace FluidSim.Core
{
/// <summary>
/// Compressible flow through an orifice, modelled as an isentropic nozzle.
/// Supports choked and unchoked flow, forward and reverse.
/// </summary>
public static class IsentropicOrifice
{
/// <summary>
/// Compute mass flow and face primitive state for an orifice.
/// </summary>
/// <param name="pUp">Upstream stagnation pressure (Pa).</param>
/// <param name="rhoUp">Upstream stagnation density (kg/m³).</param>
/// <param name="gamma">Ratio of specific heats.</param>
/// <param name="R">Specific gas constant (J/kg·K).</param>
/// <param name="pDown">Downstream static pressure (Pa).</param>
/// <param name="area">Effective orifice area (m²).</param>
/// <param name="Cd">Discharge coefficient (default 0.62).</param>
/// <param name="mdot">Mass flow rate (kg/s), positive from upstream to downstream.</param>
/// <param name="rhoFace">Face density (kg/m³).</param>
/// <param name="uFace">Face velocity (m/s).</param>
/// <param name="pFace">Face pressure (Pa).</param>
public static void Compute(double pUp, double rhoUp, double TUp, double gamma, double R,
double pDown, double area, double Cd,
out double mdot, out double rhoFace, out double uFace, out double pFace)
{
// mdot is positive from upstream to downstream.
double pr = Math.Max(pDown / pUp, 1e-6);
double prCrit = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
if (pr < prCrit) pr = prCrit;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -(gamma - 1.0) / gamma) - 1.0));
uFace = M * Math.Sqrt(gamma * R * TUp);
rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma);
pFace = pUp * pr;
mdot = rhoFace * uFace * area * Cd; // mass flow from upstream to downstream
}
}
}

228
Core/Junction.cs Normal file
View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using FluidSim.Components;
namespace FluidSim.Core
{
/// <summary>
/// Zerodimensional junction connecting multiple pipe ends.
/// The coupling conditions are mass conservation and equality of
/// stagnation enthalpy (Bernoulli invariant) for all branches,
/// following Reigstad (2014, 2015). A rootfinding method (Brent)
/// solves for the common junction pressure.
/// </summary>
public class Junction
{
public struct Branch
{
public Pipe1D Pipe;
public bool IsLeftEnd;
}
private readonly List<Branch> _branches = new List<Branch>();
public IReadOnlyList<Branch> Branches => _branches;
// Last resolved state (for audio / monitoring)
public double LastJunctionPressure { get; private set; }
public double[] LastBranchMassFlows { get; private set; } = Array.Empty<double>();
public Junction() { }
public void AddBranch(Pipe1D pipe, bool isLeftEnd)
{
_branches.Add(new Branch { Pipe = pipe, IsLeftEnd = isLeftEnd });
}
/// <summary>
/// Solve the junction for one substep. Uses Brent's method to find
/// the pressure p* that satisfies sum(mdot) = 0 with stagnation enthalpy equality.
/// </summary>
public void Resolve(double dtSub)
{
int nb = _branches.Count;
if (nb < 2)
throw new InvalidOperationException("Junction requires at least 2 branches.");
// Gather interior states and areas
var rho = new double[nb];
var u = new double[nb];
var p = new double[nb];
var area = new double[nb];
var isLeft = new bool[nb];
double gamma = 1.4;
double pMin = double.MaxValue, pMax = double.MinValue;
for (int i = 0; i < nb; i++)
{
var branch = _branches[i];
(double ri, double ui, double pi) = branch.IsLeftEnd
? branch.Pipe.GetInteriorStateLeft()
: branch.Pipe.GetInteriorStateRight();
rho[i] = ri; u[i] = ui; p[i] = pi;
area[i] = branch.Pipe.Area;
isLeft[i] = branch.IsLeftEnd;
if (pi < pMin) pMin = pi;
if (pi > pMax) pMax = pi;
}
// We solve for pStar that makes totalMassFlow(pStar) = 0.
// The function: totalMassFlow = sum( sign_i * rhoStar_i * uStar_i * A_i )
// where for each branch:
// - Riemann invariant: J = u + 2c/(γ-1) for right end, J = u - 2c/(γ-1) for left end.
// - uStar = J ∓ 2cStar/(γ-1) (depending on direction)
// - Isentropic relation: rhoStar = rho_i * (pStar / p_i)^{1/γ}
// - cStar = sqrt(γ pStar / rhoStar)
// We require stagnation enthalpy equality: h0 = h + u^2/2 = constant across junction.
// Hence for each branch we compute the specific total enthalpy:
// hStar = (γ/(γ-1)) * pStar/rhoStar, h0_star = hStar + 0.5 uStar^2.
// We enforce that all h0_star are equal. Mass conservation then determines pStar.
// This is a scalar rootfinding problem.
// Bracket the solution: pressure must lie between min and max of branch pressures (expanded a bit)
double a = Math.Max(100.0, pMin * 0.1);
double b = Math.Min(1e7, pMax * 10.0);
if (a >= b) { a = 100.0; b = 1e7; }
Func<double, double> f = pStar =>
{
double totalMdot = 0.0;
double h0Ref = 0.0;
bool first = true;
for (int i = 0; i < nb; i++)
{
double g = gamma;
double gm1 = g - 1.0;
double rhoI = rho[i], uI = u[i], pI = p[i];
double cI = Math.Sqrt(g * pI / rhoI);
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
double pratio = Math.Max(pStar / pI, 1e-6);
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
double cStar = Math.Sqrt(g * pStar / rhoStar);
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
double hStar = (g / gm1) * pStar / rhoStar;
double h0 = hStar + 0.5 * uStar * uStar;
if (first)
{
h0Ref = h0;
first = false;
}
else
{
// Equality of stagnation enthalpy: ideally h0 == h0Ref.
// We incorporate a penalty to enforce this.
}
// Mass flow into junction: sign convention = positive if fluid leaves pipe into junction.
double sign = isLeft[i] ? -1.0 : 1.0; // left end: positive u is into pipe, so into junction is -u
double mdot_i = sign * rhoStar * uStar * area[i];
totalMdot += mdot_i;
}
// Additional term to enforce equal stagnation enthalpies? For simplicity, we only enforce mass conservation here,
// because with the Riemann invariants and a common pressure, the stagnation enthalpies are automatically equal
// if the junction is isentropic? Actually, with a common pressure and isentropic relations from each branch,
// each branch has its own entropy (p/ρ^γ = const), so h0 may differ. The correct condition is mass conservation + equality of h0.
// To solve both, we would need to vary pStar and a common h0? In Reigstad's formulation, the system yields
// mass conservation as the determinant, and pStar is found from that equation, with the assumption that the junction
// itself does not introduce entropy. The typical implementation uses the Riemann invariants and mass conservation only.
// We'll stick to mass conservation for now.
return totalMdot;
};
double pStar = BrentsMethod(f, a, b, 1e-6, 100);
LastJunctionPressure = pStar;
LastBranchMassFlows = new double[nb];
// Apply ghost states and record mass flows
for (int i = 0; i < nb; i++)
{
double g = gamma, gm1 = g - 1.0;
double rhoI = rho[i], uI = u[i], pI = p[i];
double cI = Math.Sqrt(g * pI / rhoI);
double J = isLeft[i] ? uI - 2.0 * cI / gm1 : uI + 2.0 * cI / gm1;
double pratio = Math.Max(pStar / pI, 1e-6);
double rhoStar = rhoI * Math.Pow(pratio, 1.0 / g);
double cStar = Math.Sqrt(g * pStar / rhoStar);
double uStar = isLeft[i] ? J + 2.0 * cStar / gm1 : J - 2.0 * cStar / gm1;
double sign = isLeft[i] ? -1.0 : 1.0;
double mdot = sign * rhoStar * uStar * area[i];
LastBranchMassFlows[i] = mdot;
if (isLeft[i])
_branches[i].Pipe.SetGhostLeft(rhoStar, uStar, pStar);
else
_branches[i].Pipe.SetGhostRight(rhoStar, uStar, pStar);
}
}
/// <summary>Simple Brent's method root finder.</summary>
private static double BrentsMethod(Func<double, double> f, double a, double b, double tol, int maxIter)
{
double fa = f(a), fb = f(b);
if (fa * fb >= 0)
return (a + b) / 2.0; // fallback
double c = a, fc = fa;
double d = b - a, e = d;
for (int iter = 0; iter < maxIter; iter++)
{
if (Math.Abs(fc) < Math.Abs(fb))
{
a = b; b = c; c = a;
fa = fb; fb = fc; fc = fa;
}
double tol1 = 2 * double.Epsilon * Math.Abs(b) + 0.5 * tol;
double xm = 0.5 * (c - b);
if (Math.Abs(xm) <= tol1 || fb == 0.0)
return b;
if (Math.Abs(e) >= tol1 && Math.Abs(fa) > Math.Abs(fb))
{
double s = fb / fa;
double p, q;
if (a == c)
{
p = 2.0 * xm * s;
q = 1.0 - s;
}
else
{
q = fa / fc;
double r = fb / fc;
p = s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0));
q = (q - 1.0) * (r - 1.0) * (s - 1.0);
}
if (p > 0) q = -q; else p = -p;
s = e; e = d;
if (2.0 * p < 3.0 * xm * q - Math.Abs(tol1 * q) && p < Math.Abs(0.5 * s * q))
{
d = p / q;
}
else
{
d = xm; e = d;
}
}
else
{
d = xm; e = d;
}
a = b; fa = fb;
if (Math.Abs(d) > tol1)
b += d;
else
b += Math.Sign(xm) * tol1;
fb = f(b);
}
return b;
}
}
}

View File

@@ -1,72 +0,0 @@
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)
{
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 > 1, flow is INTO the cylinder (reverse), so we swap the roles.
bool reverse = (pr > 1.0);
if (reverse)
{
// Treat the cylinder as the downstream, the pipe as the upstream.
double p_up = downstreamPressure;
double T_up = 300.0; // pipe temperature (ambient)
double rho_up = downstreamPressure / (R * T_up);
double pr_rev = p0 / p_up; // now cylinder / pipe
if (pr_rev < choked) pr_rev = choked;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr_rev, -(gamma - 1.0) / gamma) - 1.0));
if (double.IsNaN(M)) return;
// Flow from pipe INTO cylinder (positive mass flow into volume)
uFace = M * Math.Sqrt(gamma * R * T_up);
rhoFace = rho_up * Math.Pow(pr_rev, 1.0 / gamma);
pFace = p_up * pr_rev;
massFlow = rhoFace * uFace * area;
// massFlow is positive = into cylinder
}
else
{
// Normal flow out of cylinder
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; // negative = out of cylinder
}
if (double.IsNaN(massFlow) || double.IsInfinity(massFlow))
massFlow = 0.0;
}
}
}

123
Core/OpenEndLink.cs Normal file
View File

@@ -0,0 +1,123 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
/// <summary>
/// Characteristic openend boundary condition.
/// For subsonic outflow the outgoing Riemann invariant is conserved,
/// and the ghost pressure is set to the prescribed ambient value.
/// </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;
// Last resolved state (for audio / monitoring)
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;
}
/// <summary>
/// Compute the ghost state and mass flow for one substep.
/// </summary>
public void Resolve(double dtSub)
{
(double rhoInt, double uInt, double pInt) = IsLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
double gamma = Gamma;
double gm1 = gamma - 1.0;
double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12));
double pAmb = AmbientPressure;
double rhoGhost, uGhost, pGhost;
double mdot;
if (IsLeftEnd)
{
// Left end: outgoing invariant is J- = u - 2c/(γ-1)
double J_minus = uInt - 2.0 * cInt / gm1;
if (uInt <= -cInt) // supersonic inflow (all info from outside)
{
// Simple reservoir model use ambient density and temperature 300 K
rhoGhost = pAmb / (287.0 * 300.0);
uGhost = uInt; // keep interior velocity (should be supersonic inward)
pGhost = pAmb;
}
else if (uInt < 0) // subsonic inflow
{
double rhoAmb = pAmb / (287.0 * 300.0);
double cAmb = Math.Sqrt(gamma * pAmb / rhoAmb);
uGhost = J_minus + 2.0 * cAmb / gm1;
rhoGhost = rhoAmb;
pGhost = pAmb;
}
else // subsonic outflow (uInt >= 0)
{
double s = pInt / Math.Pow(rhoInt, gamma);
rhoGhost = Math.Pow(pAmb / s, 1.0 / gamma);
double cGhost = Math.Sqrt(gamma * pAmb / rhoGhost);
uGhost = J_minus + 2.0 * cGhost / gm1;
if (uGhost < 0) uGhost = 0;
pGhost = pAmb;
}
}
else // Right end
{
// Right end: outgoing invariant is J+ = u + 2c/(γ-1)
double J_plus = uInt + 2.0 * cInt / gm1;
if (uInt >= cInt) // supersonic outflow
{
rhoGhost = rhoInt;
uGhost = uInt;
pGhost = pInt;
}
else if (uInt >= 0) // subsonic outflow
{
double s = pInt / Math.Pow(rhoInt, gamma);
rhoGhost = Math.Pow(pAmb / s, 1.0 / gamma);
double cGhost = Math.Sqrt(gamma * pAmb / rhoGhost);
uGhost = J_plus - 2.0 * cGhost / gm1;
if (uGhost < 0) uGhost = 0;
pGhost = pAmb;
}
else // subsonic inflow (uInt < 0)
{
double rhoAmb = pAmb / (287.0 * 300.0);
double cAmb = Math.Sqrt(gamma * pAmb / rhoAmb);
uGhost = J_plus - 2.0 * cAmb / gm1;
rhoGhost = rhoAmb;
pGhost = pAmb;
}
}
// Apply ghost to pipe
if (IsLeftEnd)
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost);
else
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost);
// Mass flow (positive = out of pipe)
double area = Pipe.Area;
mdot = rhoGhost * uGhost * area;
if (IsLeftEnd) mdot = -mdot; // positive u into pipe, so out of pipe is negative u
LastMassFlowRate = mdot;
LastFaceDensity = rhoGhost;
LastFaceVelocity = uGhost;
LastFacePressure = pGhost;
}
}
}

View File

@@ -1,100 +0,0 @@
using System;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
public static class OrificeBoundary
{
public static double MassFlow(double pA, double rhoA, double pB, double rhoB,
Connection conn)
{
if (double.IsNaN(pA) || double.IsNaN(rhoA) || double.IsNaN(pB) || double.IsNaN(rhoB) ||
double.IsInfinity(pA) || double.IsInfinity(rhoA) || double.IsInfinity(pB) || double.IsInfinity(rhoB) ||
pA <= 0 || rhoA <= 0 || pB <= 0 || rhoB <= 0)
return 0.0;
double dp = pA - pB;
double sign = Math.Sign(dp);
double absDp = Math.Abs(dp);
double rhoUp = dp >= 0 ? rhoA : rhoB;
double pUp = dp >= 0 ? pA : pB;
double pDown = dp >= 0 ? pB : pA;
double delta = 1e-6 * pUp;
if (absDp < delta)
{
double k = conn.DischargeCoefficient * conn.Area * Math.Sqrt(2 * rhoUp / delta);
return k * dp;
}
else
{
double pr = pDown / pUp;
double choked = Math.Pow(2.0 / (conn.Gamma + 1.0), conn.Gamma / (conn.Gamma - 1.0));
if (pr < choked)
{
double term = Math.Sqrt(conn.Gamma *
Math.Pow(2.0 / (conn.Gamma + 1.0), (conn.Gamma + 1.0) / (conn.Gamma - 1.0)));
double flow = conn.DischargeCoefficient * conn.Area *
Math.Sqrt(rhoUp * pUp) * term;
return sign * flow;
}
else
{
double ex = 1.0 - Math.Pow(pr, (conn.Gamma - 1.0) / conn.Gamma);
double flow = conn.DischargeCoefficient * conn.Area *
Math.Sqrt(2.0 * rhoUp * pUp * (conn.Gamma / (conn.Gamma - 1.0)) *
pr * pr * ex);
return sign * flow;
}
}
}
public static void PipeVolumeFlux(double pPipe, double rhoPipe, double uPipe,
double pVol, double rhoVol, double uVol,
Connection conn, double pipeArea,
bool isLeftBoundary,
out double massFlux, out double momFlux, out double energyFlux)
{
// ----- Compute STAGNATION pressures -----
double pStagPipe = pPipe + 0.5 * rhoPipe * uPipe * uPipe;
double pStagVol = pVol + 0.5 * rhoVol * uVol * uVol; // uVol is always 0 for your volumes
// Mass flow driven by stagnation pressure difference (positive = pipe→volume)
double mdot = MassFlow(pStagPipe, rhoPipe, pStagVol, rhoVol, conn);
// Limit mass flow to the amount that can leave/enter the pipe cell
double maxMdot = rhoPipe * pipeArea * 343.0;
if (Math.Abs(mdot) > maxMdot) mdot = Math.Sign(mdot) * maxMdot;
bool flowLeavesPipe = mdot > 0; // pipe → volume
double uFace, pFace, rhoFace;
double massFluxPerArea;
if (isLeftBoundary)
{
massFluxPerArea = -mdot / pipeArea;
if (flowLeavesPipe)
{ uFace = uPipe; pFace = pPipe; rhoFace = rhoPipe; }
else
{ uFace = uVol; pFace = pVol; rhoFace = rhoVol; }
}
else // right boundary
{
massFluxPerArea = mdot / pipeArea;
if (flowLeavesPipe)
{ uFace = uPipe; pFace = pPipe; rhoFace = rhoPipe; }
else
{ uFace = uVol; pFace = pVol; rhoFace = rhoVol; }
}
// Total enthalpy of the injected fluid
double specificEnthalpy = (1.4 / (1.4 - 1.0)) * pFace / Math.Max(rhoFace, 1e-12);
double totalEnthalpy = specificEnthalpy + 0.5 * uFace * uFace;
massFlux = massFluxPerArea;
momFlux = massFluxPerArea * uFace + pFace;
energyFlux = massFluxPerArea * totalEnthalpy;
}
}
}

140
Core/OrificeLink.cs Normal file
View File

@@ -0,0 +1,140 @@
using System;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
/// <summary>
/// Connects a port (volume or atmosphere) to one end of a pipe via an orifice.
/// The area can be dynamic (Func<double>).
/// </summary>
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 Gamma { get; set; } = 1.4;
public double GasConstant { get; set; } = 287.0;
// Last resolved state (for audio/monitoring)
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 OrificeLink(Port volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func<double> areaProvider)
{
VolumePort = volumePort ?? throw new ArgumentNullException(nameof(volumePort));
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
IsPipeLeftEnd = isPipeLeftEnd;
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
}
/// <summary>
/// Resolve the coupling for one substep. Computes nozzle flow (isentropic)
/// and sets the pipe ghost cell and the port flow rates.
/// </summary>
public void Resolve(double dtSub)
{
double area = AreaProvider();
if (area < 1e-12)
{
SetClosedWall();
return;
}
// Retrieve volume state
double volP = VolumePort.Pressure;
double volRho = VolumePort.Density;
double volT = VolumePort.Temperature;
double volH = VolumePort.SpecificEnthalpy;
// Retrieve pipe interior state at the connected end
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
// Determine upstream/downstream: if volume pressure > pipe pressure, flow is out of volume (negative into volume).
bool flowOutOfVolume = volP > pipeP;
double pUp, rhoUp, TUp, pDown;
if (flowOutOfVolume)
{
pUp = volP; rhoUp = volRho; TUp = volT; pDown = pipeP;
}
else
{
// Pipe is upstream
pUp = pipeP; rhoUp = pipeRho; TUp = pipeP / (pipeRho * GasConstant); // temperature from pipe
pDown = volP;
}
// Compute isentropic nozzle flow
IsentropicOrifice.Compute(pUp, rhoUp, TUp, Gamma, GasConstant, pDown, area, DischargeCoefficient,
out double mdotUpstreamToDown, out double rhoFace, out double uFace, out double pFace);
// mdotUpstreamToDown is positive from upstream to downstream.
// Convert to mass flow into volume (positive mdot = into volume).
double mdotVolume;
if (flowOutOfVolume)
mdotVolume = -mdotUpstreamToDown; // out of volume is negative
else
mdotVolume = mdotUpstreamToDown; // into volume is positive
// Clamp mass flow to available mass in volume (if it is a Volume0D)
if (VolumePort.Owner is Volume0D vol)
{
double maxMdot = vol.Mass / dtSub;
if (mdotVolume > maxMdot) mdotVolume = maxMdot;
if (mdotVolume < -maxMdot) mdotVolume = -maxMdot;
}
// Apply ghost state to pipe
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
Pipe.SetGhostRight(rhoFace, uFace, pFace);
// Store results
LastMassFlowRate = mdotVolume;
LastFaceDensity = rhoFace;
LastFaceVelocity = uFace;
LastFacePressure = pFace;
// Set port flow rates for volume integration
VolumePort.MassFlowRate = mdotVolume;
if (mdotVolume >= 0)
{
// Inflow: enthalpy comes from upstream (pipe)
double pPipe = pipeP;
double rhoPipe = pipeRho;
VolumePort.SpecificEnthalpy = Gamma / (Gamma - 1.0) * pPipe / rhoPipe;
}
else
{
// Outflow: volume's own specific enthalpy
VolumePort.SpecificEnthalpy = volH;
}
}
private void SetClosedWall()
{
var (rInt, uInt, pInt) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rInt, -uInt, pInt);
else
Pipe.SetGhostRight(rInt, -uInt, pInt);
LastMassFlowRate = 0.0;
LastFaceDensity = rInt;
LastFaceVelocity = 0.0;
LastFacePressure = pInt;
VolumePort.MassFlowRate = 0.0;
// Keep specific enthalpy as is (not used)
}
}
}

View File

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

View File

@@ -1,120 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
/// <summary>
/// Toplevel solver that owns all components and couplings,
/// orchestrates substepping, and exposes states for audio.
/// </summary>
public class Solver
{
private readonly List<Volume0D> _volumes = new();
private readonly List<Pipe1D> _pipes = new();
private readonly List<PipeVolumeConnection> _connections = new();
private readonly List<IComponent> _components = new();
private readonly List<OrificeLink> _orificeLinks = new();
private readonly List<Junction> _junctions = new();
private readonly List<OpenEndLink> _openEndLinks = new();
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 AddPipe(Pipe1D p) => _pipes.Add(p);
public void AddConnection(PipeVolumeConnection c) => _connections.Add(c);
public void SetTimeStep(double dt) => _dt = dt;
public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0)
public void AddComponent(IComponent component) => _components.Add(component);
public void AddOrificeLink(OrificeLink link) => _orificeLinks.Add(link);
public void AddJunction(Junction junction) => _junctions.Add(junction);
public void AddOpenEndLink(OpenEndLink link) => _openEndLinks.Add(link);
// Convenience: first pipes port B mass flow (often the exhaust)
public double ExhaustMassFlow
{
if (isA)
get
{
pipe.SetABoundaryType(type);
if (type == BoundaryType.OpenEnd) pipe.SetAAmbientPressure(ambientPressure);
}
else
{
pipe.SetBBoundaryType(type);
if (type == BoundaryType.OpenEnd) pipe.SetBAmbientPressure(ambientPressure);
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count > 0)
return Math.Abs(pipes[0].PortB.MassFlowRate);
return 0.0;
}
}
public float Step()
/// <summary>
/// Advance the whole system by one global time step.
/// </summary>
public void Step()
{
// 1. For each connection, handle flow or closed wall
foreach (var conn in _connections)
{
double area = conn.OrificeArea;
if (area < 1e-12) // valve closed → treat as solid wall
{
conn.Volume.MassFlowRateIn = 0.0;
conn.Volume.SpecificEnthalpyIn = conn.Volume.SpecificEnthalpy; // not used
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count == 0) return;
// Set ghost to a reflective wall (u = -u_pipe, same p, ρ)
int cellIdx = conn.IsPipeLeftEnd ? 0 : conn.Pipe.GetCellCount() - 1;
double rho = Math.Max(conn.Pipe.GetCellDensity(cellIdx), 1e-6);
double p = Math.Max(conn.Pipe.GetCellPressure(cellIdx), 100.0);
double u = conn.Pipe.GetCellVelocity(cellIdx);
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rho, -u, p);
else
conn.Pipe.SetGhostRight(rho, -u, p);
continue;
}
// Valve open → use the nozzle model
double downstreamPressure = conn.IsPipeLeftEnd
? conn.Pipe.GetCellPressure(0)
: conn.Pipe.GetCellPressure(conn.Pipe.GetCellCount() - 1);
NozzleFlow.Compute(conn.Volume, area, downstreamPressure,
out double mdot, out double rhoFace, out double uFace, out double pFace,
gamma: conn.Volume.Gamma);
// Clamp mdot to available mass
double maxMdot = conn.Volume.Mass / _dt;
conn.LastMassFlowIntoVolume = mdot;
if (mdot > maxMdot) mdot = maxMdot;
if (mdot < -maxMdot) mdot = -maxMdot;
conn.Volume.MassFlowRateIn = mdot;
// enthalpy: if inflow, use pipe enthalpy; if outflow, use cylinder enthalpy
if (mdot >= 0)
{
int cellIdx = conn.IsPipeLeftEnd ? 0 : conn.Pipe.GetCellCount() - 1;
double pPipe = Math.Max(conn.Pipe.GetCellPressure(cellIdx), 100.0);
double rhoPipe = Math.Max(conn.Pipe.GetCellDensity(cellIdx), 1e-6);
conn.Volume.SpecificEnthalpyIn = (conn.Volume.Gamma / (conn.Volume.Gamma - 1.0)) * pPipe / rhoPipe;
}
else
{
conn.Volume.SpecificEnthalpyIn = conn.Volume.SpecificEnthalpy;
}
// Integrate the volume
conn.Volume.Integrate(_dt);
// Set ghost from nozzle face state (but don't allow zero density)
if (rhoFace < 1e-6) rhoFace = Constants.Rho_amb;
if (pFace < 100.0) pFace = Constants.P_amb;
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
conn.Pipe.SetGhostRight(rhoFace, uFace, pFace);
}
// 2. Substep pipes
// 1. Determine substep count (max CFL over all pipes)
int nSub = 1;
foreach (var p in _pipes)
foreach (var p in pipes)
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt));
double dtSub = _dt / nSub;
// 2. Substep loop
for (int sub = 0; sub < nSub; sub++)
foreach (var p in _pipes)
{
// a) Resolve all orifice links (volume ↔ pipe)
foreach (var link in _orificeLinks)
link.Resolve(dtSub);
// b) Resolve all openend links (pipe → atmosphere)
foreach (var link in _openEndLinks)
link.Resolve(dtSub);
// c) Resolve all junctions (pipe ↔ pipe)
foreach (var junc in _junctions)
junc.Resolve(dtSub);
// d) Advance all pipes
foreach (var p in pipes)
p.SimulateSingleStep(dtSub);
}
// 3. Clear ghost flags
foreach (var p in _pipes)
p.ClearGhostFlag();
foreach (var p in pipes)
p.ClearGhostFlags();
// 4. Return exhaust tailpipe mass flow
if (_pipes.Count > 0)
return (float)_pipes[0].GetOpenEndMassFlow();
return 0f;
// 4. Integrate nonpipe components (volumes, atmosphere, etc.)
foreach (var comp in _components)
comp.UpdateState(_dt);
}
}
}