Helmholtz testing (no decay bug)

This commit is contained in:
max
2026-05-09 01:44:35 +02:00
parent 9c9e23147a
commit 77ef4753a3
23 changed files with 1811 additions and 2118 deletions

View File

@@ -2,18 +2,15 @@ using FluidSim.Interfaces;
namespace FluidSim.Components
{
/// <summary>
/// Represents the ambient atmosphere constant pressure/temperature reservoir.
/// </summary>
public class Atmosphere : IComponent
{
public double Pressure { get; set; } = 101325.0;
public double Temperature { get; set; } = 300.0;
public double GasConstant { get; set; } = 287.0;
public double Gamma => 1.4;
public float Pressure { get; set; } = 101325f;
public float Temperature { get; set; } = 300f;
public float GasConstant { get; set; } = 287f;
public float Gamma => 1.4f;
public double Density => Pressure / (GasConstant * Temperature);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density;
public float Density => Pressure / (GasConstant * Temperature);
public float SpecificEnthalpy => Gamma / (Gamma - 1f) * Pressure / Density;
public Port Port { get; }
@@ -25,9 +22,8 @@ namespace FluidSim.Components
public IReadOnlyList<Port> Ports => new[] { Port };
public void UpdateState(double dt)
public void UpdateState(float dt)
{
// Atmosphere is static just ensure the port reflects current values
UpdatePort();
}
@@ -37,7 +33,7 @@ namespace FluidSim.Components
Port.Density = Density;
Port.Temperature = Temperature;
Port.SpecificEnthalpy = SpecificEnthalpy;
Port.AirFraction = 1.0;
Port.AirFraction = 1f;
}
}
}

View File

@@ -1,54 +1,52 @@
// Components/Crankshaft.cs
using System;
namespace FluidSim.Components
{
public class Crankshaft
{
public double AngularVelocity { get; set; } // rad/s
public double CrankAngle { get; set; } // rad, 0 … 4π (fourstroke cycle)
public double PreviousAngle { get; set; } // ← now has public setter
public float AngularVelocity; // rad/s
public float CrankAngle; // rad, 0 … 4π
public float PreviousAngle;
public double Inertia { get; set; } = 0.2;
public double FrictionConstant { get; set; } = 0.0; // N·m
public double FrictionViscous { get; set; } = 0.000; // N·m per rad/s
public float Inertia = 0.2f;
public float FrictionConstant; // N·m
public float FrictionViscous; // N·m per rad/s
private double externalTorque;
private float externalTorque;
public Crankshaft(double initialRPM = 400.0)
public Crankshaft(float initialRPM = 400f)
{
AngularVelocity = initialRPM * 2.0 * Math.PI / 60.0;
CrankAngle = 0.0;
PreviousAngle = 0.0;
AngularVelocity = initialRPM * 2f * MathF.PI / 60f;
CrankAngle = 0f;
PreviousAngle = 0f;
}
public void AddTorque(double torque) => externalTorque += torque;
public void AddTorque(float torque) => externalTorque += torque;
public void Step(double dt)
public void Step(float dt)
{
// Catch NaN before it propagates
if (double.IsNaN(AngularVelocity) || double.IsInfinity(AngularVelocity))
AngularVelocity = 0.0;
if (double.IsNaN(externalTorque) || double.IsInfinity(externalTorque))
externalTorque = 0.0;
if (float.IsNaN(AngularVelocity) || float.IsInfinity(AngularVelocity))
AngularVelocity = 0f;
if (float.IsNaN(externalTorque) || float.IsInfinity(externalTorque))
externalTorque = 0f;
PreviousAngle = CrankAngle;
double friction = FrictionConstant * Math.Sign(AngularVelocity) + FrictionViscous * AngularVelocity;
double netTorque = externalTorque - friction;
double alpha = netTorque / Inertia;
float friction = FrictionConstant * MathF.Sign(AngularVelocity)
+ FrictionViscous * AngularVelocity;
float netTorque = externalTorque - friction;
float alpha = netTorque / Inertia;
AngularVelocity += alpha * dt;
if (AngularVelocity < 0) AngularVelocity = 0;
if (AngularVelocity < 0f) AngularVelocity = 0f;
CrankAngle += AngularVelocity * dt;
if (CrankAngle >= 4f * MathF.PI)
CrankAngle -= 4f * MathF.PI;
else if (CrankAngle < 0f)
CrankAngle += 4f * MathF.PI;
if (CrankAngle >= 4.0 * Math.PI)
CrankAngle -= 4.0 * Math.PI;
else if (CrankAngle < 0)
CrankAngle += 4.0 * Math.PI;
externalTorque = 0.0;
externalTorque = 0f;
}
}
}

View File

@@ -13,144 +13,103 @@ namespace FluidSim.Components
private readonly Port[] _ports;
IReadOnlyList<Port> IComponent.Ports => _ports;
// Geometry
public double Bore { get; }
public double Stroke { get; }
public double ConRodLength { get; }
public double CompressionRatio { get; }
public float Bore { get; }
public float Stroke { get; }
public float ConRodLength { get; }
public float CompressionRatio { get; }
// Valve timings (degrees, 0 = TDC compression, 720° full cycle)
public double IVO { get; }
public double IVC { get; }
public double EVO { get; }
public double EVC { get; }
public float IVO, IVC, EVO, EVC; // degrees
public float IntakeValveDiameter = 0.03f;
public float ExhaustValveDiameter = 0.028f;
public float IntakeValveLift = 0.005f;
public float ExhaustValveLift = 0.005f;
// Valve geometry
public double IntakeValveDiameter { get; set; } = 0.030;
public double ExhaustValveDiameter { get; set; } = 0.028;
public double IntakeValveLift { get; set; } = 0.005;
public double ExhaustValveLift { get; set; } = 0.005;
public float IntakeValveMaxArea => MathF.PI * IntakeValveDiameter * IntakeValveLift;
public float ExhaustValveMaxArea => MathF.PI * ExhaustValveDiameter * ExhaustValveLift;
public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift;
public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift;
public float SparkAdvance = 20f;
public float WiebeA = 5f, WiebeM = 2f, WiebeDuration = 60f, WiebeStart = 5f;
public float StoichiometricAFR = 14.7f;
public float FuelLowerHeatingValue = 44e6f;
public float EnergyVariationFraction = 0.05f;
public float MisfireProbability = 0.01f;
public float CylinderWallArea = 0.02f;
public float HeatTransferCoefficient = 100f;
public float AmbientTemperature = 300f;
// Ignition and combustion
public double SparkAdvance { get; set; } = 20.0;
public double WiebeA { get; set; } = 5.0;
public double WiebeM { get; set; } = 2.0;
public double WiebeDuration { get; set; } = 60.0;
public double WiebeStart { get; set; } = 5.0;
public float PhaseOffset; // rad
// Fuel
public double StoichiometricAFR { get; set; } = 14.7;
public double FuelLowerHeatingValue { get; set; } = 44e6;
public float Volume => cylinderVolume;
public float Pressure => (Gamma - 1f) * cylinderEnergy / MathF.Max(cylinderVolume, 1e-12f);
public float Temperature => Pressure / MathF.Max(Density * GasConstant, 1e-12f);
public float Density => Mass / MathF.Max(cylinderVolume, 1e-12f);
public float Mass => _airMass + _exhaustMass;
public float AirFraction => _airMass / MathF.Max(Mass, 1e-12f);
public float PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;
// Cycletocycle randomness
public double EnergyVariationFraction { get; set; } = 0.05;
public double MisfireProbability { get; set; } = 0.01;
// Heat loss
public double CylinderWallArea { get; set; } = 0.02;
public double HeatTransferCoefficient { get; set; } = 100.0;
public double AmbientTemperature { get; set; } = 300.0;
// ---- Multicylinder support ----
/// <summary>
/// Phase offset (radians) added to the crankshaft angle for this cylinder.
/// Used for multicylinder engines; set to 0 for singlecylinder.
/// </summary>
public double PhaseOffset { get; set; } = 0.0;
// State (public for drawing)
public double Volume => cylinderVolume;
public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double Density => Mass / Math.Max(cylinderVolume, 1e-12);
public double Mass => _airMass + _exhaustMass;
public double AirFraction => _airMass / Math.Max(Mass, 1e-12);
public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;
private double cylinderVolume;
private double cylinderEnergy;
private double _airMass;
private double _exhaustMass;
private double trappedAirMass;
private double fuelMass;
private double burnFraction;
private bool combustionActive;
private bool fuelInjected;
private double _energyFactor = 1.0;
private float cylinderVolume, cylinderEnergy;
private float _airMass, _exhaustMass;
private float trappedAirMass, fuelMass, burnFraction;
private bool combustionActive, fuelInjected;
private float _energyFactor = 1f;
private readonly Random _random = new Random();
private const double Gamma = 1.4;
private const double GasConstant = 287.0;
private const float Gamma = 1.4f;
private const float GasConstant = 287f;
private const float MaxPressurePa = 200e5f;
private const float MaxTemperatureK = 3500f;
private const double MaxPressurePa = 200e5;
private const double MaxTemperatureK = 3500.0;
public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
double ivo, double ivc, double evo, double evc, Crankshaft crankshaft)
public Cylinder(float bore, float stroke, float conRodLength, float compressionRatio,
float ivo, float ivc, float evo, float evc, Crankshaft crankshaft)
{
Bore = bore;
Stroke = stroke;
ConRodLength = conRodLength;
Bore = bore; Stroke = stroke; ConRodLength = conRodLength;
CompressionRatio = compressionRatio;
IVO = ivo;
IVC = ivc;
EVO = evo;
EVC = evc;
IVO = ivo; IVC = ivc; EVO = evo; EVC = evc;
Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft));
cylinderVolume = clearanceVolume;
double initRho = 1.225;
float initRho = 1.225f;
_airMass = initRho * clearanceVolume;
_exhaustMass = 0.0;
cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0);
_exhaustMass = 0f;
cylinderEnergy = 101325f * clearanceVolume / (Gamma - 1f);
IntakePort = new Port { Owner = this };
ExhaustPort = new Port { Owner = this };
_ports = new[] { IntakePort, ExhaustPort };
}
// Derived volumes
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
private double CrankRadius => Stroke / 2.0;
private double Obliquity => CrankRadius / ConRodLength;
private float SweptVolume => MathF.PI * 0.25f * Bore * Bore * Stroke;
private float clearanceVolume => SweptVolume / (CompressionRatio - 1f);
private float CrankRadius => Stroke * 0.5f;
private float Obliquity => CrankRadius / ConRodLength;
// Offset-aware crank angle in degrees
private double CrankDeg =>
((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
private float CrankDeg =>
((Crankshaft.CrankAngle + PhaseOffset) % (4f * MathF.PI)) * 180f / MathF.PI % 720f;
public double ComputeVolume(double thetaRad)
public float ComputeVolume(float thetaRad)
{
double r = CrankRadius;
double l = ConRodLength;
double cosTh = Math.Cos(thetaRad);
double sinTh = Math.Sin(thetaRad);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
double x = r * (1.0 - cosTh) + l * (1.0 - term);
double area = Math.PI * 0.25 * Bore * Bore;
float r = CrankRadius, l = ConRodLength;
float cosTh = MathF.Cos(thetaRad), sinTh = MathF.Sin(thetaRad);
float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh);
float x = r * (1f - cosTh) + l * (1f - term);
float area = MathF.PI * 0.25f * Bore * Bore;
return clearanceVolume + area * x;
}
private double ValveLift(double thetaDeg, double opens, double closes, double peakLift)
private float ValveLift(float thetaDeg, float opens, float closes, float peakLift)
{
double deg = thetaDeg % 720.0;
if (deg < 0) deg += 720.0;
float deg = thetaDeg % 720f;
if (deg < 0f) deg += 720f;
float duration = closes - opens;
if (duration <= 0f) return 0f;
double duration = closes - opens;
if (duration <= 0) return 0.0;
double rampDur = duration * 0.25;
double holdDur = duration - 2.0 * rampDur;
float rampDur = duration * 0.25f;
float holdDur = duration - 2f * rampDur;
if (deg >= opens && deg < opens + rampDur)
{
double t = (deg - opens) / rampDur;
return peakLift * t * t * (3.0 - 2.0 * t);
float t = (deg - opens) / rampDur;
return peakLift * t * t * (3f - 2f * t);
}
else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur)
{
@@ -158,54 +117,45 @@ namespace FluidSim.Components
}
else if (deg >= opens + rampDur + holdDur && deg <= closes)
{
double t = (deg - (opens + rampDur + holdDur)) / rampDur;
return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t);
float t = (deg - (opens + rampDur + holdDur)) / rampDur;
return peakLift * (1f - t) * (1f - t) * (1f + 2f * t);
}
return 0.0;
return 0f;
}
public double IntakeValveArea =>
Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
public float IntakeValveArea =>
MathF.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
public float ExhaustValveArea =>
MathF.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
public double ExhaustValveArea =>
Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
private double Wiebe(double angleSinceSpark)
private float Wiebe(float angleSinceSpark)
{
if (angleSinceSpark < WiebeStart) return 0.0;
double phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
if (phi <= 0) return 0.0;
return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1));
if (angleSinceSpark < WiebeStart) return 0f;
float phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
if (phi <= 0f) return 0f;
return 1f - MathF.Exp(-WiebeA * MathF.Pow(phi, WiebeM + 1f));
}
public void PreStep(double dt)
public void PreStep(float dt)
{
double prevVolume = cylinderVolume;
// ----- Use phaseoffset crank angle for this cylinder -----
double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
float prevVolume = cylinderVolume;
float crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
cylinderVolume = ComputeVolume(crankAngleRad);
double dV = cylinderVolume - prevVolume;
// Piston torque
double pRel = Pressure - 101325.0;
double sinTh = Math.Sin(crankAngleRad);
double cosTh = Math.Cos(crankAngleRad);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
double dxdtheta = CrankRadius * sinTh * (1.0 + Obliquity * cosTh / term);
double pistonArea = Math.PI * 0.25 * Bore * Bore;
double torque = pRel * pistonArea * dxdtheta;
Crankshaft.AddTorque(torque);
float dV = cylinderVolume - prevVolume;
float pRel = Pressure - 101325f;
float sinTh = MathF.Sin(crankAngleRad), cosTh = MathF.Cos(crankAngleRad);
float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh);
float dxdtheta = CrankRadius * sinTh * (1f + Obliquity * cosTh / term);
float pistonArea = MathF.PI * 0.25f * Bore * Bore;
Crankshaft.AddTorque(pRel * pistonArea * dxdtheta);
cylinderEnergy -= Pressure * dV;
// Also use offset angle for event detection
double crankshaftPrevAngle = Crankshaft.PreviousAngle;
double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0;
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
float prevDeg = (Crankshaft.PreviousAngle + PhaseOffset) * 180f / MathF.PI % 720f;
float currDeg = crankAngleRad * 180f / MathF.PI % 720f;
// ----- Intake closing: capture trapped air mass and compute fuel -----
// Intake closing
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
{
trappedAirMass = _airMass;
@@ -213,122 +163,103 @@ namespace FluidSim.Components
fuelInjected = true;
}
// ----- Spark ignition -----
double sparkAngle = 0.0 - SparkAdvance;
if (sparkAngle < 0) sparkAngle += 720.0;
// Spark
float sparkAngle = 0f - SparkAdvance;
if (sparkAngle < 0f) sparkAngle += 720f;
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
(prevDeg > sparkAngle + 360f && currDeg < sparkAngle);
if (crossedSpark && !combustionActive && fuelInjected)
{
bool misfire = _random.NextDouble() < MisfireProbability;
if (misfire)
if (_random.NextDouble() < MisfireProbability)
{
combustionActive = false;
}
else
{
combustionActive = true;
burnFraction = 0.0;
double range = EnergyVariationFraction;
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
combustionActive = true; burnFraction = 0f;
float range = EnergyVariationFraction;
_energyFactor = 1f + range * (2f * (float)_random.NextDouble() - 1f);
}
}
// ----- Combustion progress -----
// Combustion
if (combustionActive)
{
double angleSinceSpark = currDeg - sparkAngle;
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
double newFraction = Wiebe(angleSinceSpark);
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
float angleSinceSpark = currDeg - sparkAngle;
if (angleSinceSpark < 0f) angleSinceSpark += 720f;
float newFraction = Wiebe(angleSinceSpark);
if (newFraction >= 1f || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
{
newFraction = 1.0;
combustionActive = false;
double totalMass = _airMass + _exhaustMass;
_airMass = 0.0;
_exhaustMass = totalMass;
newFraction = 1f; combustionActive = false;
float totalMass = _airMass + _exhaustMass;
_airMass = 0f; _exhaustMass = totalMass;
}
double dFraction = newFraction - burnFraction;
if (dFraction > 0)
float dFraction = newFraction - burnFraction;
if (dFraction > 0f)
{
double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
float dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
cylinderEnergy += dQ;
_exhaustMass += fuelMass * dFraction;
burnFraction = newFraction;
}
}
// ----- Heat loss -----
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
(Temperature - AmbientTemperature) * dt;
// Heat loss
float dQ_loss = HeatTransferCoefficient * CylinderWallArea *
(Temperature - AmbientTemperature) * dt;
cylinderEnergy -= dQ_loss;
// Update port states
double p = Pressure, rho = Density, T = Temperature;
double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12);
double af = AirFraction;
IntakePort.Pressure = p;
IntakePort.Density = rho;
IntakePort.Temperature = T;
IntakePort.SpecificEnthalpy = h;
IntakePort.AirFraction = af;
ExhaustPort.Pressure = p;
ExhaustPort.Density = rho;
ExhaustPort.Temperature = T;
ExhaustPort.SpecificEnthalpy = h;
ExhaustPort.AirFraction = af;
float p = Pressure, rho = Density, T = Temperature;
float h = Gamma / (Gamma - 1f) * p / MathF.Max(rho, 1e-12f);
float af = AirFraction;
IntakePort.Pressure = p; IntakePort.Density = rho;
IntakePort.Temperature = T; IntakePort.SpecificEnthalpy = h; IntakePort.AirFraction = af;
ExhaustPort.Pressure = p; ExhaustPort.Density = rho;
ExhaustPort.Temperature = T; ExhaustPort.SpecificEnthalpy = h; ExhaustPort.AirFraction = af;
}
public void UpdateState(double dt)
public void UpdateState(float dt)
{
double dmAir = 0.0, dmExhaust = 0.0, dE = 0.0;
float dmAir = 0f, dmExhaust = 0f, dE = 0f;
foreach (var port in _ports)
{
double mdot = port.MassFlowRate;
double af = mdot >= 0 ? port.AirFraction : AirFraction;
float mdot = port.MassFlowRate;
float af = mdot >= 0f ? port.AirFraction : AirFraction;
dmAir += mdot * af * dt;
dmExhaust += mdot * (1.0 - af) * dt;
dmExhaust += mdot * (1f - af) * dt;
dE += mdot * port.SpecificEnthalpy * dt;
}
_airMass += dmAir;
_exhaustMass += dmExhaust;
_airMass += dmAir; _exhaustMass += dmExhaust;
cylinderEnergy += dE;
double V = Math.Max(cylinderVolume, 1e-12);
float V = MathF.Max(cylinderVolume, 1e-12f);
float currentP = (Gamma - 1f) * cylinderEnergy / V;
if (currentP > MaxPressurePa) cylinderEnergy = MaxPressurePa * V / (Gamma - 1f);
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
if (currentP > MaxPressurePa)
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
double currentRho = (_airMass + _exhaustMass) / V;
double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
float currentRho = (_airMass + _exhaustMass) / V;
float currentT = currentP / MathF.Max(currentRho * GasConstant, 1e-12f);
if (currentT > MaxTemperatureK)
{
double pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
cylinderEnergy = pAtTlimit * V / (Gamma - 1.0);
float pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
cylinderEnergy = pAtTlimit * V / (Gamma - 1f);
}
double totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9)
float totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9f)
{
_airMass = 1e-9;
_exhaustMass = 0.0;
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
_airMass = 1e-9f; _exhaustMass = 0f;
cylinderEnergy = 101325f * V / (Gamma - 1f);
}
else if (cylinderEnergy < 0.0)
else if (cylinderEnergy < 0f)
{
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
cylinderEnergy = 101325f * V / (Gamma - 1f);
}
if (_airMass < 0.0) _airMass = 0.0;
if (_exhaustMass < 0.0) _exhaustMass = 0.0;
if (_airMass < 0f) _airMass = 0f;
if (_exhaustMass < 0f) _exhaustMass = 0f;
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
namespace FluidSim.Components
{
public static class NozzleFlow
{
/// <summary>
/// Computes the nozzleexit primitive state and mass flow rate from a
/// volume to a pipe, using isentropic relations. Follows ensim4's flow() logic.
/// </summary>
public static void Compute(double Pt_high, double Tt_high,
double P_low, double gamma, double R, double area,
out double rhoExit, out double uExit,
out double pExit, out double mdot)
{
double gm1 = gamma - 1.0;
double Pt_over_Ps = Pt_high / P_low;
// Mach number (subsonic, clamped to 1)
double M = Math.Sqrt(Math.Max(0.0,
(2.0 / gm1) * (Math.Pow(Pt_over_Ps, gm1 / gamma) - 1.0)));
if (M > 1.0) M = 1.0;
double T_star = Tt_high / (1.0 + 0.5 * gm1 * M * M);
double a_star = Math.Sqrt(gamma * R * T_star);
double u_star = M * a_star;
pExit = Pt_high * Math.Pow(1.0 + 0.5 * gm1 * M * M, -gamma / gm1);
rhoExit = pExit / (R * T_star);
uExit = u_star; // positive away from highpressure side
mdot = rhoExit * uExit * area;
}
/// <summary>
/// Ambient cell for nonreflecting open end (ensim4 calc_ambient_cell).
/// </summary>
public static void ComputeAmbientCell(double rhoInt, double uInt, double pInt,
double pAmbient, double gamma,
out double rhoAmb, out double uAmb,
out double pAmb)
{
pAmb = pAmbient;
uAmb = uInt;
rhoAmb = rhoInt * Math.Pow(pAmb / pInt, 1.0 / gamma);
}
}
}

