using System; using System.Collections.Generic; using FluidSim.Components; namespace FluidSim.Core { /// /// 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. /// public class Junction { public struct Branch { public Pipe1D Pipe; public bool IsLeftEnd; } private readonly List _branches = new List(); public IReadOnlyList Branches => _branches; // Last resolved state (for audio / monitoring) public double LastJunctionPressure { get; private set; } public double[] LastBranchMassFlows { get; private set; } = Array.Empty(); public Junction() { } public void AddBranch(Pipe1D pipe, bool isLeftEnd) { _branches.Add(new Branch { Pipe = pipe, IsLeftEnd = isLeftEnd }); } /// /// 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. /// 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 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); } } /// Simple Brent's method root finder. private static double BrentsMethod(Func 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; } } }