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 namespace FluidSim.Components
{ {
/// <summary>
/// Represents the ambient atmosphere constant pressure/temperature reservoir.
/// </summary>
public class Atmosphere : IComponent public class Atmosphere : IComponent
{ {
public double Pressure { get; set; } = 101325.0; public float Pressure { get; set; } = 101325f;
public double Temperature { get; set; } = 300.0; public float Temperature { get; set; } = 300f;
public double GasConstant { get; set; } = 287.0; public float GasConstant { get; set; } = 287f;
public double Gamma => 1.4; public float Gamma => 1.4f;
public double Density => Pressure / (GasConstant * Temperature); public float Density => Pressure / (GasConstant * Temperature);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density; public float SpecificEnthalpy => Gamma / (Gamma - 1f) * Pressure / Density;
public Port Port { get; } public Port Port { get; }
@@ -25,9 +22,8 @@ namespace FluidSim.Components
public IReadOnlyList<Port> Ports => new[] { Port }; 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(); UpdatePort();
} }
@@ -37,7 +33,7 @@ namespace FluidSim.Components
Port.Density = Density; Port.Density = Density;
Port.Temperature = Temperature; Port.Temperature = Temperature;
Port.SpecificEnthalpy = SpecificEnthalpy; Port.SpecificEnthalpy = SpecificEnthalpy;
Port.AirFraction = 1.0; Port.AirFraction = 1f;
} }
} }
} }

View File

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

View File