View File

@@ -1,451 +0,0 @@
using System;
using System.Diagnostics;
using FluidSim.Interfaces;
namespace FluidSim.Components
{
/// <summary>
/// 1D compressible Euler pipe with LaxFriedrichs finitevolume scheme.
/// Ghost states are set externally via SetGhostLeft/Right; they are always required.
/// Now includes a passive scalar for air mass fraction.
/// </summary>
public class Pipe1D : IComponent
{
public const bool EnableDetailedProfiling = false; // set to false in release builds
public Port PortA { get; }
public Port PortB { get; }
public double Area { get; }
public double DampingMultiplier { get; set; } = 10.0;
public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s
public string Name = "Pipe";
private double _ambientPressure = 101325.0;
public double AmbientPressure
{
get => _ambientPressure;
set
{
_ambientPressure = value;
_ambientEnergyReference = value / (_gamma - 1.0);
}
}
private readonly int _n;
private readonly double _dx;
private readonly double _diameter;
private readonly double _gamma = 1.4;
private double[] _rho, _rhou, _E;
private double[] _Y; // air mass fraction
private double[] _fluxM, _fluxP, _fluxE;
private double _rhoGhostL, _uGhostL, _pGhostL, _YGhostL;
private double _rhoGhostR, _uGhostR, _pGhostR, _YGhostR;
private bool _ghostLValid, _ghostRValid;
private double _laminarCoeff;
private double _ambientEnergyReference;
// ---------- Profiling accumulators ----------
private long _profPrecomputeTicks;
private long _profLeftFluxTicks;
private long _profInteriorLoopTicks;
private long _profRightFluxTicks;
private long _profPortUpdateTicks;
private long _profCallCount;
public Pipe1D(double length, double area, int cellCount)
{
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
_n = cellCount;
_dx = length / _n;
Area = area;
_diameter = 2.0 * Math.Sqrt(area / Math.PI);
_rho = new double[_n];
_rhou = new double[_n];
_E = new double[_n];
_Y = new double[_n];
_fluxM = new double[_n + 1];
_fluxP = new double[_n + 1];
_fluxE = new double[_n + 1];
double mu_air = 1.8e-5;
double radius = _diameter * 0.5;
_laminarCoeff = 8.0 * mu_air / (radius * radius);
_ambientEnergyReference = 101325.0 / (_gamma - 1.0);
PortA = new Port { Owner = this };
PortB = new Port { Owner = this };
SetUniformState(1.225, 0.0, 101325.0);
}
IReadOnlyList<Port> IComponent.Ports => new[] { PortA, PortB };
public void UpdateState(double dt) { }
// ---------- Ghost interface ----------
public void SetGhostLeft(double rho, double u, double p, double airFraction)
{
_rhoGhostL = rho; _uGhostL = u; _pGhostL = p; _YGhostL = airFraction; _ghostLValid = true;
}
public void SetGhostRight(double rho, double u, double p, double airFraction)
{
_rhoGhostR = rho; _uGhostR = u; _pGhostR = p; _YGhostR = airFraction; _ghostRValid = true;
}
public void ClearGhostFlags() { _ghostLValid = false; _ghostRValid = false; }
public double GetInteriorAirFractionLeft() => _Y[0];
public double GetInteriorAirFractionRight() => _Y[_n - 1];
public (double rho, double u, double p) GetInteriorStateLeft()
{
double rho = Math.Max(_rho[0], 1e-12);
double u = _rhou[0] / rho;
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);
double u = _rhou[_n - 1] / rho;
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) => _rhou[i] / Math.Max(_rho[i], 1e-12);
public double GetCellPressure(int i) => PressureScalar(i);
public int GetRequiredSubSteps(double dtGlobal, double cflTarget = 0.8)
{
double maxW = 0.0;
for (int i = 0; i < _n; i++)
{
double rho = Math.Max(_rho[i], 1e-12);
double u = Math.Abs(_rhou[i] / rho);
double p = PressureScalar(i);
double c = Math.Sqrt(_gamma * p / rho);
double local = u + c;
if (local > maxW) maxW = local;
}
maxW = Math.Max(maxW, 1e-8);
return Math.Max(1, (int)Math.Ceiling(dtGlobal * maxW / (cflTarget * _dx)));
}
// ---------- Main step (per substep) ----------
public void SimulateSingleStep(double dtSub)
{
if (!_ghostLValid || !_ghostRValid)
throw new InvalidOperationException("Ghost cells not set before SimulateSingleStep.");
double dt = dtSub;
int n = _n;
double dt_dx = dt / _dx;
double coeff = _laminarCoeff * DampingMultiplier;
double relaxRate = EnergyRelaxationRate;
double gamma = _gamma;
double gm1 = gamma - 1.0;
// ---------- Profiling start ----------
long t0 = 0, t1 = 0;
if (EnableDetailedProfiling)
{
t0 = Stopwatch.GetTimestamp();
_profCallCount++;
}
// ---------- Phase 1: Precompute pressure and speed of sound ----------
double[] p = new double[n];
double[] c = new double[n];
for (int i = 0; i < n; i++)
{
double rho = Math.Max(_rho[i], 1e-12);
double u = _rhou[i] / rho;
p[i] = gm1 * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho);
c[i] = Math.Sqrt(gamma * p[i] / rho);
}
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profPrecomputeTicks += (t1 - t0);
t0 = t1;
}
// ---------- Local flux functions ----------
void LaxFlux(double rL, double uL, double pL, double cL,
double rR, double uR, double pR, double cR,
out double fm, out double fp, out double fe)
{
double EL = pL / (gm1 * rL) + 0.5 * uL * uL;
double ER = pR / (gm1 * rR) + 0.5 * uR * uR;
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;
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rR - rL);
fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rR * uR - rL * uL);
fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (rR * ER - rL * EL);
}
void ScalarFlux(double rL, double uL, double cL, double YL,
double rR, double uR, double cR, double YR,
double alpha, out double fy)
{
double Fm_L = rL * uL;
double Fm_R = rR * uR;
fy = 0.5 * (Fm_L * YL + Fm_R * YR) - 0.5 * alpha * (rR * YR - rL * YL);
}
// ---------- Phase 2: Left face flux (ghostL cell 0) ----------
double rL_ghost = Math.Max(_rhoGhostL, 1e-12);
double pL_ghost = _pGhostL;
double uL_ghost = _uGhostL;
double cL_ghost = Math.Sqrt(gamma * pL_ghost / rL_ghost);
LaxFlux(rL_ghost, uL_ghost, pL_ghost, cL_ghost,
_rho[0], _rhou[0] / Math.Max(_rho[0], 1e-12), p[0], c[0],
out double fluxM_left, out double fluxP_left, out double fluxE_left);
double alphaLeft = Math.Max(Math.Abs(uL_ghost) + cL_ghost,
Math.Abs(_rhou[0] / Math.Max(_rho[0], 1e-12)) + c[0]);
ScalarFlux(rL_ghost, uL_ghost, cL_ghost, _YGhostL,
_rho[0], _rhou[0] / Math.Max(_rho[0], 1e-12), c[0], _Y[0],
alphaLeft, out double fluxY_left);
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profLeftFluxTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 3: Interior loop (fluxes + cell updates) ----------
double fluxM_prev = fluxM_left;
double fluxP_prev = fluxP_left;
double fluxE_prev = fluxE_left;
double fluxY_prev = fluxY_left;
for (int i = 0; i < n - 1; i++)
{
int iL = i;
int iR = i + 1;
double rL = Math.Max(_rho[iL], 1e-12);
double uL = _rhou[iL] / rL;
double pL = p[iL];
double cL = c[iL];
double YL = _Y[iL];
double rR = Math.Max(_rho[iR], 1e-12);
double uR = _rhou[iR] / rR;
double pR = p[iR];
double cR = c[iR];
double YR = _Y[iR];
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR,
out double fluxM_right, out double fluxP_right, out double fluxE_right);
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
ScalarFlux(rL, uL, cL, YL, rR, uR, cR, YR, alpha, out double fluxY_right);
// Update cell i
double r = _rho[i];
double ru = _rhou[i];
double E = _E[i];
double Y = _Y[i];
double newR = r - dt_dx * (fluxM_right - fluxM_prev);
double newRu = ru - dt_dx * (fluxP_right - fluxP_prev);
double newE = E - dt_dx * (fluxE_right - fluxE_prev);
double oldRhoY = r * Y;
double newRhoY = oldRhoY - dt_dx * (fluxY_right - fluxY_prev);
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
newRu *= dampingFactor;
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
newR = Math.Max(newR, 1e-12);
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
double eMin = 100.0 / gm1 + kin;
newE = Math.Max(newE, eMin);
_rho[i] = newR;
_rhou[i] = newRu;
_E[i] = newE;
_Y[i] = Math.Clamp(newRhoY / newR, 0.0, 1.0);
fluxM_prev = fluxM_right;
fluxP_prev = fluxP_right;
fluxE_prev = fluxE_right;
fluxY_prev = fluxY_right;
}
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profInteriorLoopTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 4: Right face flux (cell n1 ghostR) ----------
double rR_ghost = Math.Max(_rhoGhostR, 1e-12);
double pR_ghost = _pGhostR;
double uR_ghost = _uGhostR;
double cR_ghost = Math.Sqrt(gamma * pR_ghost / rR_ghost);
double rInt = _rho[n - 1];
double uInt = _rhou[n - 1] / Math.Max(rInt, 1e-12);
LaxFlux(rInt, uInt, p[n - 1], c[n - 1],
rR_ghost, uR_ghost, pR_ghost, cR_ghost,
out double fluxM_right_final, out double fluxP_right_final, out double fluxE_right_final);
double alphaRight = Math.Max(Math.Abs(uInt) + c[n - 1], Math.Abs(uR_ghost) + cR_ghost);
ScalarFlux(rInt, uInt, c[n - 1], _Y[n - 1],
rR_ghost, uR_ghost, cR_ghost, _YGhostR,
alphaRight, out double fluxY_right_final);
// Update last cell
{
int i = n - 1;
double r = _rho[i];
double ru = _rhou[i];
double E = _E[i];
double Y = _Y[i];
double newR = r - dt_dx * (fluxM_right_final - fluxM_prev);
double newRu = ru - dt_dx * (fluxP_right_final - fluxP_prev);
double newE = E - dt_dx * (fluxE_right_final - fluxE_prev);
double oldRhoY = r * Y;
double newRhoY = oldRhoY - dt_dx * (fluxY_right_final - fluxY_prev);
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
newRu *= dampingFactor;
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
newR = Math.Max(newR, 1e-12);
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
double eMin = 100.0 / gm1 + kin;
newE = Math.Max(newE, eMin);
_rho[i] = newR;
_rhou[i] = newRu;
_E[i] = newE;
_Y[i] = Math.Clamp(newRhoY / newR, 0.0, 1.0);
}
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profRightFluxTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 5: Update port states ----------
(double rhoA, double uA, double pA) = GetInteriorStateLeft();
PortA.Pressure = pA; PortA.Density = rhoA;
PortA.Temperature = pA / (rhoA * 287.0);
PortA.SpecificEnthalpy = gm1 / (gamma - 1.0) * pA / rhoA;
PortA.AirFraction = _Y[0];
(double rhoB, double uB, double pB) = GetInteriorStateRight();
PortB.Pressure = pB; PortB.Density = rhoB;
PortB.Temperature = pB / (rhoB * 287.0);
PortB.SpecificEnthalpy = gm1 / (gamma - 1.0) * pB / rhoB;
PortB.AirFraction = _Y[_n - 1];
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profPortUpdateTicks += (t1 - t0);
}
}
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);
}
public void SetUniformState(double rho, double u, double p)
{
double e = p / ((_gamma - 1.0) * rho);
double E = rho * e + 0.5 * rho * u * u;
for (int i = 0; i < _n; i++)
{
_rho[i] = rho;
_rhou[i] = rho * u;
_E[i] = E;
_Y[i] = 1.0; // initially pure air
}
}
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;
_Y[i] = 1.0;
}
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;
}
// ---------- Public profiling interface ----------
public void ResetDetailCounters()
{
_profPrecomputeTicks = 0;
_profLeftFluxTicks = 0;
_profInteriorLoopTicks = 0;
_profRightFluxTicks = 0;
_profPortUpdateTicks = 0;
_profCallCount = 0;
}
public string GetDetailProfileReport()
{
if (!EnableDetailedProfiling)
return "Detailed profiling disabled.";
double freq = Stopwatch.Frequency;
long totalTicks = _profPrecomputeTicks + _profLeftFluxTicks +
_profInteriorLoopTicks + _profRightFluxTicks +
_profPortUpdateTicks;
if (totalTicks == 0) return "No profiling data.";
double totalSec = totalTicks / freq;
double avgCallSec = totalSec / _profCallCount;
double avgCallUs = avgCallSec * 1e6;
string report = $" Pipe detailed (over {_profCallCount} calls, total {totalSec * 1000:F2} ms):\n";
report += $" Avg per call: {avgCallUs:F2} µs\n";
report += $" Precompute p,c: {_profPrecomputeTicks * 100.0 / totalTicks:F1} % ({_profPrecomputeTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Left face flux: {_profLeftFluxTicks * 100.0 / totalTicks:F1} % ({_profLeftFluxTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Interior loop: {_profInteriorLoopTicks * 100.0 / totalTicks:F1} % ({_profInteriorLoopTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Right face flux: {_profRightFluxTicks * 100.0 / totalTicks:F1} % ({_profRightFluxTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Port update: {_profPortUpdateTicks * 100.0 / totalTicks:F1} % ({_profPortUpdateTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
return report;
}
}
}

