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;
}
}