@@ -13,144 +13,103 @@ namespace FluidSim.Components
private readonly Port[] _ports; private readonly Port[] _ports;
IReadOnlyList<Port> IComponent.Ports => _ports; IReadOnlyList<Port> IComponent.Ports => _ports;
// Geometry public float Bore { get; }
public double Bore { get; } public float Stroke { get; }
public double Stroke { get; } public float ConRodLength { get; }
public double ConRodLength { get; } public float CompressionRatio { get; }
public double CompressionRatio { get; }
// Valve timings (degrees, 0 = TDC compression, 720° full cycle) public float IVO, IVC, EVO, EVC; // degrees
public double IVO { get; } public float IntakeValveDiameter = 0.03f;
public double IVC { get; } public float ExhaustValveDiameter = 0.028f;
public double EVO { get; } public float IntakeValveLift = 0.005f;
public double EVC { get; } public float ExhaustValveLift = 0.005f;
// Valve geometry public float IntakeValveMaxArea => MathF.PI * IntakeValveDiameter * IntakeValveLift;
public double IntakeValveDiameter { get; set; } = 0.030; public float ExhaustValveMaxArea => MathF.PI * ExhaustValveDiameter * ExhaustValveLift;
public double ExhaustValveDiameter { get; set; } = 0.028;
public double IntakeValveLift { get; set; } = 0.005;
public double ExhaustValveLift { get; set; } = 0.005;
public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift; public float SparkAdvance = 20f;
public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift; 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 float PhaseOffset; // rad
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;
// Fuel public float Volume => cylinderVolume;
public double StoichiometricAFR { get; set; } = 14.7; public float Pressure => (Gamma - 1f) * cylinderEnergy / MathF.Max(cylinderVolume, 1e-12f);
public double FuelLowerHeatingValue { get; set; } = 44e6; 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 private float cylinderVolume, cylinderEnergy;
public double EnergyVariationFraction { get; set; } = 0.05; private float _airMass, _exhaustMass;
public double MisfireProbability { get; set; } = 0.01; private float trappedAirMass, fuelMass, burnFraction;
private bool combustionActive, fuelInjected;
// Heat loss private float _energyFactor = 1f;
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 readonly Random _random = new Random(); private readonly Random _random = new Random();
private const double Gamma = 1.4; private const float Gamma = 1.4f;
private const double GasConstant = 287.0; private const float GasConstant = 287f;
private const float MaxPressurePa = 200e5f;
private const float MaxTemperatureK = 3500f;
private const double MaxPressurePa = 200e5; public Cylinder(float bore, float stroke, float conRodLength, float compressionRatio,
private const double MaxTemperatureK = 3500.0; float ivo, float ivc, float evo, float evc, Crankshaft crankshaft)
public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
double ivo, double ivc, double evo, double evc, Crankshaft crankshaft)
{ {
Bore = bore; Bore = bore; Stroke = stroke; ConRodLength = conRodLength;
Stroke = stroke;
ConRodLength = conRodLength;
CompressionRatio = compressionRatio; CompressionRatio = compressionRatio;
IVO = ivo; IVO = ivo; IVC = ivc; EVO = evo; EVC = evc;
IVC = ivc;
EVO = evo;
EVC = evc;
Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft)); Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft));
cylinderVolume = clearanceVolume; cylinderVolume = clearanceVolume;
double initRho = 1.225; float initRho = 1.225f;
_airMass = initRho * clearanceVolume; _airMass = initRho * clearanceVolume;
_exhaustMass = 0.0; _exhaustMass = 0f;
cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0); cylinderEnergy = 101325f * clearanceVolume / (Gamma - 1f);
IntakePort = new Port { Owner = this }; IntakePort = new Port { Owner = this };
ExhaustPort = new Port { Owner = this }; ExhaustPort = new Port { Owner = this };
_ports = new[] { IntakePort, ExhaustPort }; _ports = new[] { IntakePort, ExhaustPort };
} }
// Derived volumes private float SweptVolume => MathF.PI * 0.25f * Bore * Bore * Stroke;
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke; private float clearanceVolume => SweptVolume / (CompressionRatio - 1f);
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0); private float CrankRadius => Stroke * 0.5f;
private double CrankRadius => Stroke / 2.0; private float Obliquity => CrankRadius / ConRodLength;
private double Obliquity => CrankRadius / ConRodLength;
// Offset-aware crank angle in degrees private float CrankDeg =>
private double CrankDeg => ((Crankshaft.CrankAngle + PhaseOffset) % (4f * MathF.PI)) * 180f / MathF.PI % 720f;
((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
public double ComputeVolume(double thetaRad) public float ComputeVolume(float thetaRad)
{ {
double r = CrankRadius; float r = CrankRadius, l = ConRodLength;
double l = ConRodLength; float cosTh = MathF.Cos(thetaRad), sinTh = MathF.Sin(thetaRad);
double cosTh = Math.Cos(thetaRad); float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh);
double sinTh = Math.Sin(thetaRad); float x = r * (1f - cosTh) + l * (1f - term);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh); float area = MathF.PI * 0.25f * Bore * Bore;
double x = r * (1.0 - cosTh) + l * (1.0 - term);
double area = Math.PI * 0.25 * Bore * Bore;
return clearanceVolume + area * x; 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; float deg = thetaDeg % 720f;
if (deg < 0) deg += 720.0; if (deg < 0f) deg += 720f;
float duration = closes - opens;
if (duration <= 0f) return 0f;
double duration = closes - opens; float rampDur = duration * 0.25f;
if (duration <= 0) return 0.0; float holdDur = duration - 2f * rampDur;
double rampDur = duration * 0.25;
double holdDur = duration - 2.0 * rampDur;
if (deg >= opens && deg < opens + rampDur) if (deg >= opens && deg < opens + rampDur)
{ {
double t = (deg - opens) / rampDur; float t = (deg - opens) / rampDur;
return peakLift * t * t * (3.0 - 2.0 * t); return peakLift * t * t * (3f - 2f * t);
} }
else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur) else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur)
{ {
@@ -158,54 +117,45 @@ namespace FluidSim.Components
} }
else if (deg >= opens + rampDur + holdDur && deg <= closes) else if (deg >= opens + rampDur + holdDur && deg <= closes)
{ {
double t = (deg - (opens + rampDur + holdDur)) / rampDur; float t = (deg - (opens + rampDur + holdDur)) / rampDur;
return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t); return peakLift * (1f - t) * (1f - t) * (1f + 2f * t);
} }
return 0.0; return 0f;
} }
public double IntakeValveArea => public float IntakeValveArea =>
Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift); MathF.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
public float ExhaustValveArea =>
MathF.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
public double ExhaustValveArea => private float Wiebe(float angleSinceSpark)
Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
private double Wiebe(double angleSinceSpark)
{ {
if (angleSinceSpark < WiebeStart) return 0.0; if (angleSinceSpark < WiebeStart) return 0f;
double phi = (angleSinceSpark - WiebeStart) / WiebeDuration; float phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
if (phi <= 0) return 0.0; if (phi <= 0f) return 0f;
return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1)); return 1f - MathF.Exp(-WiebeA * MathF.Pow(phi, WiebeM + 1f));
} }
public void PreStep(double dt) public void PreStep(float dt)
{ {
double prevVolume = cylinderVolume; float prevVolume = cylinderVolume;
float crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
// ----- Use phaseoffset crank angle for this cylinder -----
double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
cylinderVolume = ComputeVolume(crankAngleRad); cylinderVolume = ComputeVolume(crankAngleRad);
double dV = cylinderVolume - prevVolume; float dV = cylinderVolume - prevVolume;
float pRel = Pressure - 101325f;
// Piston torque float sinTh = MathF.Sin(crankAngleRad), cosTh = MathF.Cos(crankAngleRad);
double pRel = Pressure - 101325.0; float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh);
double sinTh = Math.Sin(crankAngleRad); float dxdtheta = CrankRadius * sinTh * (1f + Obliquity * cosTh / term);
double cosTh = Math.Cos(crankAngleRad); float pistonArea = MathF.PI * 0.25f * Bore * Bore;
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh); Crankshaft.AddTorque(pRel * pistonArea * dxdtheta);
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);
cylinderEnergy -= Pressure * dV; cylinderEnergy -= Pressure * dV;
// Also use offset angle for event detection float prevDeg = (Crankshaft.PreviousAngle + PhaseOffset) * 180f / MathF.PI % 720f;
double crankshaftPrevAngle = Crankshaft.PreviousAngle; float currDeg = crankAngleRad * 180f / MathF.PI % 720f;
double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0;
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
// ----- Intake closing: capture trapped air mass and compute fuel ----- // Intake closing
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC) if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
{ {
trappedAirMass = _airMass; trappedAirMass = _airMass;
@@ -213,122 +163,103 @@ namespace FluidSim.Components
fuelInjected = true; fuelInjected = true;
} }
// ----- Spark ignition ----- // Spark
double sparkAngle = 0.0 - SparkAdvance; float sparkAngle = 0f - SparkAdvance;
if (sparkAngle < 0) sparkAngle += 720.0; if (sparkAngle < 0f) sparkAngle += 720f;
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) || bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle); (prevDeg > sparkAngle + 360f && currDeg < sparkAngle);
if (crossedSpark && !combustionActive && fuelInjected) if (crossedSpark && !combustionActive && fuelInjected)
{ {
bool misfire = _random.NextDouble() < MisfireProbability; if (_random.NextDouble() < MisfireProbability)
if (misfire)
{ {
combustionActive = false; combustionActive = false;
} }
else else
{ {
combustionActive = true; combustionActive = true; burnFraction = 0f;
burnFraction = 0.0; float range = EnergyVariationFraction;
double range = EnergyVariationFraction; _energyFactor = 1f + range * (2f * (float)_random.NextDouble() - 1f);
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
} }
} }
// ----- Combustion progress ----- // Combustion
if (combustionActive) if (combustionActive)
{ {
double angleSinceSpark = currDeg - sparkAngle; float angleSinceSpark = currDeg - sparkAngle;
if (angleSinceSpark < 0) angleSinceSpark += 720.0; if (angleSinceSpark < 0f) angleSinceSpark += 720f;
double newFraction = Wiebe(angleSinceSpark); float newFraction = Wiebe(angleSinceSpark);
if (newFraction >= 1f || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
{ {
newFraction = 1.0; newFraction = 1f; combustionActive = false;
combustionActive = false; float totalMass = _airMass + _exhaustMass;
double totalMass = _airMass + _exhaustMass; _airMass = 0f; _exhaustMass = totalMass;
_airMass = 0.0;
_exhaustMass = totalMass;
} }
double dFraction = newFraction - burnFraction; float dFraction = newFraction - burnFraction;
if (dFraction > 0) if (dFraction > 0f)
{ {
double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction; float dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
cylinderEnergy += dQ; cylinderEnergy += dQ;
_exhaustMass += fuelMass * dFraction; _exhaustMass += fuelMass * dFraction;
burnFraction = newFraction; burnFraction = newFraction;
} }
} }
// ----- Heat loss ----- // Heat loss
double dQ_loss = HeatTransferCoefficient * CylinderWallArea * float dQ_loss = HeatTransferCoefficient * CylinderWallArea *
(Temperature - AmbientTemperature) * dt; (Temperature - AmbientTemperature) * dt;
cylinderEnergy -= dQ_loss; cylinderEnergy -= dQ_loss;
// Update port states // Update port states
double p = Pressure, rho = Density, T = Temperature; float p = Pressure, rho = Density, T = Temperature;
double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12); float h = Gamma / (Gamma - 1f) * p / MathF.Max(rho, 1e-12f);
double af = AirFraction; float af = AirFraction;
IntakePort.Pressure = p; IntakePort.Density = rho;
IntakePort.Pressure = p; IntakePort.Temperature = T; IntakePort.SpecificEnthalpy = h; IntakePort.AirFraction = af;
IntakePort.Density = rho; ExhaustPort.Pressure = p; ExhaustPort.Density = rho;
IntakePort.Temperature = T; ExhaustPort.Temperature = T; ExhaustPort.SpecificEnthalpy = h; ExhaustPort.AirFraction = af;
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) foreach (var port in _ports)
{ {
double mdot = port.MassFlowRate; float mdot = port.MassFlowRate;
double af = mdot >= 0 ? port.AirFraction : AirFraction; float af = mdot >= 0f ? port.AirFraction : AirFraction;
dmAir += mdot * af * dt; dmAir += mdot * af * dt;
dmExhaust += mdot * (1.0 - af) * dt; dmExhaust += mdot * (1f - af) * dt;
dE += mdot * port.SpecificEnthalpy * dt; dE += mdot * port.SpecificEnthalpy * dt;
} }
_airMass += dmAir; _airMass += dmAir; _exhaustMass += dmExhaust;
_exhaustMass += dmExhaust;
cylinderEnergy += dE; 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; float currentRho = (_airMass + _exhaustMass) / V;
if (currentP > MaxPressurePa) float currentT = currentP / MathF.Max(currentRho * GasConstant, 1e-12f);
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
double currentRho = (_airMass + _exhaustMass) / V;
double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
if (currentT > MaxTemperatureK) if (currentT > MaxTemperatureK)
{ {
double pAtTlimit = currentRho * GasConstant * MaxTemperatureK; float pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
cylinderEnergy = pAtTlimit * V / (Gamma - 1.0); cylinderEnergy = pAtTlimit * V / (Gamma - 1f);
} }
double totalMass = _airMass + _exhaustMass; float totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9) if (totalMass < 1e-9f)
{ {
_airMass = 1e-9; _airMass = 1e-9f; _exhaustMass = 0f;
_exhaustMass = 0.0; cylinderEnergy = 101325f * V / (Gamma - 1f);
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
} }
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 (_airMass < 0f) _airMass = 0f;
if (_exhaustMass < 0.0) _exhaustMass = 0.0; 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>(); public List<Port> Ports { get; } = new List<Port>();
private double _airMass; private float _airMass;
private double _exhaustMass; private float _exhaustMass;
public double InternalEnergy { get; set; } public float InternalEnergy;
public double Volume { get; set; } public float Volume;
public double Dvdt { get; set; } public float Dvdt;
public double Gamma { get; set; } = 1.4; public float Gamma { get; set; } = 1.4f;
public double GasConstant { get; set; } = 287.0; 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 float Mass => _airMass + _exhaustMass;
public double Mass => _airMass + _exhaustMass; public float AirFraction => _airMass / MathF.Max(Mass, 1e-12f);
public double AirFraction => _airMass / Math.Max(Mass, 1e-12); public float Density => Mass / MathF.Max(Volume, 1e-12f);
public double Density => Mass / Math.Max(Volume, 1e-12); public float Pressure => (Gamma - 1f) * InternalEnergy / MathF.Max(Volume, 1e-12f);
public double Pressure => (Gamma - 1.0) * InternalEnergy / Math.Max(Volume, 1e-12); public float Temperature => Pressure / MathF.Max(Density * GasConstant, 1e-12f);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12); public float SpecificEnthalpy => Gamma / (Gamma - 1f) * Pressure / MathF.Max(Density, 1e-12f);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Math.Max(Density, 1e-12);
public Volume0D(double initialVolume, double initialPressure, public Volume0D(float initialVolume, float initialPressure,
double initialTemperature, double gasConstant = 287.0, double gamma = 1.4) float initialTemperature, float gasConstant = 287f, float gamma = 1.4f)
{ {
GasConstant = gasConstant; GasConstant = gasConstant;
Gamma = gamma; Gamma = gamma;
Volume = initialVolume; Volume = initialVolume;
Dvdt = 0.0; Dvdt = 0f;
double rho0 = initialPressure / (GasConstant * initialTemperature); float rho0 = initialPressure / (GasConstant * initialTemperature);
_airMass = rho0 * Volume; // starts with all air _airMass = rho0 * Volume;
_exhaustMass = 0.0; _exhaustMass = 0f;
InternalEnergy = (initialPressure * Volume) / (Gamma - 1.0); InternalEnergy = (initialPressure * Volume) / (Gamma - 1f);
} }
public Port CreatePort() public Port CreatePort()
@@ -52,66 +56,75 @@ namespace FluidSim.Components
return port; 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); float V = MathF.Max(Volume, 1e-12f);
double T = temperature ?? Temperature; float T = temperature ?? Temperature;
double rho = pressure / (GasConstant * T); float rho = pressure / (GasConstant * T);
double totalMass = rho * V; float totalMass = rho * V;
// Keep current air fraction when setting pressure? float af = AirFraction;
double af = AirFraction;
_airMass = totalMass * af; _airMass = totalMass * af;
_exhaustMass = totalMass * (1.0 - af); _exhaustMass = totalMass * (1f - af);
InternalEnergy = pressure * V / (Gamma - 1.0); InternalEnergy = pressure * V / (Gamma - 1f);
} }
public void UpdateState(double dt) public void UpdateState(float dt)
{ {
double totalMdotAir = 0.0; float totalMdotAir = 0f, totalMdotExhaust = 0f, totalEdot = 0f;
double totalMdotExhaust = 0.0;
double totalEdot = 0.0;
foreach (var port in Ports) foreach (var port in Ports)
{ {
double mdot = port.MassFlowRate; // positive INTO volume float mdot = port.MassFlowRate;
double af = mdot >= 0 ? port.AirFraction : AirFraction; // inflow: use port's fraction; outflow: well-mixed float af = mdot >= 0f ? port.AirFraction : AirFraction;
totalMdotAir += mdot * af; totalMdotAir += mdot * af;
totalMdotExhaust += mdot * (1.0 - af); totalMdotExhaust += mdot * (1f - af);
totalEdot += mdot * port.SpecificEnthalpy; totalEdot += mdot * port.SpecificEnthalpy;
} }
double dAir = totalMdotAir * dt; float dAir = totalMdotAir * dt;
double dExhaust = totalMdotExhaust * dt; float dExhaust = totalMdotExhaust * dt;
double dE = totalEdot * dt - Pressure * Dvdt * dt; float dE = totalEdot * dt - Pressure * Dvdt * dt;
_airMass += dAir; _airMass += dAir;
_exhaustMass += dExhaust; _exhaustMass += dExhaust;
InternalEnergy += dE; InternalEnergy += dE;
double V = Math.Max(Volume, 1e-12); // ---- Thermal relaxation ----
double totalMass = _airMass + _exhaustMass; if (EnergyRelaxationRate > 0f)
if (totalMass < 1e-9)
{ {
_airMass = 1e-9; float currentMass = Mass;
_exhaustMass = 0.0; if (currentMass > 1e-12f)
InternalEnergy = AmbientPressure * V / (Gamma - 1.0); {
} // Target internal energy: current mass at ambient temperature
else if (InternalEnergy < 0.0) float targetE = currentMass * GasConstant * AmbientTemperature / (Gamma - 1f);
{ float relaxFactor = MathF.Exp(-EnergyRelaxationRate * dt);
InternalEnergy = AmbientPressure * V / (Gamma - 1.0); InternalEnergy = targetE + (InternalEnergy - targetE) * relaxFactor;
}
} }
if (_airMass < 0.0) _airMass = 0.0; float V = MathF.Max(Volume, 1e-12f);
if (_exhaustMass < 0.0) _exhaustMass = 0.0; 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) foreach (var port in Ports)
{ {
port.Pressure = p; port.Pressure = p;
port.Density = rho; port.Density = rho;
port.Temperature = T; port.Temperature = T;
port.SpecificEnthalpy = h; 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 static class Constants
{ {
public const double Gamma = 1.4; public const float Gamma = 1.4f;
public const double R_gas = 287.0; // J/(kg·K) public const float R_gas = 287f;
public const double P_amb = 101325.0; // Pa public const float P_amb = 101325f;
public const double T_amb = 300.0; // K public const float T_amb = 300f;
public static readonly double Rho_amb = P_amb / (R_gas * T_amb); // ≈ 1.177 kg/m³ 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 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 class IsentropicOrifice
{ {
public static void Compute( public static void Compute(
double pUp, double rhoUp, double TUp, // upstream stagnation float pUp, float rhoUp, float TUp,
double pDown, // downstream back pressure float pDown, float gamma, float R, float area, float Cd,
double gamma, double R, double area, double Cd, out float mdot, out float rhoFace, out float uFace, out float pFace)
out double mdot, out double rhoFace, out double uFace, out double 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) float pr = MathF.Min(pDown / pUp, 1f);
return; 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; float exponent = (gamma - 1f) / gamma;
if (pr < 1e-6) pr = 1e-6; 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)); float aUp = MathF.Sqrt(gamma * R * TUp);
if (pr < prCrit) pr = prCrit; // choked flow
double exponent = (gamma - 1.0) / gamma;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -exponent) - 1.0));
if (double.IsNaN(M)) M = 0;
double aUp = Math.Sqrt(gamma * R * TUp);
uFace = M * aUp; uFace = M * aUp;
rhoFace = rhoUp * Math.Pow(pr, 1.0 / gamma); rhoFace = rhoUp * MathF.Pow(pr, 1f / gamma);
pFace = pUp * pr; 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 public class Solver
{ {
private readonly List<IComponent> _components = new(); private readonly List<IComponent> _components = new();
private readonly List<OrificeLink> _orificeLinks = new(); private PipeSystem _pipeSystem;
private readonly List<OpenEndLink> _openEndLinks = new(); private BoundarySystem _boundarySystem;
private double _dt; private double _dt;
/// <summary>CFL target for substepping (0.30.8). Lower values are safer for shocks.</summary> public int SubStepCount { get; set; } = 4;
public double CflTarget { get; set; } = 0.9; public bool EnableProfiling { get; set; } = false;
// ---------- Timing accumulators (reset every LogInterval steps) ----------
private long _stepCount; private long _stepCount;
private double _timeTotal, _timeCFL, _timeOrifice, _timeOpenEnd, private long _ticksOrifice, _ticksOpenEnd, _ticksPipe, _ticksUpdate;
_timePipe, _timeClearGhosts, _timeUpdateState;
private const int LogInterval = 5000;
private const bool EnableLogging = false; // temporarily ON for debugging
public void SetTimeStep(double dt) => _dt = dt; public void SetTimeStep(double dt) => _dt = dt;
public void AddComponent(IComponent component) => _components.Add(component); 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() public void Step()
{ {
var pipes = _components.OfType<Pipe1D>().ToList(); if (_pipeSystem == null || _boundarySystem == null) return;
if (pipes.Count == 0) return;
var sw = Stopwatch.StartNew(); int nSub = SubStepCount;
float dtSub = (float)(_dt / nSub);
// 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;
}
for (int sub = 0; sub < nSub; sub++) for (int sub = 0; sub < nSub; sub++)
{ {
double t0; long t0;
t0 = sw.Elapsed.TotalSeconds; t0 = Stopwatch.GetTimestamp();
foreach (var link in _orificeLinks) _boundarySystem.ResolveOrifices(dtSub);
link.Resolve(dtSub); _ticksOrifice += Stopwatch.GetTimestamp() - t0;
_timeOrifice += sw.Elapsed.TotalSeconds - t0;
t0 = sw.Elapsed.TotalSeconds; t0 = Stopwatch.GetTimestamp();
foreach (var link in _openEndLinks) _boundarySystem.ResolveOpenEnds(dtSub);
link.Resolve(dtSub); _ticksOpenEnd += Stopwatch.GetTimestamp() - t0;
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
t0 = sw.Elapsed.TotalSeconds; t0 = Stopwatch.GetTimestamp();
foreach (var p in pipes) _pipeSystem.SimulateStep(dtSub);
p.SimulateSingleStep(dtSub); _ticksPipe += Stopwatch.GetTimestamp() - t0;
_timePipe += sw.Elapsed.TotalSeconds - t0;
} }
double tCG = sw.Elapsed.TotalSeconds; long tUS = Stopwatch.GetTimestamp();
foreach (var p in pipes)
p.ClearGhostFlags();
_timeClearGhosts += sw.Elapsed.TotalSeconds - tCG;
double tUS = sw.Elapsed.TotalSeconds;
foreach (var comp in _components) foreach (var comp in _components)
comp.UpdateState(_dt); comp.UpdateState((float)_dt);
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS; _ticksUpdate += Stopwatch.GetTimestamp() - tUS;
_timeTotal += sw.Elapsed.TotalSeconds;
_stepCount++; _stepCount++;
if (_stepCount % LogInterval == 0 && EnableLogging) if (_stepCount % 5000 == 0 && EnableProfiling)
{ {
if (_timeTotal > 0) double freq = Stopwatch.Frequency;
{ double total = _ticksOrifice + _ticksOpenEnd + _ticksPipe + _ticksUpdate;
double stepsPerSec = LogInterval / _timeTotal; double avgStepUs = (total / freq) * 1e6 / 5000.0;
double avgUs = (_timeTotal / LogInterval) * 1e6;
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---"); int orificeCalls = 5000 * nSub;
Console.WriteLine($" Steps per second: {stepsPerSec:F1}"); int updateCalls = 5000;
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} %"); double orificeMs = _ticksOrifice * 1000.0 / freq;
Console.WriteLine($" Substep loop:"); double openEndMs = _ticksOpenEnd * 1000.0 / freq;
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} %"); double pipeMs = _ticksPipe * 1000.0 / freq;
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} %"); double updateMs = _ticksUpdate * 1000.0 / freq;
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} %");
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} %"); double orificeAvgUs = orificeMs * 1000.0 / orificeCalls;
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} %"); double openEndAvgUs = openEndMs * 1000.0 / orificeCalls;
Console.WriteLine(); 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; _ticksOrifice = _ticksOpenEnd = _ticksPipe = _ticksUpdate = 0;
_timeCFL = 0;
_timeOrifice = 0;
_timeOpenEnd = 0;
_timePipe = 0;
_timeClearGhosts = 0;
_timeUpdateState = 0;
} }
} }
} }