View File

@@ -8,36 +8,40 @@ namespace FluidSim.Components
{
public List<Port> Ports { get; } = new List<Port>();
private double _airMass;
private double _exhaustMass;
public double InternalEnergy { get; 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;
private float _airMass;
private float _exhaustMass;
public float InternalEnergy;
public float Volume;
public float Dvdt;
public float Gamma { get; set; } = 1.4f;
public float GasConstant { get; set; } = 287f;
public float AmbientPressure { get; set; } = 101325f;
public double AmbientPressure { get; set; } = 101325.0;
// ---------- Thermal relaxation to environment ----------
/// <summary>Rate of heat transfer to the surroundings (1/s). 0 = adiabatic.</summary>
public float EnergyRelaxationRate { get; set; } = 10f;
/// <summary>Temperature to relax toward (K). Default is room temperature.</summary>
public float AmbientTemperature { get; set; } = 300f;
// Derived quantities
public double Mass => _airMass + _exhaustMass;
public double AirFraction => _airMass / Math.Max(Mass, 1e-12);
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 float Mass => _airMass + _exhaustMass;
public float AirFraction => _airMass / MathF.Max(Mass, 1e-12f);
public float Density => Mass / MathF.Max(Volume, 1e-12f);
public float Pressure => (Gamma - 1f) * InternalEnergy / MathF.Max(Volume, 1e-12f);
public float Temperature => Pressure / MathF.Max(Density * GasConstant, 1e-12f);
public float SpecificEnthalpy => Gamma / (Gamma - 1f) * Pressure / MathF.Max(Density, 1e-12f);
public Volume0D(double initialVolume, double initialPressure,
double initialTemperature, double gasConstant = 287.0, double gamma = 1.4)
public Volume0D(float initialVolume, float initialPressure,
float initialTemperature, float gasConstant = 287f, float gamma = 1.4f)
{
GasConstant = gasConstant;
Gamma = gamma;
Volume = initialVolume;
Dvdt = 0.0;
Dvdt = 0f;
double rho0 = initialPressure / (GasConstant * initialTemperature);
_airMass = rho0 * Volume; // starts with all air
_exhaustMass = 0.0;
InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0);
float rho0 = initialPressure / (GasConstant * initialTemperature);
_airMass = rho0 * Volume;
_exhaustMass = 0f;
InternalEnergy = (initialPressure * Volume) / (Gamma - 1f);
}
public Port CreatePort()
@@ -52,66 +56,75 @@ namespace FluidSim.Components
return port;
}
public void SetPressure(double pressure, double? temperature = null)
public void SetPressure(float pressure, float? temperature = null)
{
double V = Math.Max(Volume, 1e-12);
double T = temperature ?? Temperature;
double rho = pressure / (GasConstant * T);
double totalMass = rho * V;
// Keep current air fraction when setting pressure?
double af = AirFraction;
float V = MathF.Max(Volume, 1e-12f);
float T = temperature ?? Temperature;
float rho = pressure / (GasConstant * T);
float totalMass = rho * V;
float af = AirFraction;
_airMass = totalMass * af;
_exhaustMass = totalMass * (1.0 - af);
InternalEnergy = pressure * V / (Gamma - 1.0);
_exhaustMass = totalMass * (1f - af);
InternalEnergy = pressure * V / (Gamma - 1f);
}
public void UpdateState(double dt)
public void UpdateState(float dt)
{
double totalMdotAir = 0.0;
double totalMdotExhaust = 0.0;
double totalEdot = 0.0;
float totalMdotAir = 0f, totalMdotExhaust = 0f, totalEdot = 0f;
foreach (var port in Ports)
{
double mdot = port.MassFlowRate; // positive INTO volume
double af = mdot >= 0 ? port.AirFraction : AirFraction; // inflow: use port's fraction; outflow: well-mixed
float mdot = port.MassFlowRate;
float af = mdot >= 0f ? port.AirFraction : AirFraction;
totalMdotAir += mdot * af;
totalMdotExhaust += mdot * (1.0 - af);
totalMdotExhaust += mdot * (1f - af);
totalEdot += mdot * port.SpecificEnthalpy;
}
double dAir = totalMdotAir * dt;
double dExhaust = totalMdotExhaust * dt;
double dE = totalEdot * dt - Pressure * Dvdt * dt;
float dAir = totalMdotAir * dt;
float dExhaust = totalMdotExhaust * dt;
float dE = totalEdot * dt - Pressure * Dvdt * dt;
_airMass += dAir;
_exhaustMass += dExhaust;
InternalEnergy += dE;
double V = Math.Max(Volume, 1e-12);
double totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9)
// ---- Thermal relaxation ----
if (EnergyRelaxationRate > 0f)
{
_airMass = 1e-9;
_exhaustMass = 0.0;
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
}
else if (InternalEnergy < 0.0)
{
InternalEnergy = AmbientPressure * V / (Gamma - 1.0);
float currentMass = Mass;
if (currentMass > 1e-12f)
{
// Target internal energy: current mass at ambient temperature
float targetE = currentMass * GasConstant * AmbientTemperature / (Gamma - 1f);
float relaxFactor = MathF.Exp(-EnergyRelaxationRate * dt);
InternalEnergy = targetE + (InternalEnergy - targetE) * relaxFactor;
}
}
if (_airMass < 0.0) _airMass = 0.0;
if (_exhaustMass < 0.0) _exhaustMass = 0.0;
float V = MathF.Max(Volume, 1e-12f);
float totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9f)
{
_airMass = 1e-9f;
_exhaustMass = 0f;
InternalEnergy = AmbientPressure * V / (Gamma - 1f);
}
else if (InternalEnergy < 0f)
{
InternalEnergy = AmbientPressure * V / (Gamma - 1f);
}
double p = Pressure, rho = Density, T = Temperature, h = SpecificEnthalpy, afrac = AirFraction;
if (_airMass < 0f) _airMass = 0f;
if (_exhaustMass < 0f) _exhaustMass = 0f;
float p = Pressure, rho = Density, T = Temperature, h = SpecificEnthalpy, afr = AirFraction;
foreach (var port in Ports)
{
port.Pressure = p;
port.Density = rho;
port.Temperature = T;
port.SpecificEnthalpy = h;
port.AirFraction = afrac;
port.AirFraction = afr;
}
}

330
Core/BoundarySystem.cs Normal file
View File

@@ -0,0 +1,330 @@
using FluidSim.Components;
using FluidSim.Interfaces;
using System;
namespace FluidSim.Core
{
public class BoundarySystem
{
public struct OrificeDesc
{
public Port VolumePort;
public int PipeIndex;
public bool IsLeftEnd;
public int AreaIndex;
public float DischargeCoeff;
// --- Inertance support ---
public bool UseInertance;
public float EffectiveLength;
public float CurrentMdot; // kg/s, positive = volume → pipe
// --- Dissipative loss ---
public float LossCoefficient; // K factor for pressure drop = K * 0.5*rho*u^2
}
public struct OpenEndDesc
{
public int PipeIndex;
public bool IsLeftEnd;
public float AmbientPressure;
public float Gamma;
public float PipeArea;
public float LastMassFlowRate;
public float LastFacePressure;
}
private OrificeDesc[] _orifices;
private OpenEndDesc[] _openEnds;
private float[] _orificeAreas;
private PipeSystem _pipeSystem;
public BoundarySystem(PipeSystem pipeSystem, int maxOrifices, int maxOpenEnds)
{
_pipeSystem = pipeSystem;
_orifices = new OrificeDesc[maxOrifices];
_openEnds = new OpenEndDesc[maxOpenEnds];
_orificeAreas = new float[maxOrifices];
}
public int OrificeCount { get; private set; }
public int OpenEndCount { get; private set; }
public void AddOrifice(Port volumePort, int pipeIndex, bool isLeftEnd,
int areaIndex, float dischargeCoeff = 1f,
float lossCoefficient = 0f)
{
_orifices[OrificeCount] = new OrificeDesc
{
VolumePort = volumePort,
PipeIndex = pipeIndex,
IsLeftEnd = isLeftEnd,
AreaIndex = areaIndex,
DischargeCoeff = dischargeCoeff,
UseInertance = false,
EffectiveLength = 0f,
CurrentMdot = 0f,
LossCoefficient = lossCoefficient
};
OrificeCount++;
}
public void AddOrificeWithInertance(Port volumePort, int pipeIndex, bool isLeftEnd,
int areaIndex, float dischargeCoeff,
float effectiveLength, float lossCoefficient = 0f)
{
AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff, lossCoefficient);
ref var d = ref _orifices[OrificeCount - 1];
d.UseInertance = true;
d.EffectiveLength = effectiveLength;
}
public void AddOpenEnd(int pipeIndex, bool isLeftEnd,
float ambientPressure, float pipeArea, float gamma = 1.4f)
{
int idx = OpenEndCount;
_openEnds[idx] = new OpenEndDesc
{
PipeIndex = pipeIndex,
IsLeftEnd = isLeftEnd,
AmbientPressure = ambientPressure,
Gamma = gamma,
PipeArea = pipeArea
};
OpenEndCount++;
}
public void SetOrificeAreas(float[] areas)
{
for (int i = 0; i < OrificeCount; i++)
_orificeAreas[i] = areas[i];
}
public float GetOpenEndMassFlow(int openEndIndex)
{
if (openEndIndex < 0 || openEndIndex >= OpenEndCount) return 0f;
return _openEnds[openEndIndex].LastMassFlowRate;
}
public float GetOpenEndPressure(int openEndIndex)
{
if (openEndIndex < 0 || openEndIndex >= OpenEndCount) return 101325f;
return _openEnds[openEndIndex].LastFacePressure;
}
public void ResolveOrifices(float dt)
{
for (int i = 0; i < OrificeCount; i++)
{
ref var d = ref _orifices[i];
float area = _orificeAreas[d.AreaIndex];
if (area < 1e-12f || d.VolumePort == null)
{
// Closed wall reflect interior state
var (rInt, uInt, pInt) = d.IsLeftEnd
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
float afInt = d.IsLeftEnd
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
if (d.IsLeftEnd)
_pipeSystem.SetGhostLeft(d.PipeIndex, rInt, -uInt, pInt, afInt);
else
_pipeSystem.SetGhostRight(d.PipeIndex, rInt, -uInt, pInt, afInt);
if (d.VolumePort != null) d.VolumePort.MassFlowRate = 0f;
continue;
}
// Gather states
float volP = d.VolumePort.Pressure;
float volRho = d.VolumePort.Density;
float volT = d.VolumePort.Temperature;
float volH = d.VolumePort.SpecificEnthalpy;
float volAF = d.VolumePort.AirFraction;
var (pipeRho, pipeU, pipeP) = d.IsLeftEnd
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
float pipeT = pipeP / MathF.Max(pipeRho * 287f, 1e-12f);
float pipeAF = d.IsLeftEnd
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
float gamma = 1.4f, R = 287f, Cd = d.DischargeCoeff;
// --- Preliminary nozzle solution (no loss) to estimate flow direction and velocity ---
float mdotEst, rhoFaceEst, uFaceEst, pFaceEst;
if (volP >= pipeP)
{
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, Cd,
out mdotEst, out rhoFaceEst, out uFaceEst, out pFaceEst);
}
else
{
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, Cd,
out mdotEst, out rhoFaceEst, out uFaceEst, out pFaceEst);
mdotEst = -mdotEst;
}
// --- Apply symmetric loss if LossCoefficient > 0 ---
float volP_eff = volP;
float pipeP_eff = pipeP;
if (d.LossCoefficient > 0f && MathF.Abs(mdotEst) > 1e-12f)
{
float rhoRef = mdotEst >= 0 ? rhoFaceEst : rhoFaceEst; // rhoFaceEst already reflects the correct side
float uRef = uFaceEst;
float dynP = 0.5f * rhoRef * uRef * uRef * d.LossCoefficient;
// Clamp the loss to avoid overshoot (max 80% of pressure difference)
float dp = MathF.Abs(volP - pipeP);
dynP = MathF.Min(dynP, 0.8f * dp);
// Apply symmetrically: loss reduces the higher pressure and increases the lower pressure
if (mdotEst >= 0) // volume → pipe
{
volP_eff -= dynP;
pipeP_eff += dynP;
}
else // pipe → volume
{
pipeP_eff -= dynP;
volP_eff += dynP;
}
}
// --- Final nozzle solution with corrected pressures ---
float mdotSS, rhoFace0, uFace0, pFace0;
if (volP_eff >= pipeP_eff)
{
IsentropicOrifice.Compute(volP_eff, volRho, volT, pipeP_eff, gamma, R, area, Cd,
out float mUp, out rhoFace0, out uFace0, out pFace0);
mdotSS = mUp;
}
else
{
IsentropicOrifice.Compute(pipeP_eff, pipeRho, pipeT, volP_eff, gamma, R, area, Cd,
out float mUp, out rhoFace0, out uFace0, out pFace0);
mdotSS = -mUp;
}
float mdot;
if (d.UseInertance)
{
float rhoUp = d.CurrentMdot >= 0 ? volRho : pipeRho;
float inertance = rhoUp * d.EffectiveLength / area;
float dp = volP_eff - pipeP_eff;
float resistance = MathF.Abs(dp) / MathF.Max(MathF.Abs(mdotSS), 1e-12f);
float dmdot_dt = (dp - resistance * d.CurrentMdot) / inertance;
mdot = d.CurrentMdot + dmdot_dt * dt;
if (d.VolumePort.Owner is Volume0D vol0)
{
float maxOut = vol0.Mass / dt;
if (mdot > maxOut) mdot = maxOut;
}
if (float.IsNaN(mdot)) mdot = 0f;
}
else
{
mdot = mdotSS;
if (d.VolumePort.Owner is Volume0D vol0)
{
float maxOut = vol0.Mass / dt;
if (mdot > maxOut) mdot = maxOut;
}
}
d.CurrentMdot = mdot; // stored for future steps (inertance or loss)
// Ghost state construction
float rhoFace = mdot >= 0 ? volRho : pipeRho;
float pFace = pFace0;
float uFace = MathF.Abs(mdot) / MathF.Max(rhoFace * area, 1e-12f);
float airFracGhost;
if (mdot >= 0)
airFracGhost = volAF;
else
{
airFracGhost = pipeAF;
d.VolumePort.AirFraction = pipeAF;
}
if (mdot >= 0 && d.IsLeftEnd) uFace = +uFace;
else if (mdot >= 0 && !d.IsLeftEnd) uFace = -uFace;
else if (mdot < 0 && d.IsLeftEnd) uFace = -uFace;
else if (mdot < 0 && !d.IsLeftEnd) uFace = +uFace;
if (d.IsLeftEnd)
_pipeSystem.SetGhostLeft(d.PipeIndex, rhoFace, uFace, pFace, airFracGhost);
else
_pipeSystem.SetGhostRight(d.PipeIndex, rhoFace, uFace, pFace, airFracGhost);
d.VolumePort.MassFlowRate = -mdot;
if (-mdot >= 0)
{
float pipeH = gamma / (gamma - 1f) * pipeP / MathF.Max(pipeRho, 1e-12f);
d.VolumePort.SpecificEnthalpy = pipeH;
}
else
{
d.VolumePort.SpecificEnthalpy = volH;
}
}
}
public void ResolveOpenEnds(float dt)
{
for (int i = 0; i < OpenEndCount; i++)
{
ref var d = ref _openEnds[i];
var (rhoInt, uInt, pInt) = d.IsLeftEnd
? _pipeSystem.GetInteriorStateLeft(d.PipeIndex)
: _pipeSystem.GetInteriorStateRight(d.PipeIndex);
float afInt = d.IsLeftEnd
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
float gamma = d.Gamma;
float gm1 = gamma - 1f;
float cInt = MathF.Sqrt(gamma * pInt / MathF.Max(rhoInt, 1e-12f));
float pAmb = d.AmbientPressure;
float Jplus = uInt + 2f * cInt / gm1;
float Jminus = uInt - 2f * cInt / gm1;
float s = pInt / MathF.Pow(rhoInt, gamma);
float rhoIso = MathF.Pow(pAmb / s, 1f / gamma);
float cIso = MathF.Sqrt(gamma * pAmb / MathF.Max(rhoIso, 1e-12f));
float uIso = d.IsLeftEnd
? (Jminus + 2f * cIso / gm1)
: (Jplus - 2f * cIso / gm1);
bool supersonic = d.IsLeftEnd ? (uInt <= -cInt) : (uInt >= cInt);
float rhoGhost, uGhost, pGhost, afGhost;
if (supersonic)
{
rhoGhost = rhoInt; uGhost = uInt; pGhost = pInt; afGhost = afInt;
}
else
{
rhoGhost = rhoIso; uGhost = uIso; pGhost = pAmb;
bool inflow = d.IsLeftEnd ? (uIso >= 0f) : (uIso <= 0f);
afGhost = inflow ? 1f : afInt;
}
if (d.IsLeftEnd)
_pipeSystem.SetGhostLeft(d.PipeIndex, rhoGhost, uGhost, pGhost, afGhost);
else
_pipeSystem.SetGhostRight(d.PipeIndex, rhoGhost, uGhost, pGhost, afGhost);
float area = d.PipeArea;
float mdot = rhoGhost * uGhost * area;
if (d.IsLeftEnd) mdot = -mdot;
d.LastMassFlowRate = mdot;
d.LastFacePressure = pGhost;
}
}
}
}

