using System; using System.Collections.Generic; using FluidSim.Interfaces; namespace FluidSim.Components { /// /// Zero‑dimensional control volume with arbitrary number of ports. /// Integrates mass and energy fluxes from all ports. /// Safeguards keep a tiny amount of gas to avoid negative states. /// public class Volume0D : IComponent { public List Ports { get; } = new List(); public double Mass { get; private set; } public double InternalEnergy { get; private set; } public double Volume { get; set; } public double Dvdt { get; set; } public double Gamma { get; set; } = 1.4; public double GasConstant { get; set; } = 287.0; // Ambient pressure used for emergency refill – default 101325 Pa public double AmbientPressure { get; set; } = 101325.0; // Derived quantities public double Density => Mass / Math.Max(Volume, 1e-12); public double Pressure => (Gamma - 1.0) * InternalEnergy / Math.Max(Volume, 1e-12); public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12); public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Math.Max(Density, 1e-12); public Volume0D(double initialVolume, double initialPressure, double initialTemperature, double gasConstant = 287.0, double gamma = 1.4) { GasConstant = gasConstant; Gamma = gamma; Volume = initialVolume; Dvdt = 0.0; double rho0 = initialPressure / (GasConstant * initialTemperature); Mass = rho0 * Volume; InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0); } /// Add a new port and initialise it to the volume's current state. public Port CreatePort() { var port = new Port { Owner = this }; // Set the port state immediately to avoid a mismatch before the first integration port.Pressure = Pressure; port.Density = Density; port.Temperature = Temperature; port.SpecificEnthalpy = SpecificEnthalpy; Ports.Add(port); return port; } /// /// Integrate over dt using the MassFlowRate and SpecificEnthalpy /// that have been set on each port during the coupling resolution phase. /// public void UpdateState(double dt) { double totalMdot = 0.0; double totalEdot = 0.0; foreach (var port in Ports) { totalMdot += port.MassFlowRate; // mdot * h gives energy flow: positive mdot = inflow, negative = outflow totalEdot += port.MassFlowRate * port.SpecificEnthalpy; } double dm = totalMdot * dt; double dE = totalEdot * dt - Pressure * Dvdt * dt; // piston work Mass += dm; InternalEnergy += dE; // Safeguards: keep at least 1 µg of gas at a small pressure double V = Math.Max(Volume, 1e-12); if (Mass < 1e-9) { Mass = 1e-9; InternalEnergy = AmbientPressure * V / (Gamma - 1.0); } else if (InternalEnergy < 0.0) { InternalEnergy = AmbientPressure * V / (Gamma - 1.0); } // Final non‑negative clamps (should not be needed after above) if (Mass < 0.0) Mass = 1e-9; if (InternalEnergy < 0.0) InternalEnergy = AmbientPressure * V / (Gamma - 1.0); // Push updated state back to all ports double p = Pressure, rho = Density, T = Temperature, h = SpecificEnthalpy; foreach (var port in Ports) { port.Pressure = p; port.Density = rho; port.Temperature = T; port.SpecificEnthalpy = h; // will be overwritten by couplings for inflow, but this is the default } } IReadOnlyList IComponent.Ports => Ports; } }