From 685b48b5778168d0b066e1bb41b0c316b7b5b0f3 Mon Sep 17 00:00:00 2001 From: grillkol Date: Thu, 7 May 2026 12:55:57 +0200 Subject: [PATCH] Open end working --- Components/Pipe1D.cs | 243 +++++++++++++++++--------------------- Core/IsentropicOrifice.cs | 50 ++++---- Core/OpenEndLink.cs | 98 ++++++--------- Core/OrificeLink.cs | 136 ++++++++++++--------- Core/SoundProcessor.cs | 39 +++--- Program.cs | 3 +- Scenarios/TestScenario.cs | 116 ++++++++++++------ 7 files changed, 355 insertions(+), 330 deletions(-) diff --git a/Components/Pipe1D.cs b/Components/Pipe1D.cs index bd0ba4d..39171ef 100644 --- a/Components/Pipe1D.cs +++ b/Components/Pipe1D.cs @@ -4,10 +4,8 @@ using FluidSim.Interfaces; namespace FluidSim.Components { /// - /// 1‑D compressible Euler pipe (finite‑volume, HLLC flux). - /// Boundary conditions are set externally via SetGhostLeft/Right. - /// Enforces that ghosts are always valid before stepping. - /// Uses exponential damping and Newtonian energy relaxation. + /// 1‑D compressible Euler pipe with Lax‑Friedrichs finite‑volume scheme. + /// Ghost states are set externally via SetGhostLeft/Right; they are always required. /// public class Pipe1D : IComponent { @@ -17,7 +15,6 @@ namespace FluidSim.Components public double DampingMultiplier { get; set; } = 1.0; public double EnergyRelaxationRate { get; set; } = 0.0; // 1/s - // Ambient pressure for the energy relaxation term (default 101325 Pa) private double _ambientPressure = 101325.0; public double AmbientPressure { @@ -29,37 +26,21 @@ namespace FluidSim.Components } } - // Geometry - private readonly int _n; // number of real cells - private readonly double _dx; // cell size (m) - private readonly double _diameter; // m + private readonly int _n; + private readonly double _dx; + private readonly double _diameter; private readonly double _gamma = 1.4; - // Conserved variables [0 .. _n-1] - private double[] _rho; - private double[] _rhou; - private double[] _E; + private double[] _rho, _rhou, _E; + private double[] _fluxM, _fluxP, _fluxE; // flux at cell faces (0.._n) - // Face fluxes [0 .. _n] - private double[] _fluxM; - private double[] _fluxP; - private double[] _fluxE; - - // Ghost cells (set externally) private double _rhoGhostL, _uGhostL, _pGhostL; private double _rhoGhostR, _uGhostR, _pGhostR; private bool _ghostLValid, _ghostRValid; - // Pre‑computed damping coefficient private double _laminarCoeff; - private double _ambientEnergyReference; // internal energy density at ambient pressure + private double _ambientEnergyReference; - /// - /// Initialise a pipe with a given cell count. - /// - /// Pipe length (m). - /// Cross‑sectional area (m²). - /// Number of finite‑volume cells (≥ 4). public Pipe1D(double length, double area, int cellCount) { if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4"); @@ -77,48 +58,32 @@ namespace FluidSim.Components _fluxP = new double[_n + 1]; _fluxE = new double[_n + 1]; - // Laminar damping coefficient for air at 20°C (multiplied by DampingMultiplier each step) double mu_air = 1.8e-5; double radius = _diameter * 0.5; _laminarCoeff = 8.0 * mu_air / (radius * radius); - // Ambient energy reference (internal energy per unit volume at 101325 Pa) _ambientEnergyReference = 101325.0 / (_gamma - 1.0); PortA = new Port { Owner = this }; PortB = new Port { Owner = this }; - // Initial state = still air at ambient conditions SetUniformState(1.225, 0.0, 101325.0); } IReadOnlyList IComponent.Ports => new[] { PortA, PortB }; - // No integration needed for pipes – state is advanced via sub‑steps public void UpdateState(double dt) { } - // ---------- Ghost cell interface ---------- + // ---------- Ghost interface ---------- public void SetGhostLeft(double rho, double u, double p) { - _rhoGhostL = rho; - _uGhostL = u; - _pGhostL = p; - _ghostLValid = true; + _rhoGhostL = rho; _uGhostL = u; _pGhostL = p; _ghostLValid = true; } - public void SetGhostRight(double rho, double u, double p) { - _rhoGhostR = rho; - _uGhostR = u; - _pGhostR = p; - _ghostRValid = true; - } - - public void ClearGhostFlags() - { - _ghostLValid = false; - _ghostRValid = false; + _rhoGhostR = rho; _uGhostR = u; _pGhostR = p; _ghostRValid = true; } + public void ClearGhostFlags() { _ghostLValid = false; _ghostRValid = false; } public (double rho, double u, double p) GetInteriorStateLeft() { @@ -127,7 +92,6 @@ namespace FluidSim.Components double p = PressureScalar(0); return (rho, u, p); } - public (double rho, double u, double p) GetInteriorStateRight() { double rho = Math.Max(_rho[_n - 1], 1e-12); @@ -135,18 +99,11 @@ namespace FluidSim.Components double p = PressureScalar(_n - 1); return (rho, u, p); } - public int CellCount => _n; - public double GetCellDensity(int i) => _rho[i]; - public double GetCellVelocity(int i) - { - double rho = Math.Max(_rho[i], 1e-12); - return _rhou[i] / rho; - } + public double GetCellVelocity(int i) => _rhou[i] / Math.Max(_rho[i], 1e-12); public double GetCellPressure(int i) => PressureScalar(i); - // ---------- Sub‑stepping ---------- public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8) { double maxW = 0.0; @@ -163,38 +120,65 @@ namespace FluidSim.Components return Math.Max(1, (int)Math.Ceiling(dtGlobal * maxW / (cflTarget * _dx))); } - // ---------- Main simulation step (per sub‑step) ---------- + // ---------- Main step (per sub‑step) ---------- public void SimulateSingleStep(double dtSub) { - // Enforce that both ends have been provided with ghost states if (!_ghostLValid || !_ghostRValid) - throw new InvalidOperationException("Pipe boundary ghosts not set before SimulateSingleStep."); + throw new InvalidOperationException("Ghost cells not set before SimulateSingleStep."); double dt = dtSub; int n = _n; - // Left boundary face (index 0) - HLLCFlux(_rhoGhostL, _uGhostL, _pGhostL, _rho[0], _rhou[0] / _rho[0], PressureScalar(0), - out _fluxM[0], out _fluxP[0], out _fluxE[0]); + // ---- Compute fluxes at all faces using Lax‑Friedrichs ---- + // Left face (0): between ghostL and cell 0 + double rL = Math.Max(_rhoGhostL, 1e-12); + double pL = _pGhostL; + double uL = _uGhostL; + double eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL; + + double rR = Math.Max(_rho[0], 1e-12); + double pR = PressureScalar(0); + double uR = _rhou[0] / rR; + double eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR; + + LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR, + out _fluxM[0], out _fluxP[0], out _fluxE[0]); // Internal faces (1 .. n-1) for (int f = 1; f < n; f++) { - double rhoL = Math.Max(_rho[f - 1], 1e-12); - double uL = _rhou[f - 1] / rhoL; - double pL = PressureScalar(f - 1); - double rhoR = Math.Max(_rho[f], 1e-12); - double uR = _rhou[f] / rhoR; - double pR = PressureScalar(f); - HLLCFlux(rhoL, uL, pL, rhoR, uR, pR, out _fluxM[f], out _fluxP[f], out _fluxE[f]); + int iL = f - 1; + int iR = f; + + rL = Math.Max(_rho[iL], 1e-12); + pL = PressureScalar(iL); + uL = _rhou[iL] / rL; + eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL; + + rR = Math.Max(_rho[iR], 1e-12); + pR = PressureScalar(iR); + uR = _rhou[iR] / rR; + eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR; + + LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR, + out _fluxM[f], out _fluxP[f], out _fluxE[f]); } - // Right boundary face (index n) - HLLCFlux(_rho[_n - 1], _rhou[_n - 1] / _rho[_n - 1], PressureScalar(_n - 1), - _rhoGhostR, _uGhostR, _pGhostR, - out _fluxM[n], out _fluxP[n], out _fluxE[n]); + // Right face (n): between cell n-1 and ghostR + rL = Math.Max(_rho[n - 1], 1e-12); + pL = PressureScalar(n - 1); + uL = _rhou[n - 1] / rL; + eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL; - // Cell update + rR = Math.Max(_rhoGhostR, 1e-12); + pR = _pGhostR; + uR = _uGhostR; + eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR; + + LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR, + out _fluxM[n], out _fluxP[n], out _fluxE[n]); + + // ---- Cell update ---- double dt_dx = dt / _dx; double coeff = _laminarCoeff * DampingMultiplier; double relaxRate = EnergyRelaxationRate; @@ -213,15 +197,12 @@ namespace FluidSim.Components double newRu = ru - dt_dx * dP; double newE = E - dt_dx * dE_flux; - // Wall friction damping (laminar) double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt); newRu *= dampingFactor; - // Newtonian cooling toward ambient energy double relaxFactor = Math.Exp(-relaxRate * dt); newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor; - // Clamps – minimum density 1e-12, minimum pressure 100 Pa newR = Math.Max(newR, 1e-12); double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12); double eMin = 100.0 / (_gamma - 1.0) + kin; @@ -232,77 +213,54 @@ namespace FluidSim.Components _E[i] = newE; } - // Update port states to reflect the current interior state (for audio / monitoring) + // Update port states (double rhoA, double uA, double pA) = GetInteriorStateLeft(); - PortA.Pressure = pA; - PortA.Density = rhoA; + PortA.Pressure = pA; PortA.Density = rhoA; PortA.Temperature = pA / (rhoA * 287.0); PortA.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pA / rhoA; (double rhoB, double uB, double pB) = GetInteriorStateRight(); - PortB.Pressure = pB; - PortB.Density = rhoB; + PortB.Pressure = pB; PortB.Density = rhoB; PortB.Temperature = pB / (rhoB * 287.0); PortB.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pB / rhoB; } - // ---------- Private helpers ---------- + // ---------- Lax‑Friedrichs flux ---------- + private void LaxFriedrichsFlux(double rL, double uL, double pL, double eL, + double rR, double uR, double pR, double eR, + out double fm, out double fp, out double fe) + { + // Primitive states + double rhoL = rL, rhoR = rR; + double EL = rhoL * eL; // total energy per volume = rho * (specific total energy) + double ER = rhoR * eR; + + // Conserved vectors U = (ρ, ρu, E) + // Flux F = (ρu, ρu²+p, (E+p)u) + double Fm_L = rhoL * uL; + double Fp_L = rhoL * uL * uL + pL; + double Fe_L = (EL + pL) * uL; + + double Fm_R = rhoR * uR; + double Fp_R = rhoR * uR * uR + pR; + double Fe_R = (ER + pR) * uR; + + // Lax‑Friedrichs dissipation coefficient α = max(|u|+c) over whole domain, but here we use local max to be simple: + double cL = Math.Sqrt(_gamma * pL / rL); + double cR = Math.Sqrt(_gamma * pR / rR); + double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR); + + fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rhoR - rhoL); + fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rhoR * uR - rhoL * uL); + fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (ER - EL); + } + private double PressureScalar(int i) { double rho = Math.Max(_rho[i], 1e-12); return (_gamma - 1.0) * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho); } - /// - /// HLLC approximate Riemann solver (Toro, 1997). - /// Computes the numerical flux at a face given left and right states. - /// - private void HLLCFlux(double rL, double uL, double pL, double rR, double uR, double pR, - out double fm, out double fp, out double fe) - { - double cL = Math.Sqrt(_gamma * pL / rL); - double cR = Math.Sqrt(_gamma * pR / rR); - double EL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL; // specific total energy - double ER = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR; - - // Wave speed estimates (Davis, 1988) - double SL = Math.Min(uL - cL, uR - cR); - double SR = Math.Max(uL + cL, uR + cR); - - double denom = rL * (SL - uL) - rR * (SR - uR); - double Ss = (pR - pL + rL * uL * (SL - uL) - rR * uR * (SR - uR)) / denom; - - double Fm_L = rL * uL; - double Fp_L = rL * uL * uL + pL; - double Fe_L = (rL * EL + pL) * uL; - - double Fm_R = rR * uR; - double Fp_R = rR * uR * uR + pR; - double Fe_R = (rR * ER + pR) * uR; - - if (SL >= 0) { fm = Fm_L; fp = Fp_L; fe = Fe_L; } - else if (SR <= 0) { fm = Fm_R; fp = Fp_R; fe = Fe_R; } - else if (Ss >= 0) - { - double rsL = rL * (SL - uL) / (SL - Ss); - double ps = pL + rL * (SL - uL) * (Ss - uL); - double EsL = EL + (Ss - uL) * (Ss + pL / (rL * (SL - uL))); - fm = rsL * Ss; - fp = rsL * Ss * Ss + ps; - fe = (rsL * EsL + ps) * Ss; - } - else - { - double rsR = rR * (SR - uR) / (SR - Ss); - double ps = pR + rR * (SR - uR) * (Ss - uR); - double EsR = ER + (Ss - uR) * (Ss + pR / (rR * (SR - uR))); - fm = rsR * Ss; - fp = rsR * Ss * Ss + ps; - fe = (rsR * EsR + ps) * Ss; - } - } - - /// Initialise all cells to a uniform state (rho, u, p). public void SetUniformState(double rho, double u, double p) { double e = p / ((_gamma - 1.0) * rho); @@ -314,5 +272,24 @@ namespace FluidSim.Components _E[i] = E; } } + + public void SetCellState(int i, double rho, double u, double p) + { + if (i < 0 || i >= _n) return; + double e = p / ((_gamma - 1.0) * rho); + double E = rho * e + 0.5 * rho * u * u; + _rho[i] = rho; + _rhou[i] = rho * u; + _E[i] = E; + } + + public void SetCellPressure(int i, double p) + { + if (i < 0 || i >= _n) return; + double rho = _rho[i]; + double u = _rhou[i] / rho; + double e = p / ((_gamma - 1.0) * rho); + _E[i] = rho * e + 0.5 * rho * u * u; + } } } \ No newline at end of file diff --git a/Core/IsentropicOrifice.cs b/Core/IsentropicOrifice.cs index effe861..ce23e83 100644 --- a/Core/IsentropicOrifice.cs +++ b/Core/IsentropicOrifice.cs @@ -4,38 +4,38 @@ namespace FluidSim.Core { /// /// Compressible flow through an orifice, modelled as an isentropic nozzle. - /// Supports choked and unchoked flow, forward and reverse. + /// 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. /// public static class IsentropicOrifice { - /// - /// Compute mass flow and face primitive state for an orifice. - /// - /// Upstream stagnation pressure (Pa). - /// Upstream stagnation density (kg/m³). - /// Ratio of specific heats. - /// Specific gas constant (J/kg·K). - /// Downstream static pressure (Pa). - /// Effective orifice area (m²). - /// Discharge coefficient (default 0.62). - /// Mass flow rate (kg/s), positive from upstream to downstream. - /// Face density (kg/m³). - /// Face velocity (m/s). - /// Face pressure (Pa). - 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) + 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) { - // 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; + mdot = 0; rhoFace = rhoUp; uFace = 0; pFace = pUp; - 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); + if (area <= 0 || pUp <= 0 || rhoUp <= 0 || TUp <= 0) + return; + + double pr = pDown / pUp; + if (pr < 1e-6) pr = 1e-6; + + 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); + uFace = M * aUp; rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma); pFace = pUp * pr; - mdot = rhoFace * uFace * area * Cd; // mass flow from upstream to downstream + mdot = rhoFace * uFace * area * Cd; // positive from upstream to downstream } } } \ No newline at end of file diff --git a/Core/OpenEndLink.cs b/Core/OpenEndLink.cs index 939c1fe..a6efce3 100644 --- a/Core/OpenEndLink.cs +++ b/Core/OpenEndLink.cs @@ -3,19 +3,15 @@ using FluidSim.Components; namespace FluidSim.Core { - /// - /// 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. - /// 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 GasConstant { get; set; } = 287.0; + public double AmbientTemperature { get; set; } = 300.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; } @@ -27,9 +23,6 @@ namespace FluidSim.Core IsLeftEnd = isLeftEnd; } - /// - /// Compute the ghost state and mass flow for one sub‑step. - /// public void Resolve(double dtSub) { (double rhoInt, double uInt, double pInt) = IsLeftEnd @@ -40,80 +33,61 @@ namespace FluidSim.Core double gm1 = gamma - 1.0; double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12)); double pAmb = AmbientPressure; + double rhoAmb = pAmb / (GasConstant * AmbientTemperature); + double aAmb = Math.Sqrt(gamma * pAmb / rhoAmb); double rhoGhost, uGhost, pGhost; - double mdot; - if (IsLeftEnd) + // ----- Supersonic outflow: extrapolate interior ----- + bool supersonicOut = IsLeftEnd ? (uInt <= -cInt) : (uInt >= cInt); + if (supersonicOut) { - // Left end: outgoing invariant is J- = u - 2c/(γ-1) + rhoGhost = rhoInt; + uGhost = uInt; + pGhost = pInt; + } + else + { + // Riemann invariants + double J_plus = uInt + 2.0 * cInt / gm1; 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; + // Trial subsonic outflow ghost state + double s = pInt / Math.Pow(rhoInt, gamma); + double rhoOut = Math.Pow(pAmb / s, 1.0 / gamma); + double cOut = Math.Sqrt(gamma * pAmb / rhoOut); + double uOut = IsLeftEnd + ? (J_minus + 2.0 * cOut / gm1) + : (J_plus - 2.0 * cOut / gm1); - if (uInt >= cInt) // supersonic outflow + bool outflowPossible = IsLeftEnd ? (uOut <= 0) : (uOut >= 0); + + if (outflowPossible) { - 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; + // Subsonic outflow pGhost = pAmb; + rhoGhost = rhoOut; + uGhost = uOut; } - else // subsonic inflow (uInt < 0) + else { - double rhoAmb = pAmb / (287.0 * 300.0); - double cAmb = Math.Sqrt(gamma * pAmb / rhoAmb); - uGhost = J_plus - 2.0 * cAmb / gm1; + // Subsonic inflow (ambient reservoir model) + pGhost = pAmb; rhoGhost = rhoAmb; - pGhost = pAmb; + uGhost = IsLeftEnd + ? (J_minus + 2.0 * aAmb / gm1) + : (J_plus - 2.0 * aAmb / gm1); } } - // 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 + double mdot = rhoGhost * uGhost * area; + if (IsLeftEnd) mdot = -mdot; LastMassFlowRate = mdot; LastFaceDensity = rhoGhost; LastFaceVelocity = uGhost; diff --git a/Core/OrificeLink.cs b/Core/OrificeLink.cs index 156590b..2843589 100644 --- a/Core/OrificeLink.cs +++ b/Core/OrificeLink.cs @@ -6,7 +6,8 @@ namespace FluidSim.Core { /// /// Connects a port (volume or atmosphere) to one end of a pipe via an orifice. - /// The area can be dynamic (Func). + /// Uses the isentropic nozzle model for the steady‑state relationship, + /// and includes acoustic inertance for dynamic (Helmholtz) behaviour. /// public class OrificeLink { @@ -15,105 +16,131 @@ namespace FluidSim.Core public bool IsPipeLeftEnd { get; } public Func 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) + // Acoustic length (wall thickness + end correction) – controls the resonance frequency + public double EffectiveLength { get; set; } = 0.001; // 1 mm + + // Whether to include inertance; if false, uses the steady‑state nozzle model directly + public bool UseInertance { get; set; } = true; + + // Current mass flow (kg/s, positive = volume → pipe) + private double _mdot; + 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 areaProvider) + public OrificeLink(Port? volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func areaProvider) { - VolumePort = volumePort ?? throw new ArgumentNullException(nameof(volumePort)); + VolumePort = volumePort; // null is allowed Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe)); IsPipeLeftEnd = isPipeLeftEnd; AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider)); + _mdot = 0.0; } - /// - /// Resolve the coupling for one sub‑step. Computes nozzle flow (isentropic) - /// and sets the pipe ghost cell and the port flow rates. - /// public void Resolve(double dtSub) { double area = AreaProvider(); - if (area < 1e-12) + // Closed wall or missing volume port => reflective boundary + if (area < 1e-12 || VolumePort == null) { SetClosedWall(); return; } - // Retrieve volume state - double volP = VolumePort.Pressure; + // Gather volume state + double volP = VolumePort.Pressure; double volRho = VolumePort.Density; - double volT = VolumePort.Temperature; - double volH = VolumePort.SpecificEnthalpy; + double volT = VolumePort.Temperature; + double volH = VolumePort.SpecificEnthalpy; - // Retrieve pipe interior state at the connected end + // Gather 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) + double pipeT = pipeP / Math.Max(pipeRho * 287.0, 1e-12); + double gamma = 1.4; + double R = 287.0; + + // ---- Steady‑state mass flow from isentropic nozzle ---- + double mdotSS; // positive = volume → pipe + double rhoFace, uFace, pFace; + + if (volP >= pipeP) { - pUp = volP; rhoUp = volRho; TUp = volT; pDown = pipeP; + IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient, + out double mdotUpToDown, out rhoFace, out uFace, out pFace); + mdotSS = mdotUpToDown; // volume → pipe } else { - // Pipe is upstream - pUp = pipeP; rhoUp = pipeRho; TUp = pipeP / (pipeRho * GasConstant); // temperature from pipe - pDown = volP; + IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient, + out double mdotUpToDown, out rhoFace, out uFace, out pFace); + mdotSS = -mdotUpToDown; // pipe → volume → negative for volume→pipe convention } - // 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 + // ---- Inertance ODE (optional) ---- + 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 - mdotVolume = mdotUpstreamToDown; // into volume is positive + { + _mdot = mdotSS; + } - // Clamp mass flow to available mass in volume (if it is a Volume0D) + // Clamp outflow to available mass (if finite volume) if (VolumePort.Owner is Volume0D vol) { - double maxMdot = vol.Mass / dtSub; - if (mdotVolume > maxMdot) mdotVolume = maxMdot; - if (mdotVolume < -maxMdot) mdotVolume = -maxMdot; + double maxOut = vol.Mass / dtSub; + if (_mdot > maxOut) _mdot = maxOut; } - // Apply ghost state to pipe + // ---- Ghost state ---- + // Density = upstream density (consistent with current flow direction) + rhoFace = _mdot >= 0 ? volRho : pipeRho; + // Pressure = downstream pressure (consistent with nozzle exit) + pFace = _mdot >= 0 ? pipeP : volP; + // Velocity magnitude derived from actual mass flow + double mdotMag = Math.Abs(_mdot); + uFace = mdotMag / (rhoFace * area); + + if (IsPipeLeftEnd) + uFace = _mdot >= 0 ? uFace : -uFace; // left end: positive u = into pipe + else + uFace = _mdot >= 0 ? -uFace : uFace; // right end: positive u = out of pipe + + // Apply ghost to pipe if (IsPipeLeftEnd) Pipe.SetGhostLeft(rhoFace, uFace, pFace); else Pipe.SetGhostRight(rhoFace, uFace, pFace); - // Store results - LastMassFlowRate = mdotVolume; - LastFaceDensity = rhoFace; + // ---- Store results ---- + double mdotIntoVolume = -_mdot; // positive = into volume + LastMassFlowRate = mdotIntoVolume; + LastFaceDensity = rhoFace; LastFaceVelocity = uFace; LastFacePressure = pFace; - // Set port flow rates for volume integration - VolumePort.MassFlowRate = mdotVolume; - if (mdotVolume >= 0) + VolumePort.MassFlowRate = mdotIntoVolume; + + // Enthalpy for volume integration + if (mdotIntoVolume >= 0) // inflow → pipe enthalpy { - // Inflow: enthalpy comes from upstream (pipe) - double pPipe = pipeP; - double rhoPipe = pipeRho; - VolumePort.SpecificEnthalpy = Gamma / (Gamma - 1.0) * pPipe / rhoPipe; + double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12); + VolumePort.SpecificEnthalpy = hPipe; } - else + else // outflow → volume's own enthalpy { - // Outflow: volume's own specific enthalpy VolumePort.SpecificEnthalpy = volH; } } @@ -130,11 +157,12 @@ namespace FluidSim.Core Pipe.SetGhostRight(rInt, -uInt, pInt); LastMassFlowRate = 0.0; - LastFaceDensity = rInt; + LastFaceDensity = rInt; LastFaceVelocity = 0.0; LastFacePressure = pInt; - VolumePort.MassFlowRate = 0.0; - // Keep specific enthalpy as is (not used) + // Don't touch VolumePort if it's null + if (VolumePort != null) + VolumePort.MassFlowRate = 0.0; } } } \ No newline at end of file diff --git a/Core/SoundProcessor.cs b/Core/SoundProcessor.cs index 9229e9f..59d9fba 100644 --- a/Core/SoundProcessor.cs +++ b/Core/SoundProcessor.cs @@ -1,18 +1,20 @@ using System; -using FluidSim.Interfaces; +using FluidSim.Core; namespace FluidSim.Core { public class SoundProcessor { private readonly double dt; - private readonly double scaleFactor; // 1 / (4π r) and a user gain + private readonly double scaleFactor; // 1 / (4π r) private double prevMassFlowOut; - - // Simple low‑pass for derivative smoothing (≈ 2‑3 ms) private double smoothDMdt; private readonly double alpha; + // New: low‑pass the mass flow signal before derivative + private double flowLP; + private readonly double lpAlpha; + public float Gain { get; set; } = 1.0f; public SoundProcessor(int sampleRate, double listenerDistanceMeters = 1.0) @@ -20,29 +22,34 @@ namespace FluidSim.Core dt = 1.0 / sampleRate; scaleFactor = 1.0 / (4.0 * Math.PI * listenerDistanceMeters); - // Smoothing time constant ~ 2 ms, blocks single‑sample spikes - double tau = 0.002; + // Smoothing time constant for the derivative: 10 ms (much smoother) + double tau = 0.010; // 10 ms alpha = Math.Exp(-dt / tau); + + // Low‑pass time constant for the mass flow: 5 ms (kneecap high‑freq directly) + double tauLP = 0.005; + lpAlpha = Math.Exp(-dt / tauLP); } - public float Process(Port port) + public float Process(OpenEndLink openEnd) { - // Outflow mass flow (positive = leaving pipe) - double flowOut = -port.MassFlowRate; + double flowOut = openEnd.LastMassFlowRate; - // Derivative - double rawDerivative = (flowOut - prevMassFlowOut) / dt; - prevMassFlowOut = flowOut; + // Low‑pass the mass flow signal + flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut; - // Smooth the derivative to kill isolated spikes + // Derivative of the smoothed mass flow + double rawDerivative = (flowLP - prevMassFlowOut) / dt; + prevMassFlowOut = flowLP; + + // Smooth the derivative smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative; // Far‑field monopole pressure double pressure = smoothDMdt * scaleFactor * Gain; - // Soft clip to ±1 for audio output (safe limit) - float sample = (float)Math.Tanh(pressure); - return sample; + // Soft clip to ±1 (should rarely trigger now) + return (float)Math.Tanh(pressure); } } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 330b59b..f4cd113 100644 --- a/Program.cs +++ b/Program.cs @@ -14,7 +14,8 @@ public class Program private static Scenario scenario; // Speed control (existing + new throttle) - private static double desiredSpeed = 0.01; + private static double desiredSpeed = 0.001; + //private static double desiredSpeed = 1.0; private static double currentSpeed = desiredSpeed; private const double MinSpeed = 0.0001; private const double MaxSpeed = 1.0; diff --git a/Scenarios/TestScenario.cs b/Scenarios/TestScenario.cs index 6fead4d..6178091 100644 --- a/Scenarios/TestScenario.cs +++ b/Scenarios/TestScenario.cs @@ -9,40 +9,41 @@ namespace FluidSim.Tests public class TestScenario : Scenario { private Solver solver; - private Volume0D volume; private Pipe1D pipe; - private Atmosphere atmosphere; - private OrificeLink orificeLink; - private OpenEndLink openEndLink; + private OrificeLink closedEnd; // left end – closed wall + private OpenEndLink openEndLink; // right end – atmosphere + private SoundProcessor soundProcessor; + private OutdoorExhaustReverb reverb; private int stepCount; + private double simTime; // elapsed simulation time (seconds) + private double pulseInterval = 0.1; // seconds between pulses + private double nextPulseTime; + private double dt; public override void Initialize(int sampleRate) { - double dt = 1.0 / sampleRate; + dt = 1.0 / sampleRate; + + soundProcessor = new SoundProcessor(sampleRate, 1); + soundProcessor.Gain = 10; + + reverb = new OutdoorExhaustReverb(sampleRate); solver = new Solver(); solver.SetTimeStep(dt); - volume = new Volume0D(1e-3, 150000.0, 300.0); - solver.AddComponent(volume); - - pipe = new Pipe1D(2.0, 1e-4, 20); + // Pipe: 2 m long, 1 cm² area, 200 cells + pipe = new Pipe1D(length: 2, area: 1e-4, cellCount: 100); solver.AddComponent(pipe); - atmosphere = new Atmosphere(); - solver.AddComponent(atmosphere); + // Initially pipe at ambient conditions + pipe.SetUniformState(1.225, 0.0, 101325.0); - // Volume → left pipe end (orifice) - var volPort = volume.CreatePort(); - orificeLink = new OrificeLink(volPort, pipe, isPipeLeftEnd: true, areaProvider: () => 1e-5) - { - DischargeCoefficient = 0.62, - Gamma = volume.Gamma, - GasConstant = volume.GasConstant - }; - solver.AddOrificeLink(orificeLink); + // Left end: closed wall (area = 0 → reflective) + closedEnd = new OrificeLink(null, pipe, isPipeLeftEnd: true, areaProvider: () => 0.0); + solver.AddOrificeLink(closedEnd); - // Right pipe end → atmosphere (characteristic open‑end) + // Right end: open to atmosphere openEndLink = new OpenEndLink(pipe, isLeftEnd: false) { AmbientPressure = 101325.0, @@ -51,45 +52,82 @@ namespace FluidSim.Tests solver.AddOpenEndLink(openEndLink); stepCount = 0; - Console.WriteLine("TestScenario initialized with sampleRate = " + sampleRate); + simTime = 0.0; + nextPulseTime = pulseInterval; // first pulse at 100 ms + + Console.WriteLine("Pulse reflection test – closed left, open right"); + Console.WriteLine("Pulse injected every 100 ms at left end (cell 0)"); } public override float Process() { solver.Step(); stepCount++; + simTime += dt; - if (stepCount % 100 == 0) + // ---- Inject a pressure pulse at the closed end every 100 ms ---- + if (simTime >= nextPulseTime) { - double volPressure = volume.Pressure; - double volMass = volume.Mass; - double pipeLeftPressure = pipe.GetCellPressure(0); - double pipeRightPressure = pipe.GetCellPressure(pipe.CellCount - 1); - double mdotOrifice = orificeLink.LastMassFlowRate; - double mdotOpen = openEndLink.LastMassFlowRate; + // Apply a Gaussian pulse to the first few cells + double ambientPressure = 101325.0; + double pulseAmplitude = 20 * ambientPressure; // 0.5 atm overpressure + double pulseWidth = 0.05; // m (spread over a few cells) + int n = pipe.CellCount; + double dx = 2.0 / n; - Console.WriteLine($"Step {stepCount}:"); - Console.WriteLine($" Vol Pressure = {volPressure:F1} Pa, Mass = {volMass:E4} kg"); - Console.WriteLine($" Pipe left P = {pipeLeftPressure:F1} Pa, right P = {pipeRightPressure:F1} Pa"); - Console.WriteLine($" Orifice mdot = {mdotOrifice:E4} kg/s, Open‑end mdot = {mdotOpen:E4} kg/s"); - Console.WriteLine(); + // Only modify cells within 2*pulseWidth from the left end + int maxCell = Math.Min(5, n - 1); // at most the first 5 cells + for (int i = 0; i <= maxCell; i++) + { + double x = (i + 0.5) * dx; + double P = pulseAmplitude * Math.Exp(-x * x / (pulseWidth * pulseWidth)); + double currentP = pipe.GetCellPressure(i); + double newP = P; + // Update pressure, keeping density and velocity unchanged + // We recompute total energy accordingly + double rho = pipe.GetCellDensity(i); + double u = pipe.GetCellVelocity(i); + double e = newP / ((1.4 - 1.0) * rho); + double E = rho * e + 0.5 * rho * u * u; + pipe.SetCellState(i, rho, u, newP); + } + Console.WriteLine($"Pulse injected at t = {simTime:F3} s"); + nextPulseTime += pulseInterval; } - // Audio sample from the open‑end mass flow - return (float)openEndLink.LastMassFlowRate; + // Audio from open‑end mass flow + float sample = soundProcessor.Process(openEndLink); + + // Log every 200 steps + if (stepCount % 1000 == 0) + { + int leftIdx = 0; + int midIdx = pipe.CellCount / 2; + int rightIdx = pipe.CellCount - 1; + double pL = pipe.GetCellPressure(leftIdx); + double pM = pipe.GetCellPressure(midIdx); + double pR = pipe.GetCellPressure(rightIdx); + + Console.WriteLine($"Step {stepCount}: P_left={pL:F1} Pa, P_mid={pM:F1} Pa, P_right={pR:F1} Pa"); + } + + if (double.IsNaN(pipe.GetCellPressure(0))) + { + Console.WriteLine("NaN detected – stopping simulation."); + return 0f; + } + + return reverb.Process(sample); } public override void Draw(RenderWindow target) { float winWidth = target.GetView().Size.X; float winHeight = target.GetView().Size.Y; - float pipeCenterY = winHeight / 2f; float margin = 60f; float pipeStartX = margin; float pipeEndX = winWidth - margin; - - // Use the shared pipe drawing from the base class DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX); } }