View File

@@ -2,10 +2,10 @@ namespace FluidSim.Core
{
public static class Constants
{
public const double Gamma = 1.4;
public const double R_gas = 287.0; // J/(kg·K)
public const double P_amb = 101325.0; // Pa
public const double T_amb = 300.0; // K
public static readonly double Rho_amb = P_amb / (R_gas * T_amb); // ≈ 1.177 kg/m³
public const float Gamma = 1.4f;
public const float R_gas = 287f;
public const float P_amb = 101325f;
public const float T_amb = 300f;
public static readonly float Rho_amb = P_amb / (R_gas * T_amb);
}
}

27
Core/GhostBuffer.cs Normal file
View File

@@ -0,0 +1,27 @@
namespace FluidSim.Core
{
public class GhostBuffer
{
public float[] Rho, U, P, Y;
public int PipeCount { get; }
public GhostBuffer(int pipeCount)
{
PipeCount = pipeCount;
int size = pipeCount * 2;
Rho = new float[size];
U = new float[size];
P = new float[size];
Y = new float[size];
}
public void Set(int pipeIndex, bool isLeftEnd, float rho, float u, float p, float y)
{
int idx = pipeIndex * 2 + (isLeftEnd ? 0 : 1);
Rho[idx] = rho;
U[idx] = u;
P[idx] = p;
Y[idx] = y;
}
}
}

View File

@@ -2,40 +2,30 @@ using System;
namespace FluidSim.Core
{
/// <summary>
/// Compressible flow through an orifice, modelled as an isentropic nozzle.
/// 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.
/// </summary>
public static class IsentropicOrifice
{
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)
float pUp, float rhoUp, float TUp,
float pDown, float gamma, float R, float area, float Cd,
out float mdot, out float rhoFace, out float uFace, out float pFace)
{
mdot = 0; rhoFace = rhoUp; uFace = 0; pFace = pUp;
mdot = 0f; rhoFace = rhoUp; uFace = 0f; pFace = pUp;
if (area <= 0f || pUp <= 0f || rhoUp <= 0f || TUp <= 0f) return;
if (area <= 0 || pUp <= 0 || rhoUp <= 0 || TUp <= 0)
return;
float pr = MathF.Min(pDown / pUp, 1f);
if (pr < 1e-6f) pr = 1e-6f;
float prCrit = MathF.Pow(2f / (gamma + 1f), gamma / (gamma - 1f));
if (pr < prCrit) pr = prCrit;
double pr = pDown / pUp;
if (pr < 1e-6) pr = 1e-6;
float exponent = (gamma - 1f) / gamma;
float M = MathF.Sqrt((2f / (gamma - 1f)) * (MathF.Pow(pr, -exponent) - 1f));
if (float.IsNaN(M)) M = 0f;
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);
float aUp = MathF.Sqrt(gamma * R * TUp);
uFace = M * aUp;
rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma);
rhoFace = rhoUp * MathF.Pow(pr, 1f / gamma);
pFace = pUp * pr;
mdot = rhoFace * uFace * area * Cd; // positive from upstream to downstream
mdot = rhoFace * uFace * area * Cd;
}
}
}

View File

@@ -1,120 +0,0 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
/// <summary>
/// Characteristic openend boundary condition after Jones (1978).
/// For all subsonic flow (outflow and inflow), the ghost state is derived
/// from the isentropic expansion to ambient pressure, using the pipe's entropy,
/// and the outgoing Riemann invariant. This avoids a density jump at flow reversal.
/// Supersonic outflow extrapolates the interior state.
/// Now includes air fraction tracking: incoming air is fresh (AF=1), outgoing uses interior pipe AF.
/// </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;
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;
}
public void Resolve(double dtSub)
{
(double rhoInt, double uInt, double pInt) = IsLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
double airFracInt = IsLeftEnd
? Pipe.GetInteriorAirFractionLeft()
: Pipe.GetInteriorAirFractionRight();
double gamma = Gamma;
double gm1 = gamma - 1.0;
double cInt = Math.Sqrt(gamma * pInt / Math.Max(rhoInt, 1e-12));
double pAmb = AmbientPressure;
// Riemann invariants
double J_plus = uInt + 2.0 * cInt / gm1;
double J_minus = uInt - 2.0 * cInt / gm1;
double rhoGhost, uGhost, pGhost, airFracGhost;
// ---- Subsonic branch (used for both outflow and inflow) ----
double s = pInt / Math.Pow(rhoInt, gamma); // entropy constant
double rhoIso = Math.Pow(pAmb / s, 1.0 / gamma);
double cIso = Math.Sqrt(gamma * pAmb / Math.Max(rhoIso, 1e-12));
double uIso = IsLeftEnd
? (J_minus + 2.0 * cIso / gm1)
: (J_plus - 2.0 * cIso / gm1);
// Check for supersonic outflow
bool supersonic = IsLeftEnd
? (uInt <= -cInt)
: (uInt >= cInt);
if (!supersonic)
{
if (IsLeftEnd)
supersonic = uIso <= -cIso;
else
supersonic = uIso >= cIso;
}
if (supersonic)
{
// Supersonic outflow extrapolate interior
rhoGhost = rhoInt;
uGhost = uInt;
pGhost = pInt;
airFracGhost = airFracInt; // whatever is leaving
}
else
{
// Subsonic flow use isentropic state to ambient
rhoGhost = rhoIso;
uGhost = uIso;
pGhost = pAmb;
// Determine if inflow or outflow
bool isInflow = IsLeftEnd ? (uIso >= 0) : (uIso <= 0); // positive u means into pipe from left end? Wait: left end: u>0 means flow to the right, into pipe. Right end: u>0 means flow to the right, out of pipe. Let's use mass flow sign later.
// More straightforward: if using the isentropic state, the ghost velocity direction indicates flow. For inflow (ambient to pipe), airFraction = 1.0; for outflow, airFraction = interior's AF.
if ((IsLeftEnd && uIso >= 0) || (!IsLeftEnd && uIso <= 0))
{
// Inflow (ambient enters pipe)
airFracGhost = 1.0;
}
else
{
// Outflow (pipe exits to ambient)
airFracGhost = airFracInt;
}
}
// Apply ghost to pipe
if (IsLeftEnd)
Pipe.SetGhostLeft(rhoGhost, uGhost, pGhost, airFracGhost);
else
Pipe.SetGhostRight(rhoGhost, uGhost, pGhost, airFracGhost);
// Mass flow out of the pipe (positive = leaving)
double area = Pipe.Area;
double mdot = rhoGhost * uGhost * area;
if (IsLeftEnd) mdot = -mdot; // left end: positive u is into pipe, outward flow is -u
LastMassFlowRate = mdot;
LastFaceDensity = rhoGhost;
LastFaceVelocity = uGhost;
LastFacePressure = pGhost;
}
}
}

View File

@@ -1,173 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
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 EffectiveLength { get; set; } = 0.001;
public bool UseInertance { get; set; } = false;
// Current mass flow (kg/s, positive = volume → pipe)
private double _mdot;
public double LastMassFlowRate { get; private set; } // positive = into volume
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;
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
IsPipeLeftEnd = isPipeLeftEnd;
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
_mdot = 0.0;
}
public void Resolve(double dtSub)
{
double area = AreaProvider();
if (area < 1e-12 || VolumePort == null)
{
SetClosedWall();
return;
}
// Gather states
double volP = VolumePort.Pressure;
double volRho = VolumePort.Density;
double volT = VolumePort.Temperature;
double volH = VolumePort.SpecificEnthalpy;
double volAF = VolumePort.AirFraction;
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
double pipeT = pipeP / Math.Max(pipeRho * 287.0, 1e-12);
double pipeAF = IsPipeLeftEnd
? Pipe.GetInteriorAirFractionLeft()
: Pipe.GetInteriorAirFractionRight();
double gamma = 1.4;
double R = 287.0;
// ---- Steadystate nozzle solution ----
double mdotSS; // positive = volume → pipe
double rhoFace0, uFace0, pFace0;
if (volP >= pipeP)
{
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
mdotSS = mdotUpToDown;
}
else
{
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
mdotSS = -mdotUpToDown;
}
// ---- Dynamic update ----
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
{
_mdot = mdotSS;
}
// Clamp outflow to available mass (if finite volume)
if (VolumePort.Owner is Volume0D vol)
{
double maxOut = vol.Mass / dtSub;
if (_mdot > maxOut) _mdot = maxOut;
}
// ---- Ghost state with air fraction ----
double rhoFace = _mdot >= 0 ? volRho : pipeRho;
double pFace = pFace0;
double mdotMag = Math.Abs(_mdot);
double uFace = mdotMag / (rhoFace * area);
// Determine air fraction for ghost and for volume port
double airFracGhost; // air fraction of ghost cell (at pipe end)
double airFracForVolume; // if flow reverses into volume, this is the air fraction entering volume
if (_mdot >= 0) // volume → pipe
{
airFracGhost = volAF;
// Flow enters pipe; no need to set volume's air fraction (port already has its own)
airFracForVolume = volAF; // unused
}
else // pipe → volume
{
airFracGhost = pipeAF;
airFracForVolume = pipeAF;
VolumePort.AirFraction = airFracForVolume;
}
if (IsPipeLeftEnd)
uFace = _mdot >= 0 ? uFace : -uFace;
else
uFace = _mdot >= 0 ? -uFace : uFace;
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rhoFace, uFace, pFace, airFracGhost);
else
Pipe.SetGhostRight(rhoFace, uFace, pFace, airFracGhost);
// Store results (positive = into volume)
LastMassFlowRate = -_mdot;
LastFaceDensity = rhoFace;
LastFaceVelocity = uFace;
LastFacePressure = pFace;
VolumePort.MassFlowRate = -_mdot;
// Enthalpy transport
if (-_mdot >= 0) // inflow → pipe enthalpy
{
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
VolumePort.SpecificEnthalpy = hPipe;
}
else
{
VolumePort.SpecificEnthalpy = volH;
}
}
private void SetClosedWall()
{
var (rInt, uInt, pInt) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight();
if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
else
Pipe.SetGhostRight(rInt, -uInt, pInt, IsPipeLeftEnd ? Pipe.GetInteriorAirFractionLeft() : Pipe.GetInteriorAirFractionRight());
LastMassFlowRate = 0.0;
LastFaceDensity = rInt;
LastFaceVelocity = 0.0;
LastFacePressure = pInt;
if (VolumePort != null)
VolumePort.MassFlowRate = 0.0;
}
}
}

560
Core/Pipesystem.cs Normal file
View File