View File

@@ -1,76 +1,34 @@
using System; using System;
using FluidSim.Core;
namespace 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 public class SoundProcessor
{ {
private readonly double dt; private readonly float dt;
private readonly double r; // listener distance (m) private readonly float scaleFactor; // 1 / (4π r)
private readonly double scaleFactor; // 1 / (4π r) (free-field monopole) private float flowLP, prevMassFlowOut, smoothDMdt;
private readonly float lpAlpha, alpha;
// ---------- Massflow derivative (identical to original) ---------- public float Gain = 1f;
private double flowLP;
private readonly double lpAlpha;
private double prevMassFlowOut;
private double smoothDMdt;
private readonly double alpha;
public float Gain { get; set; } = 1.0f; public SoundProcessor(int sampleRate, float listenerDistance = 1f)
/// <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)
{ {
dt = 1.0 / sampleRate; dt = 1f / sampleRate;
r = listenerDistanceMeters; scaleFactor = 1f / (4f * MathF.PI * listenerDistance);
scaleFactor = 1.0 / (4.0 * Math.PI * r); // freefield monopole float tau = 0.02f;
alpha = MathF.Exp(-dt / tau);
// ---- Smoothing time constants (unchanged) ---- float tauLP = 0.005f;
double tau = 0.02; // 2 ms for derivative lpAlpha = MathF.Exp(-dt / tauLP);
alpha = Math.Exp(-dt / tau);
double tauLP = 0.00001; // 5 ms lowpass on mass flow
lpAlpha = Math.Exp(-dt / tauLP);
} }
/// <summary> public float Process(float massFlowOut)
/// Process one sample. The OpenEndLink provides the instantaneous
/// exitplane mass flow.
/// </summary>
public float Process(OpenEndLink openEnd)
{ {
double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe flowLP = lpAlpha * flowLP + (1f - lpAlpha) * massFlowOut;
float rawDerivative = (flowLP - prevMassFlowOut) / dt;
// Lowpass the mass flow signal
flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut;
// Derivative of the smoothed mass flow
double rawDerivative = (flowLP - prevMassFlowOut) / dt;
prevMassFlowOut = flowLP; prevMassFlowOut = flowLP;
smoothDMdt = alpha * smoothDMdt + (1f - alpha) * rawDerivative;
// Smooth the derivative float pressure = smoothDMdt * scaleFactor * Gain;
smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative; return MathF.Tanh(pressure);
// Farfield monopole pressure (freefield, Jones eq. 2.15 adapted)
double pressure = smoothDMdt * scaleFactor * Gain;
// Soft clip to ±1
return (float)pressure;
} }
} }
} }

