refactoring (broken right now)
This commit is contained in:
41
Core/IsentropicOrifice.cs
Normal file
41
Core/IsentropicOrifice.cs
Normal 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
228
Core/Junction.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Zero‑dimensional 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 root‑finding 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 sub‑step. 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 root‑finding 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
123
Core/OpenEndLink.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Characteristic open‑end 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 sub‑step.
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
140
Core/OrificeLink.cs
Normal 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 sub‑step. 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Core/Solver.cs
140
Core/Solver.cs
@@ -1,120 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Interfaces;
|
||||
|
||||
namespace FluidSim.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Top‑level solver that owns all components and couplings,
|
||||
/// orchestrates sub‑stepping, 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 pipe’s 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. Sub‑step pipes
|
||||
// 1. Determine sub‑step 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. Sub‑step 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 open‑end 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 non‑pipe components (volumes, atmosphere, etc.)
|
||||
foreach (var comp in _components)
|
||||
comp.UpdateState(_dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user