@@ -0,0 +1,560 @@
using System;
using System.Diagnostics;
using System.Numerics;
namespace FluidSim.Core
{
public class PipeSystem
{
// ---------- Master arrays ----------
private float[] _rho, _rhou, _E, _Y;
private readonly float[] _area;
private readonly float[] _dx;
private readonly int[] _pipeStart;
private readonly int[] _pipeEnd;
private readonly int _totalCells; // original cell count (visible)
private readonly int _allCells; // total allocated (padded to Vector<float>.Count)
private readonly int _pipeCount;
// Derived state _p is kept for visualization, _c is gone
private float[] _p;
// Flux arrays (size = _allCells + 1)
private float[] _fluxM, _fluxP, _fluxE, _fluxY;
// Damping and relaxation (computed onthefly only if used)
private float[] _dampingFactors;
private float[] _relaxFactors;
private bool _applyDamping;
private bool _applyRelax;
// Ghost buffer
private readonly GhostBuffer _ghost;
// Wall mask precomputed once
private readonly bool[] _isWallFace;
// ---------- Physical constants ----------
private const float Gamma = 1.4f;
private const float Gm1 = 0.4f;
private const float Gm1Inv = 1f / Gm1; // 2.5
private const float GammaOverGm1 = Gamma / Gm1; // 3.5
private float _coeffBase;
private float _relaxRate;
private float _ambientPressure = 101325f;
private float _ambientEnergyRef;
public float DampingMultiplier
{
set
{
_coeffBase = 0.1f * value;
_applyDamping = _coeffBase != 0f;
}
}
public float EnergyRelaxationRate
{
set
{
_relaxRate = value;
_applyRelax = _relaxRate != 0f;
}
}
public float AmbientPressure
{
set
{
_ambientPressure = value;
_ambientEnergyRef = value * Gm1Inv;
}
}
// ---------- Profiling ----------
public bool EnableProfiling { get; set; }
private long _profFluxTicks;
private long _profUpdateTicks;
private long _profCallCount;
// ---------- Construction ----------
public PipeSystem(int totalCells, int[] pipeStart, int[] pipeEnd,
float[] area, float[] dx,
float initialRho, float initialU, float initialP)
{
_pipeStart = pipeStart;
_pipeEnd = pipeEnd;
_pipeCount = pipeStart.Length;
_totalCells = totalCells;
_area = area;
_dx = dx;
// Pad to SIMD width so all vectorized loops cover the whole data
int vecSize = Vector<float>.Count;
_allCells = totalCells % vecSize == 0 ? totalCells : totalCells + vecSize - (totalCells % vecSize);
_rho = new float[_allCells];
_rhou = new float[_allCells];
_E = new float[_allCells];
_Y = new float[_allCells];
_p = new float[_allCells]; // pressure for drawing
int faceCount = _allCells + 1;
_fluxM = new float[faceCount];
_fluxP = new float[faceCount];
_fluxE = new float[faceCount];
_fluxY = new float[faceCount];
_dampingFactors = new float[_allCells];
_relaxFactors = new float[_allCells];
_applyDamping = _coeffBase != 0f;
_applyRelax = _relaxRate != 0f;
_ghost = new GhostBuffer(_pipeCount);
_ambientEnergyRef = initialP * Gm1Inv;
// Precompute wall face flags: each face that sits between two different pipes is a wall
_isWallFace = new bool[faceCount];
for (int f = 1; f < _totalCells; f++)
{
for (int p = 0; p < _pipeCount; p++)
{
if (f == _pipeEnd[p] && f < _totalCells)
{
_isWallFace[f] = true;
break;
}
}
}
// Initialize uniform state
float initE = initialP / (Gm1 * initialRho);
float rhoE = initialRho * initE + 0.5f * initialRho * initialU * initialU;
for (int i = 0; i < totalCells; i++)
{
_rho[i] = initialRho;
_rhou[i] = initialRho * initialU;
_E[i] = rhoE;
_Y[i] = 1f;
}
}
// ---------- Ghost setters (for BoundarySystem) ----------
public void SetGhostLeft(int pipeIndex, float rho, float u, float p, float y)
=> _ghost.Set(pipeIndex, true, rho, u, p, y);
public void SetGhostRight(int pipeIndex, float rho, float u, float p, float y)
=> _ghost.Set(pipeIndex, false, rho, u, p, y);
// ---------- Public read methods ----------
public int TotalCells => _totalCells;
public int PipeCount => _pipeCount;
public int GetPipeStart(int pipeIdx) => _pipeStart[pipeIdx];
public int GetPipeEnd(int pipeIdx) => _pipeEnd[pipeIdx];
public float GetCellPressure(int i) => _p[i];
public float GetCellDensity(int i) => _rho[i];
public float GetCellVelocity(int i)
{
float rho = _rho[i];
return rho > 1e-12f ? _rhou[i] / rho : 0f;
}
public float GetCellAirFraction(int i) => _Y[i];
public (float rho, float u, float p) GetInteriorStateLeft(int pipeIdx)
{
int i = _pipeStart[pipeIdx];
float rho = _rho[i];
float rhou = _rhou[i];
float u = rhou / MathF.Max(rho, 1e-12f);
float p = Gm1 * (_E[i] - 0.5f * rhou * u);
return (rho, u, p);
}
public (float rho, float u, float p) GetInteriorStateRight(int pipeIdx)
{
int i = _pipeEnd[pipeIdx] - 1;
float rho = _rho[i];
float rhou = _rhou[i];
float u = rhou / MathF.Max(rho, 1e-12f);
float p = Gm1 * (_E[i] - 0.5f * rhou * u);
return (rho, u, p);
}
public float GetInteriorAirFractionLeft(int pipeIdx) => _Y[_pipeStart[pipeIdx]];
public float GetInteriorAirFractionRight(int pipeIdx) => _Y[_pipeEnd[pipeIdx] - 1];
public void SetCellState(int i, float rho, float u, float p, float y = 1f)
{
if (i < 0 || i >= _totalCells) return;
_rho[i] = rho;
_rhou[i] = rho * u;
_E[i] = p * Gm1Inv + 0.5f * rho * u * u;
_Y[i] = y;
}
// ---------- Main step ----------
public void SimulateStep(float dt)
{
long t0 = 0, t1 = 0;
if (EnableProfiling)
{
_profCallCount++;
t0 = Stopwatch.GetTimestamp();
}
ComputeFluxes(dt);
if (EnableProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profFluxTicks += (t1 - t0);
t0 = t1;
}
UpdateCells(dt);
if (EnableProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profUpdateTicks += (t1 - t0);
}
}
// ---------- Flux computation: fuses primitive calculation and flux evaluation ----------
private void ComputeFluxes(float dt)
{
float fm, fp, fe;
int vecSize = Vector<float>.Count;
// ---- 1. Left ghost boundaries ----
for (int p = 0; p < _pipeCount; p++)
{
int idx = _pipeStart[p];
int ghostIdx = p * 2;
float rL = _ghost.Rho[ghostIdx];
float uL = _ghost.U[ghostIdx];
float pL = _ghost.P[ghostIdx];
float YL = _ghost.Y[ghostIdx];
float cL = MathF.Sqrt(Gamma * pL / MathF.Max(rL, 1e-12f));
float rR = _rho[idx], rhouR = _rhou[idx];
float invRhoR = MathF.ReciprocalEstimate(MathF.Max(rR, 1e-12f));
float uR = rhouR * invRhoR;
float pR = Gm1 * (_E[idx] - 0.5f * rhouR * uR);
float cR = MathF.Sqrt(Gamma * pR * invRhoR);
float YR = _Y[idx];
// store pressure for cell idx
_p[idx] = pR;
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR, out fm, out fp, out fe);
_fluxM[idx] = fm; _fluxP[idx] = fp; _fluxE[idx] = fe;
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
ScalarFlux(rL, uL, YL, rR, uR, YR, alpha, out float fy);
_fluxY[idx] = fy;
}
// ---- 2. Right ghost boundaries ----
for (int p = 0; p < _pipeCount; p++)
{
int idx = _pipeEnd[p] - 1;
int face = idx + 1;
int ghostIdx = p * 2 + 1;
float rR = _ghost.Rho[ghostIdx];
float uR = _ghost.U[ghostIdx];
float pR = _ghost.P[ghostIdx];
float YR = _ghost.Y[ghostIdx];
float cR = MathF.Sqrt(Gamma * pR / MathF.Max(rR, 1e-12f));
float rL = _rho[idx], rhouL = _rhou[idx];
float invRhoL = MathF.ReciprocalEstimate(MathF.Max(rL, 1e-12f));
float uL = rhouL * invRhoL;
float pL = Gm1 * (_E[idx] - 0.5f * rhouL * uL);
float cL = MathF.Sqrt(Gamma * pL * invRhoL);
float YL = _Y[idx];
// store pressure for cell idx
_p[idx] = pL;
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR, out fm, out fp, out fe);
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
ScalarFlux(rL, uL, YL, rR, uR, YR, alpha, out float fy);
_fluxY[face] = fy;
}
// ---- 3. Interior faces vectorised SIMD ----
for (int face = 1; face < _totalCells; face++)
{
// Handle walls (rare) with scalar code
if (_isWallFace[face])
{
int iL = face - 1;
float rL = _rho[iL], rhouL = _rhou[iL];
float invRhoL = MathF.ReciprocalEstimate(MathF.Max(rL, 1e-12f));
float uL = rhouL * invRhoL;
float pL = Gm1 * (_E[iL] - 0.5f * rhouL * uL);
float cL = MathF.Sqrt(Gamma * pL * invRhoL);
_p[iL] = pL;
LaxFlux(rL, uL, pL, cL, rL, -uL, pL, cL, out fm, out fp, out fe);
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
_fluxY[face] = 0f;
continue;
}
// If the next vecSize faces contain a wall, fall back to scalar for this block
if (face + vecSize - 1 < _totalCells)
{
bool hasWall = false;
for (int f = face; f < face + vecSize; f++)
if (_isWallFace[f]) { hasWall = true; break; }
if (!hasWall)
{
// --- Vectorised block ---
var rhoL = new Vector<float>(_rho, face - 1);
var rhouL = new Vector<float>(_rhou, face - 1);
var EL = new Vector<float>(_E, face - 1);
var YL = new Vector<float>(_Y, face - 1);
var rhoR = new Vector<float>(_rho, face);
var rhouR = new Vector<float>(_rhou, face);
var ER = new Vector<float>(_E, face);
var YR = new Vector<float>(_Y, face);
var invRhoL = Vector<float>.One / Vector.Max(rhoL, new Vector<float>(1e-12f));
var invRhoR = Vector<float>.One / Vector.Max(rhoR, new Vector<float>(1e-12f));
var uL = rhouL * invRhoL;
var uR = rhouR * invRhoR;
var kinL = 0.5f * rhouL * uL;
var kinR = 0.5f * rhouR * uR;
var pL = Gm1 * (EL - kinL);
var pR = Gm1 * (ER - kinR);
var cL = Vector.SquareRoot(Gamma * pL * invRhoL);
var cR = Vector.SquareRoot(Gamma * pR * invRhoR);
// Store pressures for visualisation (left cell of each face)
pL.CopyTo(_p, face - 1);
// LaxFriedrichs fluxes
var ELs = pL * Gm1Inv * invRhoL + 0.5f * uL * uL; // energy per mass
var ERs = pR * Gm1Inv * invRhoR + 0.5f * uR * uR;
var FmL = rhoL * uL;
var FpL = rhoL * uL * uL + pL;
var FeL = (rhoL * ELs + pL) * uL;
var FmR = rhoR * uR;
var FpR = rhoR * uR * uR + pR;
var FeR = (rhoR * ERs + pR) * uR;
var absUL = Vector.Abs(uL);
var absUR = Vector.Abs(uR);
var alpha = Vector.Max(absUL + cL, absUR + cR);
var fmVec = 0.5f * (FmL + FmR) - 0.5f * alpha * (rhoR - rhoL);
var fpVec = 0.5f * (FpL + FpR) - 0.5f * alpha * (rhouR - rhouL);
var feVec = 0.5f * (FeL + FeR) - 0.5f * alpha * (rhoR * ERs - rhoL * ELs);
var fyL = FmL * YL;
var fyR = FmR * YR;
var fyVec = 0.5f * (fyL + fyR) - 0.5f * alpha * (rhoR * YR - rhoL * YL);
fmVec.CopyTo(_fluxM, face);
fpVec.CopyTo(_fluxP, face);
feVec.CopyTo(_fluxE, face);
fyVec.CopyTo(_fluxY, face);
face += vecSize - 1; // loop increment will add 1, so we advance vecSize faces
continue;
}
}
// --- Scalar interior face (fallback) ---
{
int iLf = face - 1, iRf = face;
float rLf = _rho[iLf], rhouLf = _rhou[iLf];
float invRhoLf = MathF.ReciprocalEstimate(MathF.Max(rLf, 1e-12f));
float uLf = rhouLf * invRhoLf;
float pLf = Gm1 * (_E[iLf] - 0.5f * rhouLf * uLf);
float cLf = MathF.Sqrt(Gamma * pLf * invRhoLf);
float YLf = _Y[iLf];
_p[iLf] = pLf;
float rRf = _rho[iRf], rhouRf = _rhou[iRf];
float invRhoRf = MathF.ReciprocalEstimate(MathF.Max(rRf, 1e-12f));
float uRf = rhouRf * invRhoRf;
float pRf = Gm1 * (_E[iRf] - 0.5f * rhouRf * uRf);
float cRf = MathF.Sqrt(Gamma * pRf * invRhoRf);
float YRf = _Y[iRf];
LaxFlux(rLf, uLf, pLf, cLf, rRf, uRf, pRf, cRf, out fm, out fp, out fe);
_fluxM[face] = fm; _fluxP[face] = fp; _fluxE[face] = fe;
float alpha = MathF.Max(MathF.Abs(uLf) + cLf, MathF.Abs(uRf) + cRf);
ScalarFlux(rLf, uLf, YLf, rRf, uRf, YRf, alpha, out float fy);
_fluxY[face] = fy;
}
}
// If damping/relaxation are active, compute the factors here (re-uses _dampingFactors/_relaxFactors arrays,
// but we no longer have a separate precompute pass). We compute them on demand in UpdateCells anyway?
// Actually UpdateCells multiplies by these factors; we can compute them there if needed.
}
// ---------- Cell update (unchanged core, but skips relaxation/damping when not needed) ----------
private void UpdateCells(float dt)
{
int vecSize = Vector<float>.Count;
float dtRelax = -_relaxRate * dt;
// Compute damping and relaxation factors if needed
if (_applyDamping)
{
for (int i = 0; i < _totalCells; i++)
{
float rho = _rho[i];
_dampingFactors[i] = rho > 1e-12f
? MathF.Exp(-_coeffBase * dt / rho)
: 1f;
}
}
if (_applyRelax)
{
var relaxVal = MathF.Exp(dtRelax);
for (int i = 0; i < _totalCells; i++)
_relaxFactors[i] = relaxVal;
}
int iCell = 0;
for (; iCell <= _totalCells - vecSize; iCell += vecSize)
{
var rhoOld = new Vector<float>(_rho, iCell);
var rhouOld = new Vector<float>(_rhou, iCell);
var EOld = new Vector<float>(_E, iCell);
var YOld = new Vector<float>(_Y, iCell);
var fluxM_L = new Vector<float>(_fluxM, iCell);
var fluxP_L = new Vector<float>(_fluxP, iCell);
var fluxE_L = new Vector<float>(_fluxE, iCell);
var fluxY_L = new Vector<float>(_fluxY, iCell);
var fluxM_R = new Vector<float>(_fluxM, iCell + 1);
var fluxP_R = new Vector<float>(_fluxP, iCell + 1);
var fluxE_R = new Vector<float>(_fluxE, iCell + 1);
var fluxY_R = new Vector<float>(_fluxY, iCell + 1);
var dtdx = new Vector<float>(dt) / new Vector<float>(_dx, iCell);
var rhoNew = rhoOld - dtdx * (fluxM_R - fluxM_L);
var rhouNew = rhouOld - dtdx * (fluxP_R - fluxP_L);
var ENew = EOld - dtdx * (fluxE_R - fluxE_L);
var rhoYOld = rhoOld * YOld;
var rhoYNew = rhoYOld - dtdx * (fluxY_R - fluxY_L);
if (_applyDamping)
rhouNew *= new Vector<float>(_dampingFactors, iCell);
if (_applyRelax)
{
var ambRef = new Vector<float>(_ambientEnergyRef);
var relax = new Vector<float>(_relaxFactors, iCell);
ENew = ambRef + (ENew - ambRef) * relax;
}
rhoNew = Vector.Max(rhoNew, new Vector<float>(1e-12f));
var kinNew = 0.5f * rhouNew * rhouNew / rhoNew;
var eMin = new Vector<float>(100f * Gm1Inv) + kinNew;
ENew = Vector.Max(ENew, eMin);
rhoNew.CopyTo(_rho, iCell);
rhouNew.CopyTo(_rhou, iCell);
ENew.CopyTo(_E, iCell);
var yNew = rhoYNew / rhoNew;
yNew = Vector.Min(Vector.Max(yNew, Vector<float>.Zero), Vector<float>.One);
yNew.CopyTo(_Y, iCell);
}
// Scalar remainder (only a few cells)
for (; iCell < _totalCells; iCell++)
{
float rhoOld = _rho[iCell], rhouOld = _rhou[iCell], EOld = _E[iCell], YOld = _Y[iCell];
float fluxM_L = _fluxM[iCell], fluxP_L = _fluxP[iCell], fluxE_L = _fluxE[iCell], fluxY_L = _fluxY[iCell];
float fluxM_R = _fluxM[iCell + 1], fluxP_R = _fluxP[iCell + 1], fluxE_R = _fluxE[iCell + 1], fluxY_R = _fluxY[iCell + 1];
float dtdx = dt / _dx[iCell];
float rhoNew = rhoOld - dtdx * (fluxM_R - fluxM_L);
float rhouNew = rhouOld - dtdx * (fluxP_R - fluxP_L);
float ENew = EOld - dtdx * (fluxE_R - fluxE_L);
float rhoYOld = rhoOld * YOld;
float rhoYNew = rhoYOld - dtdx * (fluxY_R - fluxY_L);
if (_applyDamping) rhouNew *= _dampingFactors[iCell];
if (_applyRelax) ENew = _ambientEnergyRef + (ENew - _ambientEnergyRef) * _relaxFactors[iCell];
rhoNew = MathF.Max(rhoNew, 1e-12f);
float kin = 0.5f * rhouNew * rhouNew / rhoNew;
float eMin = 100f * Gm1Inv + kin;
ENew = MathF.Max(ENew, eMin);
_rho[iCell] = rhoNew;
_rhou[iCell] = rhouNew;
_E[iCell] = ENew;
_Y[iCell] = Math.Clamp(rhoYNew / rhoNew, 0f, 1f);
}
}
// ---------- Scalar flux helpers (used in boundaries and scalar fallback) ----------
private static void LaxFlux(float rL, float uL, float pL, float cL,
float rR, float uR, float pR, float cR,
out float fm, out float fp, out float fe)
{
float EL = pL * Gm1Inv / rL + 0.5f * uL * uL;
float ER = pR * Gm1Inv / rR + 0.5f * uR * uR;
float FmL = rL * uL;
float FpL = rL * uL * uL + pL;
float FeL = (rL * EL + pL) * uL;
float FmR = rR * uR;
float FpR = rR * uR * uR + pR;
float FeR = (rR * ER + pR) * uR;
float alpha = MathF.Max(MathF.Abs(uL) + cL, MathF.Abs(uR) + cR);
fm = 0.5f * (FmL + FmR) - 0.5f * alpha * (rR - rL);
fp = 0.5f * (FpL + FpR) - 0.5f * alpha * (rR * uR - rL * uL);
fe = 0.5f * (FeL + FeR) - 0.5f * alpha * (rR * ER - rL * EL);
}
private static void ScalarFlux(float rL, float uL, float YL,
float rR, float uR, float YR,
float alpha, out float fy)
{
float FyL = rL * uL * YL;
float FyR = rR * uR * YR;
fy = 0.5f * (FyL + FyR) - 0.5f * alpha * (rR * YR - rL * YL);
}
// ---------- Profiling report ----------
public string GetProfileReport()
{
if (!EnableProfiling || _profCallCount == 0)
return "Pipe profiling disabled or no data.";
double freq = Stopwatch.Frequency;
long totalTicks = _profFluxTicks + _profUpdateTicks;
if (totalTicks == 0) return "No pipe profile data collected.";
double totalMs = totalTicks * 1000.0 / freq;
double avgCallUs = totalMs * 1000.0 / _profCallCount;
double fluxMs = _profFluxTicks * 1000.0 / freq;
double updateMs = _profUpdateTicks * 1000.0 / freq;
double fluxAvgUs = fluxMs * 1000.0 / _profCallCount;
double updateAvgUs = updateMs * 1000.0 / _profCallCount;
string report = $" Pipe kernel (over {_profCallCount} calls, total {totalMs:F2} ms, avg {avgCallUs:F2} µs/call):\n";
report += $" Fluxes (incl. primitives): {fluxMs:F2} ms ({_profFluxTicks * 100.0 / totalTicks:F1}%), avg {fluxAvgUs:F2} µs/call\n";
report += $" Update cells: {updateMs:F2} ms ({_profUpdateTicks * 100.0 / totalTicks:F1}%), avg {updateAvgUs:F2} µs/call\n";
_profFluxTicks = 0;
_profUpdateTicks = 0;
_profCallCount = 0;
return report;
}
}
}

View File