View File

@@ -2,18 +2,9 @@ using System.Collections.Generic;
namespace FluidSim.Interfaces namespace FluidSim.Interfaces
{ {
/// <summary>
/// Minimal interface for all simulation components that have ports.
/// </summary>
public interface IComponent public interface IComponent
{ {
/// <summary>All ports exposed by this component.</summary>
IReadOnlyList<Port> Ports { get; } IReadOnlyList<Port> Ports { get; }
void UpdateState(float dt);
/// <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);
} }
} }

View File

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

View File

@@ -17,7 +17,7 @@ public class Program
private const double DrawFrequency = 60.0; private const double DrawFrequency = 60.0;
// Playback speed // Playback speed
private static double _desiredSpeed = 0.01; private static double _desiredSpeed = 0.001;
private static double _currentDisplaySpeed = _desiredSpeed; private static double _currentDisplaySpeed = _desiredSpeed;
private const double MinSpeed = 0.0001; private const double MinSpeed = 0.0001;
private const double MaxSpeed = 1.0; private const double MaxSpeed = 1.0;
@@ -38,11 +38,11 @@ public class Program
private static Text? _overlayText; private static Text? _overlayText;
// Throttle control // Throttle control
private static double _throttleTarget = 1.0; // 01, set by arrow keys private static float _throttleTarget = 1.0f; // 01, set by arrow keys
private static double _throttleCurrent = 0.0; // actual current fraction (lerped) private static float _throttleCurrent = 0.0f; // actual current fraction (lerped)
private const double ThrottleLerpRate = 10.0; // times per second (speed of movement) private const float ThrottleLerpRate = 10.0f; // times per second (speed of movement)
private static bool _wKeyHeld = false; private static bool _wKeyHeld = false;
private static double _lastThrottleUpdateTime; private static float _lastThrottleUpdateTime;
private const int TargetMaxFill = (int)(SampleRate * 0.2); private const int TargetMaxFill = (int)(SampleRate * 0.2);
@@ -50,11 +50,11 @@ public class Program
{ {
var window = CreateWindow(); var window = CreateWindow();
LoadFont(); LoadFont();
_scenario = new Inline4Scenario(); _scenario = new HelmholtzScenario();
_scenario.Initialize(SampleRate); _scenario.Initialize(SampleRate);
_lastThrottleUpdateTime = 0.0; _lastThrottleUpdateTime = 0.0f;
_simRingBuffer = new SimulationRingBuffer(131072); _simRingBuffer = new SimulationRingBuffer(8192);
_soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 }; _soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 };
_soundEngine.Start(); _soundEngine.Start();
@@ -77,19 +77,19 @@ public class Program
_soundEngine.Speed = _currentDisplaySpeed; _soundEngine.Speed = _currentDisplaySpeed;
// ---- Throttle update ---- // ---- Throttle update ----
double dtThrottle = now - _lastThrottleUpdateTime; float dtThrottle = (float)now - _lastThrottleUpdateTime;
_lastThrottleUpdateTime = now; _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) // Snap to zero instantly when target is zero (key released)
if (throttleDesiredFraction == 0.0) if (throttleDesiredFraction == 0.0)
{ {
_throttleCurrent = 0.0; _throttleCurrent = 0.0f;
} }
else else
{ {
double smoothing = 1.0 - Math.Exp(-ThrottleLerpRate * dtThrottle); float smoothing = 1.0f - MathF.Exp(-ThrottleLerpRate * dtThrottle);
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing; _throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
} }
@@ -199,11 +199,11 @@ public class Program
break; break;
case Keyboard.Key.Up: case Keyboard.Key.Up:
_throttleTarget = Math.Min(1.0, _throttleTarget + 0.05); _throttleTarget = MathF.Min(1.0f, _throttleTarget + 0.05f);
break; break;
case Keyboard.Key.Down: case Keyboard.Key.Down:
_throttleTarget = Math.Max(0.0, _throttleTarget - 0.05); _throttleTarget = MathF.Max(0.0f, _throttleTarget - 0.05f);
break; 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 SFML.System;
using FluidSim.Core;
using FluidSim.Components; using FluidSim.Components;
namespace FluidSim.Tests namespace FluidSim.Tests
{ {
public abstract class Scenario 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 void Initialize(int sampleRate);
public abstract float Process(); public abstract float Process();
public abstract void Draw(RenderWindow target); public abstract void Draw(RenderWindow target);
protected const double AmbientPressure = 101325.0; protected Color PressureColor(float pressurePa)
protected const double AmbientTemperature = 300.0;
public double Throttle { get; set; } = 0.0;
// ---------- Color from pressure (volumes) ----------
protected Color PressureColor(double pressurePa)
{ {
double bar = pressurePa / 1e5; // convert to bar for easier mapping float bar = pressurePa / 1e5f;
byte r, g, b; byte r, g, b;
if (bar < 1f)
if (bar < 1.0) // vacuum → blue to green
{ {
double factor = Math.Clamp(bar, 0.0, 1.0); float f = Math.Clamp(bar, 0f, 1f);
r = 0; r = 0; g = (byte)(255 * f); b = (byte)(255 * (1 - f));
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));
} }
else else
{ {
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); float f = Math.Min((bar - 1f) / 9f, 1f);
r = (byte)(255 * factor); r = (byte)(255 * f); g = (byte)(255 * (1 - f)); b = 0;
g = (byte)(255 * (1.0 - factor)); }
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); return new Color(r, g, b);
} }
// ---------- Draw a generic volume (e.g. plenum) ----------
protected void DrawVolume(RenderWindow target, Volume0D volume, protected void DrawVolume(RenderWindow target, Volume0D volume,
float centerX, float topY, float width, float height) float centerX, float topY, float width, float height)
{ {
var rect = new RectangleShape(new Vector2f(width, 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) Position = new Vector2f(centerX - width / 2f, topY)
}; };
target.Draw(rect); target.Draw(rect);
@@ -75,122 +63,99 @@ namespace FluidSim.Tests
FillColor = Color.Transparent, FillColor = Color.Transparent,
OutlineColor = Color.White, OutlineColor = Color.White,
OutlineThickness = 1f, OutlineThickness = 1f,
Position = new Vector2f(centerX - width / 2f, topY) Position = rect.Position
}; };
target.Draw(border); target.Draw(border);
} }
// ---------- Draw an engine cylinder ----------
protected void DrawCylinder(RenderWindow target, Cylinder cylinder, protected void DrawCylinder(RenderWindow target, Cylinder cylinder,
float centerX, float topY, float width, float maxHeight) float centerX, float topY, float width, float maxHeight)
{ {
double fraction = cylinder.PistonFraction; float fraction = cylinder.PistonFraction;
float currentHeight = (float)(maxHeight * fraction); float currentHeight = maxHeight * fraction;
var wall = new RectangleShape(new Vector2f(width, maxHeight))
// Walls {
var wall = new RectangleShape(new Vector2f(width, maxHeight)); FillColor = new Color(60, 60, 60),
wall.FillColor = new Color(60, 60, 60); Position = new Vector2f(centerX - width / 2f, topY)
wall.Position = new Vector2f(centerX - width / 2f, topY); };
target.Draw(wall); target.Draw(wall);
var gas = new RectangleShape(new Vector2f(width, currentHeight))
// Gas colored by pressure now {
float gasTop = topY; FillColor = PressureColor(cylinder.Pressure),
var gasRect = new RectangleShape(new Vector2f(width, currentHeight)); Position = new Vector2f(centerX - width / 2f, topY)
gasRect.FillColor = PressureColor(cylinder.Pressure); // ← pressurebased };
gasRect.Position = new Vector2f(centerX - width / 2f, gasTop); target.Draw(gas);
target.Draw(gasRect); var piston = new RectangleShape(new Vector2f(width, 4f))
{
// Piston line FillColor = Color.White,
var pistonLine = new RectangleShape(new Vector2f(width, 4f)); Position = new Vector2f(centerX - width / 2f, topY + currentHeight)
pistonLine.FillColor = Color.White; };
pistonLine.Position = new Vector2f(centerX - width / 2f, topY + currentHeight); target.Draw(piston);
target.Draw(pistonLine);
// Valve indicators
float valveW = 6f, valveH = 10f, valveY = topY + 4f; float valveW = 6f, valveH = 10f, valveY = topY + 4f;
var intakeValve = new RectangleShape(new Vector2f(valveW, valveH)); var iv = 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); FillColor = cylinder.IntakeValveArea > 0f ? Color.Green : Color.Red,
target.Draw(intakeValve); Position = new Vector2f(centerX - width / 2f - valveW - 2f, valveY)
};
var exhaustValve = new RectangleShape(new Vector2f(valveW, valveH)); target.Draw(iv);
exhaustValve.FillColor = cylinder.ExhaustValveArea > 0 ? Color.Green : Color.Red; var ev = new RectangleShape(new Vector2f(valveW, valveH))
exhaustValve.Position = new Vector2f(centerX + width / 2f + 2f, valveY); {
target.Draw(exhaustValve); 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, PipeSystem pipeSystem, int pipeIndex,
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX) 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; if (n < 2) return;
float pipeLengthPx = pipeEndX - pipeStartX; float pipeLen = pipeEndX - pipeStartX;
float dx = pipeLengthPx / (n - 1); float dx = pipeLen / (n - 1);
float baseRadius = 25f; 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 centers = new float[n];
var radii = new float[n]; var radii = new float[n];
var temperatures = new double[n]; var temps = new float[n];
double R_gas = 287.0;
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
double p = pipe.GetCellPressure(i); int cell = start + i;
double rho = pipe.GetCellDensity(i); float p = pipeSystem.GetCellPressure(cell);
double T = p / Math.Max(rho * R_gas, 1e-12); float rho = pipeSystem.GetCellDensity(cell);
temperatures[i] = T; temps[i] = p / MathF.Max(rho * 287f, 1e-12f);
float dev = MathF.Tanh((p - AmbientPressure) / AmbientPressure * 0.5f);
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor); radii[i] = baseRadius * (1f + dev * 2f);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f; if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx; centers[i] = pipeStartX + i * dx;
} }
int segmentsPerCell = 8; int segments = 8;
int totalPoints = n + (n - 1) * segmentsPerCell; var va = new VertexArray(PrimitiveType.TriangleStrip);
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
float x = centers[i]; float x = centers[i], r = radii[i];
float r = radii[i]; Color col = TemperatureColor(temps[i]);
Color col = TemperatureColor(temperatures[i]); // pipes still use temperature va.Append(new Vertex(new Vector2f(x, pipeCenterY - r), col));
va.Append(new Vertex(new Vector2f(x, pipeCenterY + r), col));
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col);
if (i < n - 1) if (i < n - 1)
{ {
for (int s = 1; s <= segmentsPerCell; s++) for (int s = 1; s <= segments; s++)
{ {
float t = s / (float)segmentsPerCell; float t = s / (float)segments;
float st = SmoothStep(0f, 1f, t);
float xi = centers[i] + (centers[i + 1] - centers[i]) * t; float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st; float ri = radii[i] + (radii[i + 1] - radii[i]) * t;
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; float Ti = temps[i] + (temps[i + 1] - temps[i]) * t;
Color coli = TemperatureColor(Ti); Color colS = TemperatureColor(Ti);
va.Append(new Vertex(new Vector2f(xi, pipeCenterY - ri), colS));
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli); va.Append(new Vertex(new Vector2f(xi, pipeCenterY + ri), colS));
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
} }
} }
} }
target.Draw(va);
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