@@ -10,136 +10,91 @@ namespace FluidSim.Core
public class Solver
{
private readonly List<IComponent> _components = new();
private readonly List<OrificeLink> _orificeLinks = new();
private readonly List<OpenEndLink> _openEndLinks = new();
private PipeSystem _pipeSystem;
private BoundarySystem _boundarySystem;
private double _dt;
/// <summary>CFL target for substepping (0.30.8). Lower values are safer for shocks.</summary>
public double CflTarget { get; set; } = 0.9;
public int SubStepCount { get; set; } = 4;
public bool EnableProfiling { get; set; } = false;
// ---------- Timing accumulators (reset every LogInterval steps) ----------
private long _stepCount;
private double _timeTotal, _timeCFL, _timeOrifice, _timeOpenEnd,
_timePipe, _timeClearGhosts, _timeUpdateState;
private const int LogInterval = 5000;
private const bool EnableLogging = false; // temporarily ON for debugging
private long _ticksOrifice, _ticksOpenEnd, _ticksPipe, _ticksUpdate;
public void SetTimeStep(double dt) => _dt = dt;
public void AddComponent(IComponent component) => _components.Add(component);
public void AddOrificeLink(OrificeLink link) => _orificeLinks.Add(link);
public void AddOpenEndLink(OpenEndLink link) => _openEndLinks.Add(link);
public void SetPipeSystem(PipeSystem pipeSystem)
{
_pipeSystem = pipeSystem;
}
public void SetBoundarySystem(BoundarySystem boundarySystem)
{
_boundarySystem = boundarySystem;
}
public void Step()
{
var pipes = _components.OfType<Pipe1D>().ToList();
if (pipes.Count == 0) return;
if (_pipeSystem == null || _boundarySystem == null) return;
var sw = Stopwatch.StartNew();
// CFL count track which pipe demands the most substeps
int nSub = 1;
Pipe1D worstPipe = pipes[0];
foreach (var p in pipes)
{
int n = p.GetRequiredSubSteps(_dt, CflTarget);
if (n > nSub)
{
nSub = n;
worstPipe = p;
}
}
double dtSub = _dt / nSub;
// ----- Diagnostic: warn if nSub is high -----
if (nSub > 50)
{
double maxW = 0;
for (int i = 0; i < worstPipe.CellCount; i++)
{
double rho = worstPipe.GetCellDensity(i);
double u = Math.Abs(worstPipe.GetCellVelocity(i));
double p = worstPipe.GetCellPressure(i);
double c = Math.Sqrt(1.4 * p / Math.Max(rho, 1e-12));
if (u + c > maxW) maxW = u + c;
}
Console.WriteLine($"nSub = {nSub} (worst pipe: {worstPipe.Name}, maxW = {maxW:F0} m/s)");
}
_timeCFL += sw.Elapsed.TotalSeconds;
// ----- Safety cap prevent the solver from hanging -----
const int maxSubSteps = 10000;
const int hardLimit = 500; // temporary low cap for debugging
if (nSub > hardLimit)
{
Console.WriteLine($"nSub ({nSub}) exceeds hard limit {hardLimit}. Simulation step skipped.");
return;
}
int nSub = SubStepCount;
float dtSub = (float)(_dt / nSub);
for (int sub = 0; sub < nSub; sub++)
{
double t0;
long t0;
t0 = sw.Elapsed.TotalSeconds;
foreach (var link in _orificeLinks)
link.Resolve(dtSub);
_timeOrifice += sw.Elapsed.TotalSeconds - t0;
t0 = Stopwatch.GetTimestamp();
_boundarySystem.ResolveOrifices(dtSub);
_ticksOrifice += Stopwatch.GetTimestamp() - t0;
t0 = sw.Elapsed.TotalSeconds;
foreach (var link in _openEndLinks)
link.Resolve(dtSub);
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
t0 = Stopwatch.GetTimestamp();
_boundarySystem.ResolveOpenEnds(dtSub);
_ticksOpenEnd += Stopwatch.GetTimestamp() - t0;
t0 = sw.Elapsed.TotalSeconds;
foreach (var p in pipes)
p.SimulateSingleStep(dtSub);
_timePipe += sw.Elapsed.TotalSeconds - t0;
t0 = Stopwatch.GetTimestamp();
_pipeSystem.SimulateStep(dtSub);
_ticksPipe += Stopwatch.GetTimestamp() - t0;
}
double tCG = sw.Elapsed.TotalSeconds;
foreach (var p in pipes)
p.ClearGhostFlags();
_timeClearGhosts += sw.Elapsed.TotalSeconds - tCG;
double tUS = sw.Elapsed.TotalSeconds;
long tUS = Stopwatch.GetTimestamp();
foreach (var comp in _components)
comp.UpdateState(_dt);
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS;
_timeTotal += sw.Elapsed.TotalSeconds;
comp.UpdateState((float)_dt);
_ticksUpdate += Stopwatch.GetTimestamp() - tUS;
_stepCount++;
if (_stepCount % LogInterval == 0 && EnableLogging)
if (_stepCount % 5000 == 0 && EnableProfiling)
{
if (_timeTotal > 0)
{
double stepsPerSec = LogInterval / _timeTotal;
double avgUs = (_timeTotal / LogInterval) * 1e6;
double freq = Stopwatch.Frequency;
double total = _ticksOrifice + _ticksOpenEnd + _ticksPipe + _ticksUpdate;
double avgStepUs = (total / freq) * 1e6 / 5000.0;
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---");
Console.WriteLine($" Steps per second: {stepsPerSec:F1}");
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} %");
Console.WriteLine($" Substep loop:");
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} %");
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} %");
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} %");
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} %");
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} %");
Console.WriteLine();
int orificeCalls = 5000 * nSub;
int updateCalls = 5000;
double orificeMs = _ticksOrifice * 1000.0 / freq;
double openEndMs = _ticksOpenEnd * 1000.0 / freq;
double pipeMs = _ticksPipe * 1000.0 / freq;
double updateMs = _ticksUpdate * 1000.0 / freq;
double orificeAvgUs = orificeMs * 1000.0 / orificeCalls;
double openEndAvgUs = openEndMs * 1000.0 / orificeCalls;
double pipeAvgUs = pipeMs * 1000.0 / orificeCalls;
double updateAvgUs = updateMs * 1000.0 / updateCalls;
Console.WriteLine($"--- Solver ({5000} steps, nSub={nSub}) ---");
Console.WriteLine($" Average step: {avgStepUs:F2} µs");
Console.WriteLine($" Orifice: {orificeMs:F2} ms ({(double)_ticksOrifice / total * 100:F1}%), avg {orificeAvgUs:F2} µs/call");
Console.WriteLine($" OpenEnd: {openEndMs:F2} ms ({(double)_ticksOpenEnd / total * 100:F1}%), avg {openEndAvgUs:F2} µs/call");
Console.WriteLine($" Pipe: {pipeMs:F2} ms ({(double)_ticksPipe / total * 100:F1}%), avg {pipeAvgUs:F2} µs/call");
Console.WriteLine($" Update: {updateMs:F2} ms ({(double)_ticksUpdate / total * 100:F1}%), avg {updateAvgUs:F2} µs/call");
// Pipe internal breakdown (with per-phase averages)
if (_pipeSystem.EnableProfiling)
{
Console.WriteLine(_pipeSystem.GetProfileReport());
}
_timeTotal = 0;
_timeCFL = 0;
_timeOrifice = 0;
_timeOpenEnd = 0;
_timePipe = 0;
_timeClearGhosts = 0;
_timeUpdateState = 0;
_ticksOrifice = _ticksOpenEnd = _ticksPipe = _ticksUpdate = 0;
}
}
}

View File

@@ -1,76 +1,34 @@
using System;
using FluidSim.Core;
namespace FluidSim.Core
{
/// <summary>
/// Synthesises farfield exhaust sound using the monopole model
/// of Jones (1978). The radiated pressure is proportional to the
/// time derivative of the mass flow at the pipe exit.
///
/// Reference:
/// Jones, A.D. (1978) "Noise characteristics and exhaust process
/// gas dynamics of a small 2-stroke engine", PhD thesis, Univ. Adelaide.
/// </summary>
public class SoundProcessor
{
private readonly double dt;
private readonly double r; // listener distance (m)
private readonly double scaleFactor; // 1 / (4π r) (free-field monopole)
private readonly float dt;
private readonly float scaleFactor; // 1 / (4π r)
private float flowLP, prevMassFlowOut, smoothDMdt;
private readonly float lpAlpha, alpha;
// ---------- Massflow derivative (identical to original) ----------
private double flowLP;
private readonly double lpAlpha;
private double prevMassFlowOut;
private double smoothDMdt;
private readonly double alpha;
public float Gain = 1f;
public float Gain { get; set; } = 1.0f;
/// <summary>
/// </summary>
/// <param name="sampleRate">Audio sample rate (Hz).</param>
/// <param name="listenerDistanceMeters">Listener distance (m).</param>
/// <param name="pipeDiameterMeters">Ignored in this model; kept for compatibility.</param>
public SoundProcessor(int sampleRate,
double listenerDistanceMeters = 1.0,
double pipeDiameterMeters = 0.0217)
public SoundProcessor(int sampleRate, float listenerDistance = 1f)
{
dt = 1.0 / sampleRate;
r = listenerDistanceMeters;
scaleFactor = 1.0 / (4.0 * Math.PI * r); // freefield monopole
// ---- Smoothing time constants (unchanged) ----
double tau = 0.02; // 2 ms for derivative
alpha = Math.Exp(-dt / tau);
double tauLP = 0.00001; // 5 ms lowpass on mass flow
lpAlpha = Math.Exp(-dt / tauLP);
dt = 1f / sampleRate;
scaleFactor = 1f / (4f * MathF.PI * listenerDistance);
float tau = 0.02f;
alpha = MathF.Exp(-dt / tau);
float tauLP = 0.005f;
lpAlpha = MathF.Exp(-dt / tauLP);
}
/// <summary>
/// Process one sample. The OpenEndLink provides the instantaneous
/// exitplane mass flow.
/// </summary>
public float Process(OpenEndLink openEnd)
public float Process(float massFlowOut)
{
double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe
// Lowpass the mass flow signal
flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut;
// Derivative of the smoothed mass flow
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
flowLP = lpAlpha * flowLP + (1f - lpAlpha) * massFlowOut;
float rawDerivative = (flowLP - prevMassFlowOut) / dt;
prevMassFlowOut = flowLP;
// Smooth the derivative
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative;
// Farfield monopole pressure (freefield, Jones eq. 2.15 adapted)
double pressure = smoothDMdt * scaleFactor * Gain;
// Soft clip to ±1
return (float)pressure;
smoothDMdt = alpha * smoothDMdt + (1f - alpha) * rawDerivative;
float pressure = smoothDMdt * scaleFactor * Gain;
return MathF.Tanh(pressure);
}
}
}

View File

@@ -2,18 +2,9 @@ using System.Collections.Generic;
namespace FluidSim.Interfaces
{
/// <summary>
/// Minimal interface for all simulation components that have ports.
/// </summary>
public interface IComponent
{
/// <summary>All ports exposed by this component.</summary>
IReadOnlyList<Port> Ports { get; }
/// <summary>
/// Called once per global time step to update the component's internal state
/// using the port flow data accumulated during substeps.
/// </summary>
void UpdateState(double dt);
void UpdateState(float dt);
}
}

View File

@@ -2,23 +2,23 @@
{
public class Port
{
public double MassFlowRate; // kg/s, positive INTO the component that owns this port
public double SpecificEnthalpy; // J/kg
public double Pressure; // Pa
public double Density; // kg/m³
public double Temperature; // K
public double AirFraction; // mass fraction of air (0 = exhaust, 1 = air)
public float MassFlowRate; // kg/s, positive INTO owning component
public float SpecificEnthalpy; // J/kg
public float Pressure; // Pa
public float Density; // kg/m³
public float Temperature; // K
public float AirFraction; // mass fraction (0 = exhaust, 1 = air)
public object? Owner { get; set; }
public Port()
{
MassFlowRate = 0.0;
SpecificEnthalpy = 0.0;
Pressure = 101325.0;
Density = 1.225;
Temperature = 300.0;
AirFraction = 1.0; // default fresh air
MassFlowRate = 0f;
SpecificEnthalpy = 0f;
Pressure = 101325f;
Density = 1.225f;
Temperature = 300f;
AirFraction = 1f;
}
}
}

View File

@@ -17,7 +17,7 @@ public class Program
private const double DrawFrequency = 60.0;
// Playback speed
private static double _desiredSpeed = 0.01;
private static double _desiredSpeed = 0.001;
private static double _currentDisplaySpeed = _desiredSpeed;
private const double MinSpeed = 0.0001;
private const double MaxSpeed = 1.0;
@@ -38,11 +38,11 @@ public class Program
private static Text? _overlayText;
// Throttle control
private static double _throttleTarget = 1.0; // 01, set by arrow keys
private static double _throttleCurrent = 0.0; // actual current fraction (lerped)
private const double ThrottleLerpRate = 10.0; // times per second (speed of movement)
private static float _throttleTarget = 1.0f; // 01, set by arrow keys
private static float _throttleCurrent = 0.0f; // actual current fraction (lerped)
private const float ThrottleLerpRate = 10.0f; // times per second (speed of movement)
private static bool _wKeyHeld = false;
private static double _lastThrottleUpdateTime;
private static float _lastThrottleUpdateTime;
private const int TargetMaxFill = (int)(SampleRate * 0.2);
@@ -50,11 +50,11 @@ public class Program
{
var window = CreateWindow();
LoadFont();
_scenario = new Inline4Scenario();
_scenario = new HelmholtzScenario();
_scenario.Initialize(SampleRate);
_lastThrottleUpdateTime = 0.0;
_lastThrottleUpdateTime = 0.0f;
_simRingBuffer = new SimulationRingBuffer(131072);
_simRingBuffer = new SimulationRingBuffer(8192);
_soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 };
_soundEngine.Start();
@@ -77,19 +77,19 @@ public class Program
_soundEngine.Speed = _currentDisplaySpeed;
// ---- Throttle update ----
double dtThrottle = now - _lastThrottleUpdateTime;
_lastThrottleUpdateTime = now;
float dtThrottle = (float)now - _lastThrottleUpdateTime;
_lastThrottleUpdateTime = (float)now;
double throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0;
float throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0f;
// Snap to zero instantly when target is zero (key released)
if (throttleDesiredFraction == 0.0)
{
_throttleCurrent = 0.0;
_throttleCurrent = 0.0f;
}
else
{
double smoothing = 1.0 - Math.Exp(-ThrottleLerpRate * dtThrottle);
float smoothing = 1.0f - MathF.Exp(-ThrottleLerpRate * dtThrottle);
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
}
@@ -199,11 +199,11 @@ public class Program
break;
case Keyboard.Key.Up:
_throttleTarget = Math.Min(1.0, _throttleTarget + 0.05);
_throttleTarget = MathF.Min(1.0f, _throttleTarget + 0.05f);
break;
case Keyboard.Key.Down:
_throttleTarget = Math.Max(0.0, _throttleTarget - 0.05);
_throttleTarget = MathF.Max(0.0f, _throttleTarget - 0.05f);
break;
}
}

View File

@@ -0,0 +1,152 @@
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Interfaces;
using SFML.Graphics;
using SFML.System;
using System;
namespace FluidSim.Tests
{
public class HelmholtzScenario : Scenario
{
private Volume0D cavity;
private Port cavityPort;
private PipeSystem pipeSystem;
private int[] pipeStart = { 0 };
private int[] pipeEnd;
private BoundarySystem boundaries;
private int cavityOrificeIdx = 0;
private int openEndIdx = 0;
private Solver solver;
private double dt;
private int stepCount;
private SoundProcessor soundProcessor;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// --- Realistic Helmholtz resonator dimensions ---
float cavityVolume = 1e-3f; // 1 liter
float neckLength = 0.05f; // 5 cm
float neckDiameter = 0.02f; // 2 cm diameter
float neckArea = MathF.PI * 0.25f * neckDiameter * neckDiameter;
int neckCells = 20;
// --- Volume (cavity) ---
float initialPressure = 1.2f * 101325f; // slight overpressure
float initialTemperature = 300f;
cavity = new Volume0D(cavityVolume, initialPressure, initialTemperature);
cavityPort = cavity.CreatePort();
// --- Pipe (neck) ---
float[] areas = new float[neckCells];
float[] dxs = new float[neckCells];
float dx = neckLength / neckCells;
for (int i = 0; i < neckCells; i++)
{
areas[i] = neckArea;
dxs[i] = dx;
}
pipeEnd = new[] { neckCells };
float rho0 = 101325f / (287f * 300f);
pipeSystem = new PipeSystem(neckCells, pipeStart, pipeEnd, areas, dxs, rho0, 0f, 101325f);
// Energy loss
cavity.EnergyRelaxationRate = 80f;
pipeSystem.EnergyRelaxationRate = 0f;
pipeSystem.DampingMultiplier = 2000f;
// --- Boundary system ---
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 1, maxOpenEnds: 1);
// Use steady orifice the pipe already provides the inertia
boundaries.AddOrifice(cavityPort, pipeIndex: 0, isLeftEnd: true, areaIndex: cavityOrificeIdx, dischargeCoeff: 1f, lossCoefficient: 0.1f);
// LOSS COEFFICIENT BREAKS THE SYSTEM AT ~0.55, AT VALUES LOWER THAN THAT, IT SEEMS TO ONLY AFFECT VOLUME, NOT COMPOUND
// Open end at right side of pipe
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: false, 101325f, neckArea);
float[] orificeAreas = new float[1] { neckArea };
boundaries.SetOrificeAreas(orificeAreas);
// --- Solver ---
// Slightly higher substep count to ensure stability of the resonant oscillation
solver = new Solver { SubStepCount = 6, EnableProfiling = true };
solver.SetTimeStep(dt);
solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
solver.AddComponent(cavity);
// --- Sound ---
soundProcessor = new SoundProcessor(sampleRate, 1f) { Gain = 2f };
Console.WriteLine("Helmholtz resonator ready.");
stepCount = 0;
}
public override float Process()
{
stepCount++;
if (stepCount <= 8192) return 0f; // let buffer prefill
solver.Step();
float flow = boundaries.GetOpenEndMassFlow(openEndIdx);
float sample = soundProcessor.Process(flow);
if (stepCount % 10000 == 0)
{
float cavityP = cavity.Pressure;
float cavityT = cavity.Temperature;
float cavityRho = cavity.Density;
float cCavity = MathF.Sqrt(1.4f * cavityP / MathF.Max(cavityRho, 1e-12f));
// Temperature in the middle of the neck
int midCell = 10;
float pMid = pipeSystem.GetCellPressure(midCell);
float rhoMid = pipeSystem.GetCellDensity(midCell);
float tMid = pMid / MathF.Max(rhoMid * 287f, 1e-12f);
// Neck effective length (physical + end correction)
float neckLen = 0.05f; // physical
float neckDia = 0.02f;
float neckArea = MathF.PI * 0.25f * neckDia * neckDia;
float endCorr = 0.85f * neckDia; // unflanged end
float L_eff = neckLen + endCorr;
// Theoretical Helmholtz frequency from current cavity sound speed
float fHelmholtz = cCavity / (2f * MathF.PI) *
MathF.Sqrt(neckArea / (cavity.Volume * L_eff));
Console.WriteLine(
$"Step {stepCount}: cav P={cavityP / 1e5f:F4} bar, T={cavityT:F1} K, " +
$"pipeMid T={tMid:F1} K, est f={fHelmholtz:F1} Hz");
}
return sample;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float cavityCenterX = 100f;
float cavityWidth = 80f, cavityHeight = 100f;
float cavityTopY = winH / 2f - cavityHeight / 2f;
DrawVolume(target, cavity, cavityCenterX, cavityTopY - 40f, cavityWidth, cavityHeight);
float pipeStartX = cavityCenterX + cavityWidth / 2f + 10f;
float pipeEndX = winW - 50f;
float pipeCenterY = winH / 2f;
DrawPipe(target, pipeSystem, 0, pipeCenterY, pipeStartX, pipeEndX);
}
}
}

View File