@@ -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.Graphics;
using SFML.System; using SFML.System;
using FluidSim.Components;
using FluidSim.Core; using FluidSim.Core;
using FluidSim.Utils;
namespace FluidSim.Tests namespace FluidSim.Tests
{ {
public class TestScenario : Scenario public class TestScenario : Scenario
{ {
// Engine private PipeSystem pipeSystem;
private Cylinder cylinder; private BoundarySystem boundaries;
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 Solver solver; private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor; private int[] pipeStart = { 0 };
private OutdoorExhaustReverb reverb; private int[] pipeEnd;
private double dt; private double dt;
private int stepCount; private int stepCount;
// ---------- Throttle control ---------- // Sound output: use pressure at open end
public double MaxThrottleArea { get; set; } = 1 * Units.cm2; // 2 cm² private SoundProcessor openEndSound;
private int openEndIdx = 0; // index of the open end in BoundarySystem (we added only one)
public override void Initialize(int sampleRate) public override void Initialize(int sampleRate)
{ {
dt = 1.0 / 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.SetTimeStep(dt);
solver.CflTarget = 0.9; solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
// ---- Crankshaft (external, passed to cylinder) ---- solver.EnableProfiling = true;
crankshaft = new Crankshaft(600); pipeSystem.EnableProfiling = true;
crankshaft.Inertia = 0.2;
crankshaft.FrictionConstant = 2;
crankshaft.FrictionViscous = 0.04;
// ---- Cylinder ---- // Simple sound processor: convert mass flow rate to audio
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; openEndSound = new SoundProcessor(sampleRate, 1f) { Gain = 2f };
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);
Console.WriteLine("Pulse test ready.");
stepCount = 0; 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() public override float Process()
{ {
cylinder.Crankshaft.Step(dt);
cylinder.PreStep(dt);
solver.Step(); solver.Step();
stepCount++; stepCount++;
if (stepCount % 10000 == 0) float flow = boundaries.GetOpenEndMassFlow(openEndIdx);
{ float sample = openEndSound.Process(flow);
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;
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " + return sample;
$"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);
} }
public override void Draw(RenderWindow target) public override void Draw(RenderWindow target)
@@ -178,56 +93,10 @@ namespace FluidSim.Tests
float winW = target.GetView().Size.X; float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y; float winH = target.GetView().Size.Y;
float intakeY = winH / 2f - 40f; float startX = 50f;
float exhaustY = winH / 2f + 80f; float endX = winW - 50f;
float y = winH / 2f;
// Open end marker DrawPipe(target, pipeSystem, 0, y, startX, endX);
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);
} }
} }
} }