@@ -1,472 +0,0 @@
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Utils;
namespace FluidSim.Tests
{
public class Inline4Scenario : Scenario
{
// Crankshaft
private Crankshaft crankshaft;
// Cylinders
private Cylinder cyl1, cyl2, cyl3, cyl4;
// Intake
private Pipe1D intakePipeBeforeThrottle;
private Volume0D intakePlenum;
// Runners (shorter, fewer cells)
private Pipe1D runner1, runner2, runner3, runner4;
// Exhaust collector + tailpipe
private Volume0D exhaustCollector;
private Pipe1D tailPipe;
// Exhaust stubs (short pipes between cylinders and collector)
private Pipe1D exhStub1, exhStub2, exhStub3, exhStub4;
// Links intake
private OpenEndLink intakeOpenEnd;
private OrificeLink throttleOrifice;
// Plenumtorunner orifices
private OrificeLink plenumToRunner1, plenumToRunner2, plenumToRunner3, plenumToRunner4;
// Intake valves
private OrificeLink intakeValve1, intakeValve2, intakeValve3, intakeValve4;
// Exhaust valves (cylinder → stub)
private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4;
// Stubtocollector orifices
private OrificeLink stubToCollector1, stubToCollector2, stubToCollector3, stubToCollector4;
// Collectortotailpipe orifice
private OrificeLink collectorToTailpipe;
// Exhaust open end (tailpipe exit)
private OpenEndLink exhaustOpenEnd;
private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private double dt;
private int stepCount;
public double MaxThrottleArea { get; set; } = 10 * Units.cm2;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
solver = new Solver();
solver.SetTimeStep(dt);
solver.CflTarget = 1;
// ---- Shared crankshaft ----
crankshaft = new Crankshaft(800);
crankshaft.Inertia = 1;
crankshaft.FrictionConstant = 3;
crankshaft.FrictionViscous = 0.2;
// ---- Cylinder geometry ----
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 10;
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
// Firing order 1-3-4-2 with 180° intervals (0°, 180°, 360°, 540°)
double phaseCyl1 = 0.0;
double phaseCyl3 = Math.PI; // 180°
double phaseCyl4 = 2.0 * Math.PI; // 360°
double phaseCyl2 = 3.0 * Math.PI; // 540°
cyl1 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl1,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl2 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl2,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl3 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl3,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl4 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl4,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
solver.AddComponent(cyl1);
solver.AddComponent(cyl2);
solver.AddComponent(cyl3);
solver.AddComponent(cyl4);
double pipeDiameter = 4 * Units.cm;
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
// Sound processors (only one exhaust source now)
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f };
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f };
reverb = new OutdoorExhaustReverb(sampleRate);
// ---- Intake pipe before throttle (shorter, fewer cells) ----
intakePipeBeforeThrottle = new Pipe1D(0.1, pipeArea, 10);
intakePipeBeforeThrottle.Name = "Intake pipe";
solver.AddComponent(intakePipeBeforeThrottle);
// ---- Plenum ----
intakePlenum = new Volume0D(100 * Units.mL, 101325.0, 300.0);
var plenumInlet = intakePlenum.CreatePort(); // port 0
var plenumOut1 = intakePlenum.CreatePort(); // port 1
var plenumOut2 = intakePlenum.CreatePort(); // port 2
var plenumOut3 = intakePlenum.CreatePort(); // port 3
var plenumOut4 = intakePlenum.CreatePort(); // port 4
solver.AddComponent(intakePlenum);
// ---- Intake runners (shorter, fewer cells) ----
runner1 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 1" };
runner2 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 2" };
runner3 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 3" };
runner4 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 4" };
solver.AddComponent(runner1);
solver.AddComponent(runner2);
solver.AddComponent(runner3);
solver.AddComponent(runner4);
// ---- Exhaust collector volume ----
exhaustCollector = new Volume0D(200 * Units.mL, 101325.0, 800.0);
var colIn1 = exhaustCollector.CreatePort(); // cylinder 1 stub
var colIn2 = exhaustCollector.CreatePort(); // cylinder 2 stub
var colIn3 = exhaustCollector.CreatePort(); // cylinder 3 stub
var colIn4 = exhaustCollector.CreatePort(); // cylinder 4 stub
var colOut = exhaustCollector.CreatePort(); // to tailpipe
solver.AddComponent(exhaustCollector);
// ---- Exhaust stub pipes (short connection cylinder → collector) ----
exhStub1 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 1" };
exhStub2 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 2" };
exhStub3 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 3" };
exhStub4 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 4" };
solver.AddComponent(exhStub1);
solver.AddComponent(exhStub2);
solver.AddComponent(exhStub3);
solver.AddComponent(exhStub4);
foreach (var p in new[] { runner1, runner2, runner3, runner4, exhStub1, exhStub2, exhStub3, exhStub4, intakePipeBeforeThrottle })
{
p.DampingMultiplier = 0.5;
p.EnergyRelaxationRate = 0.0;
}
// ---- Tailpipe (single exhaust pipe) ----
tailPipe = new Pipe1D(0.5, pipeArea, 20)
{
Name = "Tailpipe",
DampingMultiplier = 0.5,
EnergyRelaxationRate = 0.0
};
solver.AddComponent(tailPipe);
// ---- Plenum → runner orifices (volume port to pipe left end) ----
plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(plenumToRunner1);
solver.AddOrificeLink(plenumToRunner2);
solver.AddOrificeLink(plenumToRunner3);
solver.AddOrificeLink(plenumToRunner4);
// ---- Intake valves (cylinder port to runner right end) ----
intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(intakeValve1);
solver.AddOrificeLink(intakeValve2);
solver.AddOrificeLink(intakeValve3);
solver.AddOrificeLink(intakeValve4);
// ---- Exhaust valves (cylinder port to stub left end) ----
exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exhStub1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exhStub2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exhStub3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exhStub4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(exhaustValve1);
solver.AddOrificeLink(exhaustValve2);
solver.AddOrificeLink(exhaustValve3);
solver.AddOrificeLink(exhaustValve4);
// ---- Stub → collector orifices (collector port to stub right end) ----
stubToCollector1 = new OrificeLink(colIn1, exhStub1, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector2 = new OrificeLink(colIn2, exhStub2, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector3 = new OrificeLink(colIn3, exhStub3, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector4 = new OrificeLink(colIn4, exhStub4, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(stubToCollector1);
solver.AddOrificeLink(stubToCollector2);
solver.AddOrificeLink(stubToCollector3);
solver.AddOrificeLink(stubToCollector4);
// ---- Collector → tailpipe (collector port to tailpipe left end) ----
collectorToTailpipe = new OrificeLink(colOut, tailPipe, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(collectorToTailpipe);
// ---- Exhaust open end (tailpipe exit) ----
exhaustOpenEnd = new OpenEndLink(tailPipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(exhaustOpenEnd);
// ---- Intake open end ----
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(intakeOpenEnd);
// ---- Throttle ----
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0005, 1.0))
{
DischargeCoefficient = 0.9,
UseInertance = false
};
solver.AddOrificeLink(throttleOrifice);
stepCount = 0;
Console.WriteLine("Inline-4 engine test");
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
Console.WriteLine("Firing order 1-3-4-2, 180° intervals");
}
public override float Process()
{
crankshaft.Step(dt);
cyl1.PreStep(dt);
cyl2.PreStep(dt);
cyl3.PreStep(dt);
cyl4.PreStep(dt);
solver.Step();
stepCount++;
if (stepCount % 10000 == 0)
{
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
Console.WriteLine($"Step {stepCount}, RPM = {rpm:F0}, " +
$"cyl1 P = {cyl1.Pressure / 1e5:F2} bar, " +
$"plenum P = {intakePlenum.Pressure / 1e5:F2} bar");
}
// Sound: only one exhaust source now
float exhaustSound = exhaustSoundProcessor.Process(exhaustOpenEnd);
float intakeSound = intakeSoundProcessor.Process(intakeOpenEnd);
return reverb.Process(exhaustSound * 0.25f + intakeSound);
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
// --- Layout constants ---
float leftMargin = 40f;
float plenumW = 50f, plenumH = 120f;
float cylinderWidth = 60f, cylinderMaxHeight = 180f;
float cylinderSpacing = 90f;
float cylinderTopY = winH * 0.25f;
// Plenum position
float plenumCenterX = leftMargin + plenumW / 2f;
float plenumTopY = cylinderTopY - 20f;
DrawVolume(target, intakePlenum, plenumCenterX, plenumTopY, plenumW, plenumH);
// Throttle symbol (yellow rectangle) left of plenum
float throttleWidth = 8f, throttleHeight = 30f;
float throttleCenterX = leftMargin - 10f;
var throttleRect = new RectangleShape(new Vector2f(throttleWidth, throttleHeight))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleCenterX - throttleWidth / 2f, plenumTopY + plenumH / 2f - throttleHeight / 2f)
};
target.Draw(throttleRect);
// Intake pipe before throttle (left of throttle)
float intakePipeEndX = throttleCenterX - throttleWidth / 2f;
float intakePipeStartX = intakePipeEndX - 100f;
float intakePipeY = plenumTopY + plenumH / 2f;
DrawPipe(target, intakePipeBeforeThrottle, intakePipeY, intakePipeStartX, intakePipeEndX);
// Intake open end marker
var intakeMark = new CircleShape(4f) { FillColor = Color.Magenta };
intakeMark.Position = new Vector2f(intakePipeStartX - 4f, intakePipeY - 4f);
target.Draw(intakeMark);
// Cylinders and runners
float runnerStartX = leftMargin + plenumW;
Cylinder[] cyls = { cyl1, cyl2, cyl3, cyl4 };
Pipe1D[] runners = { runner1, runner2, runner3, runner4 };
for (int i = 0; i < 4; i++)
{
float cylCenterX = runnerStartX + 40f + i * cylinderSpacing;
float runnerEndX = cylCenterX;
DrawPipe(target, runners[i], plenumTopY + plenumH / 2f, runnerStartX, runnerEndX);
DrawCylinder(target, cyls[i], cylCenterX, cylinderTopY, cylinderWidth, cylinderMaxHeight);
}
// Exhaust collector below cylinders
float collectorLeftX = runnerStartX + 40f - cylinderWidth / 2f;
float collectorWidth = 3 * cylinderSpacing + cylinderWidth;
float collectorTopY = cylinderTopY + cylinderMaxHeight + 40f;
float collectorHeight = 50f;
float collectorCenterX = collectorLeftX + collectorWidth / 2f;
DrawVolume(target, exhaustCollector, collectorCenterX, collectorTopY, collectorWidth, collectorHeight);
// Tailpipe from right edge of collector
float tailStartX = collectorLeftX + collectorWidth;
float tailEndX = tailStartX + 150f;
float tailCenterY = collectorTopY + collectorHeight / 2f;
DrawPipe(target, tailPipe, tailCenterY, tailStartX, tailEndX);
// Exhaust open end marker
var exhaustMark = new CircleShape(4f) { FillColor = Color.Magenta };
exhaustMark.Position = new Vector2f(tailEndX - 4f, tailCenterY - 4f);
target.Draw(exhaustMark);
// Exhaust stubs (vertical connections from cylinder bottom to collector)
Pipe1D[] stubs = { exhStub1, exhStub2, exhStub3, exhStub4 };
for (int i = 0; i < 4; i++)
{
float cylCenterX = runnerStartX + 40f + i * cylinderSpacing;
float vertStartY = cylinderTopY + cylinderMaxHeight;
float vertEndY = collectorTopY;
// Draw stub as a vertical pipe
DrawPipeVertical(target, stubs[i], cylCenterX, vertStartY, vertEndY);
}
}
// Helper to draw a pipe vertically (reuse temperature coloring)
private void DrawPipeVertical(RenderWindow target, Pipe1D pipe, float centerX, float topY, float bottomY)
{
int n = pipe.CellCount;
if (n < 2) return;
float pipeLengthPx = bottomY - topY;
float dy = pipeLengthPx / (n - 1);
float baseRadius = 25f;
float rangeFactor = 2f;
float scaleFactor = 2f;
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
var centersY = new float[n];
var radii = new float[n];
var temperatures = new double[n];
double R_gas = 287.0;
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / Math.Max(rho * R_gas, 1e-12);
temperatures[i] = T;
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centersY[i] = topY + i * dy;
}
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
for (int i = 0; i < n; i++)
{
float y = centersY[i];
float r = radii[i];
Color col = TemperatureColor(temperatures[i]);
stripVertices[idx++] = new Vertex(new Vector2f(centerX - r, y), col);
stripVertices[idx++] = new Vertex(new Vector2f(centerX + r, y), col);
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
float st = SmoothStep(0f, 1f, t);
float yi = centersY[i] + (centersY[i + 1] - centersY[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
Color coli = TemperatureColor(Ti);
stripVertices[idx++] = new Vertex(new Vector2f(centerX - ri, yi), coli);
stripVertices[idx++] = new Vertex(new Vector2f(centerX + ri, yi), coli);
}
}
}
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
}
}
}

View File

@@ -1,72 +1,60 @@
using System;
using SFML.Graphics;
using SFML.Graphics;
using SFML.System;
using FluidSim.Core;
using FluidSim.Components;
namespace FluidSim.Tests
{
public abstract class Scenario
{
protected const float AmbientPressure = 101325f;
protected const float AmbientTemperature = 300f;
public float Throttle { get; set; }
public abstract void Initialize(int sampleRate);
public abstract float Process();
public abstract void Draw(RenderWindow target);
protected const double AmbientPressure = 101325.0;
protected const double AmbientTemperature = 300.0;
public double Throttle { get; set; } = 0.0;
// ---------- Color from pressure (volumes) ----------
protected Color PressureColor(double pressurePa)
protected Color PressureColor(float pressurePa)
{
double bar = pressurePa / 1e5; // convert to bar for easier mapping
float bar = pressurePa / 1e5f;
byte r, g, b;
if (bar < 1.0) // vacuum → blue to green
if (bar < 1f)
{
double factor = Math.Clamp(bar, 0.0, 1.0);
r = 0;
g = (byte)(255 * factor);
b = (byte)(255 * (1.0 - factor));
}
else // above ambient → green to red
{
double factor = Math.Min((bar - 1.0) / 9.0, 1.0); // 1→10 bar maps to 0→1
r = (byte)(255 * factor);
g = (byte)(255 * (1.0 - factor));
b = 0;
}
return new Color(r, g, b);
}
// ---------- Color from temperature (pipes) ----------
protected Color TemperatureColor(double temperature)
{
double t = Math.Clamp(temperature, 0.0, 2000.0);
byte r, g, b;
if (t < AmbientTemperature)
{
double factor = t / AmbientTemperature;
r = 0;
g = (byte)(255 * factor);
b = (byte)(255 * (1.0 - factor));
float f = Math.Clamp(bar, 0f, 1f);
r = 0; g = (byte)(255 * f); b = (byte)(255 * (1 - f));
}
else
{
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature);
r = (byte)(255 * factor);
g = (byte)(255 * (1.0 - factor));
b = 0;
float f = Math.Min((bar - 1f) / 9f, 1f);
r = (byte)(255 * f); g = (byte)(255 * (1 - f)); b = 0;
}
return new Color(r, g, b);
}
protected Color TemperatureColor(float t)
{
t = Math.Clamp(t, 0f, 2000f);
byte r, g, b;
if (t < AmbientTemperature)
{
float f = t / AmbientTemperature;
r = 0; g = (byte)(255 * f); b = (byte)(255 * (1 - f));
}
else
{
float f = (t - AmbientTemperature) / (2000f - AmbientTemperature);
r = (byte)(255 * f); g = (byte)(255 * (1 - f)); b = 0;
}
return new Color(r, g, b);
}
// ---------- Draw a generic volume (e.g. plenum) ----------
protected void DrawVolume(RenderWindow target, Volume0D volume,
float centerX, float topY, float width, float height)
{
var rect = new RectangleShape(new Vector2f(width, height))
{
FillColor = PressureColor(volume.Pressure), // ← pressurebased
FillColor = PressureColor(volume.Pressure),
Position = new Vector2f(centerX - width / 2f, topY)
};
target.Draw(rect);
@@ -75,122 +63,99 @@ namespace FluidSim.Tests
FillColor = Color.Transparent,
OutlineColor = Color.White,
OutlineThickness = 1f,
Position = new Vector2f(centerX - width / 2f, topY)
Position = rect.Position
};
target.Draw(border);
}
// ---------- Draw an engine cylinder ----------
protected void DrawCylinder(RenderWindow target, Cylinder cylinder,
float centerX, float topY, float width, float maxHeight)
{
double fraction = cylinder.PistonFraction;
float currentHeight = (float)(maxHeight * fraction);
// Walls
var wall = new RectangleShape(new Vector2f(width, maxHeight));
wall.FillColor = new Color(60, 60, 60);
wall.Position = new Vector2f(centerX - width / 2f, topY);
float fraction = cylinder.PistonFraction;
float currentHeight = maxHeight * fraction;
var wall = new RectangleShape(new Vector2f(width, maxHeight))
{
FillColor = new Color(60, 60, 60),
Position = new Vector2f(centerX - width / 2f, topY)
};
target.Draw(wall);
// Gas colored by pressure now
float gasTop = topY;
var gasRect = new RectangleShape(new Vector2f(width, currentHeight));
gasRect.FillColor = PressureColor(cylinder.Pressure); // ← pressurebased
gasRect.Position = new Vector2f(centerX - width / 2f, gasTop);
target.Draw(gasRect);
// Piston line
var pistonLine = new RectangleShape(new Vector2f(width, 4f));
pistonLine.FillColor = Color.White;
pistonLine.Position = new Vector2f(centerX - width / 2f, topY + currentHeight);
target.Draw(pistonLine);
// Valve indicators
var gas = new RectangleShape(new Vector2f(width, currentHeight))
{
FillColor = PressureColor(cylinder.Pressure),
Position = new Vector2f(centerX - width / 2f, topY)
};
target.Draw(gas);
var piston = new RectangleShape(new Vector2f(width, 4f))
{
FillColor = Color.White,
Position = new Vector2f(centerX - width / 2f, topY + currentHeight)
};
target.Draw(piston);
float valveW = 6f, valveH = 10f, valveY = topY + 4f;
var intakeValve = new RectangleShape(new Vector2f(valveW, valveH));
intakeValve.FillColor = cylinder.IntakeValveArea > 0 ? Color.Green : Color.Red;
intakeValve.Position = new Vector2f(centerX - width / 2f - valveW - 2f, valveY);
target.Draw(intakeValve);
var exhaustValve = new RectangleShape(new Vector2f(valveW, valveH));
exhaustValve.FillColor = cylinder.ExhaustValveArea > 0 ? Color.Green : Color.Red;
exhaustValve.Position = new Vector2f(centerX + width / 2f + 2f, valveY);
target.Draw(exhaustValve);
var iv = new RectangleShape(new Vector2f(valveW, valveH))
{
FillColor = cylinder.IntakeValveArea > 0f ? Color.Green : Color.Red,
Position = new Vector2f(centerX - width / 2f - valveW - 2f, valveY)
};
target.Draw(iv);
var ev = new RectangleShape(new Vector2f(valveW, valveH))
{
FillColor = cylinder.ExhaustValveArea > 0f ? Color.Green : Color.Red,
Position = new Vector2f(centerX + width / 2f + 2f, valveY)
};
target.Draw(ev);
}
// ---------- Draw a pipe (unchanged) ----------
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
protected void DrawPipe(RenderWindow target, PipeSystem pipeSystem, int pipeIndex,
float pipeCenterY, float pipeStartX, float pipeEndX)
{
int n = pipe.CellCount;
int start = pipeSystem.GetPipeStart(pipeIndex);
int end = pipeSystem.GetPipeEnd(pipeIndex);
int n = end - start;
if (n < 2) return;
float pipeLengthPx = pipeEndX - pipeStartX;
float dx = pipeLengthPx / (n - 1);
float pipeLen = pipeEndX - pipeStartX;
float dx = pipeLen / (n - 1);
float baseRadius = 25f;
float rangeFactor = 2f;
float scaleFactor = 2f;
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
var centers = new float[n];
var radii = new float[n];
var temperatures = new double[n];
double R_gas = 287.0;
var temps = new float[n];
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / Math.Max(rho * R_gas, 1e-12);
temperatures[i] = T;
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
int cell = start + i;
float p = pipeSystem.GetCellPressure(cell);
float rho = pipeSystem.GetCellDensity(cell);
temps[i] = p / MathF.Max(rho * 287f, 1e-12f);
float dev = MathF.Tanh((p - AmbientPressure) / AmbientPressure * 0.5f);
radii[i] = baseRadius * (1f + dev * 2f);
if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx;
}
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
int segments = 8;
var va = new VertexArray(PrimitiveType.TriangleStrip);
for (int i = 0; i < n; i++)
{
float x = centers[i];
float r = radii[i];
Color col = TemperatureColor(temperatures[i]); // pipes still use temperature
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col);
float x = centers[i], r = radii[i];
Color col = TemperatureColor(temps[i]);
va.Append(new Vertex(new Vector2f(x, pipeCenterY - r), col));
va.Append(new Vertex(new Vector2f(x, pipeCenterY + r), col));
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
for (int s = 1; s <= segments; s++)
{
float t = s / (float)segmentsPerCell;
float st = SmoothStep(0f, 1f, t);
float t = s / (float)segments;
float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
Color coli = TemperatureColor(Ti);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
float ri = radii[i] + (radii[i + 1] - radii[i]) * t;
float Ti = temps[i] + (temps[i + 1] - temps[i]) * t;
Color colS = TemperatureColor(Ti);
va.Append(new Vertex(new Vector2f(xi, pipeCenterY - ri), colS));
va.Append(new Vertex(new Vector2f(xi, pipeCenterY + ri), colS));
}
}
}
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
target.Draw(va);
}
}
}

View File

@@ -0,0 +1,220 @@
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Interfaces;
using SFML.Graphics;
using SFML.System;
using System;
namespace FluidSim.Tests
{
public class SingleCylScenario : Scenario
{
private Crankshaft crankshaft;
private Cylinder cylinder;
private PipeSystem pipeSystem;
private BoundarySystem boundaries;
private Solver solver;
private Volume0D intakePlenum;
private Port plenumInlet, plenumOutlet;
private Volume0D exhaustCollector;
private Port colIn, colOut;
private int throttleAreaIdx, plenumRunnerAreaIdx, intakeValveIdx, exhaustValveIdx;
private float[] orificeAreas;
private int intakeOpenIdx, exhaustOpenIdx;
private SoundProcessor exhaustSound, intakeSound;
private OutdoorExhaustReverb reverb;
private double dt;
private int stepCount;
public float MaxThrottleArea = 1e-4f; // 1 cm²
// pipe area for open end calculations
private float pipeArea;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// ---- Crankshaft ----
crankshaft = new Crankshaft(600);
crankshaft.Inertia = 0.2f;
crankshaft.FrictionConstant = 2f;
crankshaft.FrictionViscous = 0.04f;
// ---- Cylinder ----
float bore = 0.056f, stroke = 0.057f, conRod = 0.110f, compRatio = 9.2f;
float ivo = 350f, ivc = 580f, evo = 120f, evc = 370f;
cylinder = new Cylinder(bore, stroke, conRod, compRatio,
ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 0.03f,
IntakeValveLift = 0.005f,
ExhaustValveDiameter = 0.028f,
ExhaustValveLift = 0.005f
};
// ---- Pipe system ----
int totalCells = 10 + 10 + 50;
int[] pipeStart = { 0, 10, 20 };
int[] pipeEnd = { 10, 20, 70 };
float[] area = new float[totalCells];
float[] dx = new float[totalCells];
float pipeDiameter = 0.02f; // 2 cm
pipeArea = MathF.PI * 0.25f * pipeDiameter * pipeDiameter;
float areaVal = pipeArea;
float intakeLenBefore = 0.2f, intakeLenRunner = 0.2f, exhaustLen = 0.5f;
for (int i = 0; i < totalCells; i++)
{
area[i] = areaVal;
if (i < 10) dx[i] = intakeLenBefore / 10f;
else if (i < 20) dx[i] = intakeLenRunner / 10f;
else dx[i] = exhaustLen / 50f;
}
pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx,
1.225f, 0f, 101325f);
pipeSystem.DampingMultiplier = 0.5f;
pipeSystem.EnergyRelaxationRate = 0f;
pipeSystem.AmbientPressure = 101325f;
// ---- Volumes ----
intakePlenum = new Volume0D(5e-6f, 101325f, 300f); // 5 mL
plenumInlet = intakePlenum.CreatePort();
plenumOutlet = intakePlenum.CreatePort();
exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // 10 mL (unused but present)
colIn = exhaustCollector.CreatePort();
colOut = exhaustCollector.CreatePort();
// ---- Boundary system ----
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2);
throttleAreaIdx = 0;
plenumRunnerAreaIdx = 1;
intakeValveIdx = 2;
exhaustValveIdx = 3;
// Intake open end (pipe0 left)
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, pipeArea);
intakeOpenIdx = 0;
// Throttle orifice (plenum inlet to pipe0 right)
boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false, throttleAreaIdx, 0.2f);
// Plenum to runner (plenum outlet to pipe1 left)
boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true, plenumRunnerAreaIdx, 1f);
// Intake valve (cylinder intake to pipe1 right)
boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false, intakeValveIdx, 1f);
// Exhaust valve (cylinder exhaust to pipe2 left)
boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true, exhaustValveIdx, 1f);
// Exhaust open end (pipe2 right)
boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, pipeArea);
exhaustOpenIdx = 1;
orificeAreas = new float[4];
orificeAreas[plenumRunnerAreaIdx] = areaVal; // fixed plenum->runner area
// ---- Solver ----
solver = new Solver { SubStepCount = 4, EnableProfiling = false };
solver.SetTimeStep(dt);
solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
solver.AddComponent(cylinder);
solver.AddComponent(intakePlenum);
solver.AddComponent(exhaustCollector);
// ---- Sound ----
exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 1f };
intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 1f };
reverb = new OutdoorExhaustReverb(sampleRate);
stepCount = 0;
Console.WriteLine("TestScenario ready.");
}
public override float Process()
{
crankshaft.Step((float)dt);
cylinder.PreStep((float)dt);
// Update variable orifice areas
float throttledArea = MaxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1f);
orificeAreas[throttleAreaIdx] = throttledArea;
orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea;
orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea;
boundaries.SetOrificeAreas(orificeAreas);
solver.Step();
stepCount++;
// Retrieve openend mass flows for sound synthesis
float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx);
float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx);
float exhaustDry = exhaustSound.Process(exhaustFlow);
float intakeDry = intakeSound.Process(intakeFlow);
if (stepCount % 1000 == 0)
{
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
Console.WriteLine($"Step {stepCount}, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar");
Console.WriteLine($"intake flow: {intakeFlow:F12}, exhaust flow: {exhaustFlow:F16}");
}
return reverb.Process(intakeDry);
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float intakeY = winH / 2f - 40f;
float exhaustY = winH / 2f + 80f;
float openEndX = 40f;
// Intake pipe before throttle (pipe 0)
float pipe1StartX = openEndX;
float pipe1EndX = pipe1StartX + 120f;
DrawPipe(target, pipeSystem, 0, intakeY, pipe1StartX, pipe1EndX);
// Throttle symbol
float throttleX = pipe1EndX + 5f;
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleX, intakeY - 15f)
};
target.Draw(throttleRect);
// Plenum
float plenW = 60f, plenH = 80f;
float plenLeftX = throttleX + 10f;
float plenCenterX = plenLeftX + plenW / 2f;
float plenTopY = intakeY - plenH / 2f;
DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH);
// Runner pipe (pipe 1)
float runnerStartX = plenLeftX + plenW + 5f;
float runnerEndX = runnerStartX + 100f;
DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerEndX);
// Cylinder
float cylCX = runnerEndX + 50f;
float cylTopY = intakeY - 120f;
float cylW = 80f, cylMaxH = 240f;
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
// Exhaust pipe (pipe 2)
float exhStartX = cylCX + cylW / 2f + 20f;
float exhEndX = winW - 60f;
DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, exhEndX);
}
}
}

View File

@@ -1,176 +1,91 @@
using System;
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Utils;
namespace FluidSim.Tests
{
public class TestScenario : Scenario
{
// Engine
private Cylinder cylinder;
private Crankshaft crankshaft;
// Intake side
private Pipe1D intakePipeBeforeThrottle;
private Volume0D intakePlenum; // 5 mL
private Pipe1D intakeRunner;
// Exhaust side
private Pipe1D exhaustPipe;
// Links
private OpenEndLink intakeOpenEnd;
private OrificeLink throttleOrifice;
private OrificeLink plenumToRunner;
private OrificeLink intakeValve;
private OrificeLink exhaustValve;
private OpenEndLink exhaustOpenEnd;
private PipeSystem pipeSystem;
private BoundarySystem boundaries;
private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private int[] pipeStart = { 0 };
private int[] pipeEnd;
private double dt;
private int stepCount;
// ---------- Throttle control ----------
public double MaxThrottleArea { get; set; } = 1 * Units.cm2; // 2 cm²
// Sound output: use pressure at open end
private SoundProcessor openEndSound;
private int openEndIdx = 0; // index of the open end in BoundarySystem (we added only one)
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
solver = new Solver();
const int cellCount = 200;
float length = 2f;
float dia = 0.02f;
float area = MathF.PI * 0.25f * dia * dia;
float[] areas = new float[cellCount];
float[] dxs = new float[cellCount];
float dx = length / cellCount;
for (int i = 0; i < cellCount; i++)
{
areas[i] = area;
dxs[i] = dx;
}
pipeEnd = new[] { cellCount };
float rho0 = 101325f / (287f * 300f);
pipeSystem = new PipeSystem(cellCount, pipeStart, pipeEnd, areas, dxs,
rho0, 0f, 101325f);
pipeSystem.DampingMultiplier = 0f;
pipeSystem.EnergyRelaxationRate = 0f;
pipeSystem.AmbientPressure = 101325f;
// Pressure bubble near right end
float pBubble = 10f * 101325f;
float TBubble = 2000f;
float rhoBubble = pBubble / (287f * TBubble);
for (int i = 0; i <= 10; i++)
pipeSystem.SetCellState(i, rhoBubble, 0f, pBubble);
// Boundaries: left closed, right open
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 1, maxOpenEnds: 1);
boundaries.AddOrifice(null, pipeIndex: 0, isLeftEnd: true, areaIndex: 0, 1f);
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: false, 101325f, area);
float[] orificeAreas = new float[1] { 0f };
boundaries.SetOrificeAreas(orificeAreas);
solver = new Solver { SubStepCount = 3};
solver.SetTimeStep(dt);
solver.CflTarget = 0.9;
solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
// ---- Crankshaft (external, passed to cylinder) ----
crankshaft = new Crankshaft(600);
crankshaft.Inertia = 0.2;
crankshaft.FrictionConstant = 2;
crankshaft.FrictionViscous = 0.04;
solver.EnableProfiling = true;
pipeSystem.EnableProfiling = true;
// ---- Cylinder ----
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2;
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm, // 30 mm
IntakeValveLift = 5 * Units.mm, // 5 mm
ExhaustValveDiameter = 28 * Units.mm, // 28 mm
ExhaustValveLift = 5 * Units.mm // 5 mm
};
solver.AddComponent(cylinder);
double pipeDiameter = 2 * Units.cm;
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
reverb = new OutdoorExhaustReverb(sampleRate);
// ---- Pipes ----
intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10);
intakeRunner = new Pipe1D(0.2, pipeArea, 10);
exhaustPipe = new Pipe1D(0.5, pipeArea, 50);
solver.AddComponent(intakePipeBeforeThrottle);
solver.AddComponent(intakeRunner);
solver.AddComponent(exhaustPipe);
intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0);
var plenumInlet = intakePlenum.CreatePort();
var plenumOutlet = intakePlenum.CreatePort();
solver.AddComponent(intakePlenum);
// ---- Intake open end ----
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(intakeOpenEnd);
// ---- Throttle orifice (variable area) ----
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0001, 1))
{
DischargeCoefficient = 0.2,
UseInertance = false
};
solver.AddOrificeLink(throttleOrifice);
// ---- Plenum to runner (fixed area) ----
plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true,
areaProvider: () => pipeArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(plenumToRunner);
// ---- Intake valve ----
intakeValve = new OrificeLink(cylinder.IntakePort, intakeRunner, isPipeLeftEnd: false,
areaProvider: () => cylinder.IntakeValveArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(intakeValve);
// ---- Exhaust valve ----
exhaustValve = new OrificeLink(cylinder.ExhaustPort, exhaustPipe, isPipeLeftEnd: true,
areaProvider: () => cylinder.ExhaustValveArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(exhaustValve);
// ---- Exhaust open end ----
exhaustOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(exhaustOpenEnd);
// Simple sound processor: convert mass flow rate to audio
openEndSound = new SoundProcessor(sampleRate, 1f) { Gain = 2f };
Console.WriteLine("Pulse test ready.");
stepCount = 0;
Console.WriteLine("4Stroke engine test (plenum + two pipes)");
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)");
}
public override float Process()
{
cylinder.Crankshaft.Step(dt);
cylinder.PreStep(dt);
solver.Step();
stepCount++;
if (stepCount % 10000 == 0)
{
double crankDeg = cylinder.Crankshaft.CrankAngle * 180.0 / Math.PI % 720.0;
double cylP = cylinder.Pressure / 1e5;
double cylT = cylinder.Temperature;
double cylMass = cylinder.Mass * 1e6;
double mdotI = intakeValve.LastMassFlowRate;
double mdotE = exhaustValve.LastMassFlowRate;
double pipeR = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
double plenumP = intakePlenum.Pressure / 1e5;
double actualArea = MaxThrottleArea * Throttle;
float flow = boundaries.GetOpenEndMassFlow(openEndIdx);
float sample = openEndSound.Process(flow);
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " +
$"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " +
$"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar");
Console.WriteLine($"Throttle = {Throttle * 100:F0}% area = {actualArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
}
float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd);
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
return reverb.Process(exhaustDry + intakeDry);
return sample;
}
public override void Draw(RenderWindow target)
@@ -178,56 +93,10 @@ namespace FluidSim.Tests
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float intakeY = winH / 2f - 40f;
float exhaustY = winH / 2f + 80f;
// Open end marker
float openEndX = 40f;
var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan };
openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f);
target.Draw(openEndMark);
// First intake pipe
float pipe1StartX = openEndX;
float pipe1EndX = pipe1StartX + 120f;
DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX);
// Throttle symbol
float throttleX = pipe1EndX + 5f;
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleX, intakeY - 15f)
};
target.Draw(throttleRect);
// Plenum
float plenW = 60f, plenH = 80f;
float plenLeftX = throttleX + 10f;
float plenCenterX = plenLeftX + plenW / 2f;
float plenTopY = intakeY - plenH / 2f;
DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH);
// Runner pipe
float runnerStartX = plenLeftX + plenW + 5f;
float runnerEndX = runnerStartX + 100f;
DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX);
// Cylinder
float cylCX = runnerEndX + 50f;
float cylTopY = intakeY - 120f;
float cylW = 80f, cylMaxH = 240f;
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
// Exhaust pipe
float exhStartX = cylCX + cylW / 2f + 20f;
float exhEndX = winW - 60f;
DrawPipe(target, exhaustPipe, exhaustY, exhStartX, exhEndX);
// Exhaust open end marker
var exhOpenEndMark = new CircleShape(5f) { FillColor = Color.Magenta };
exhOpenEndMark.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f);
target.Draw(exhOpenEndMark);
float startX = 50f;
float endX = winW - 50f;
float y = winH / 2f;
DrawPipe(target, pipeSystem, 0, y, startX, endX);
}
}
}