Helmholtz testing (no decay bug)
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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π (four‑stroke 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
// Cycle‑to‑cycle 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;
|
|
||||||
|
|
||||||
// ---- Multi‑cylinder support ----
|
|
||||||
/// <summary>
|
|
||||||
/// Phase offset (radians) added to the crankshaft angle for this cylinder.
|
|
||||||
/// Used for multi‑cylinder engines; set to 0 for single‑cylinder.
|
|
||||||
/// </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 phase‑offset 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace FluidSim.Components
|
|
||||||
{
|
|
||||||
public static class NozzleFlow
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the nozzle‑exit 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 high‑pressure side
|
|
||||||
mdot = rhoExit * uExit * area;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ambient cell for non‑reflecting 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using FluidSim.Interfaces;
|
|
||||||
|
|
||||||
namespace FluidSim.Components
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 1‑D compressible Euler pipe with Lax‑Friedrichs finite‑volume 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 sub‑step) ----------
|
|
||||||
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: Pre‑compute 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 n‑1 – 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
330
Core/BoundarySystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
27
Core/GhostBuffer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
using System;
|
|
||||||
using FluidSim.Components;
|
|
||||||
|
|
||||||
namespace FluidSim.Core
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Characteristic open‑end 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
// ---- Steady‑state 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
560
Core/Pipesystem.cs
Normal 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 on‑the‑fly 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;
|
||||||
|
|
||||||
|
// Pre‑compute 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);
|
||||||
|
|
||||||
|
// Lax‑Friedrichs 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
Core/Solver.cs
163
Core/Solver.cs
@@ -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 sub‑stepping (0.3‑0.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 sub‑steps
|
|
||||||
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($" Sub‑step 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,34 @@
|
|||||||
using System;
|
using System;
|
||||||
using FluidSim.Core;
|
|
||||||
|
|
||||||
namespace FluidSim.Core
|
namespace FluidSim.Core
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Synthesises far‑field 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;
|
||||||
|
|
||||||
// ---------- Mass‑flow 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); // free‑field 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 low‑pass on mass flow
|
|
||||||
lpAlpha = Math.Exp(-dt / tauLP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public float Process(float massFlowOut)
|
||||||
/// Process one sample. The OpenEndLink provides the instantaneous
|
|
||||||
/// exit‑plane 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;
|
||||||
// Low‑pass 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);
|
||||||
|
|
||||||
// Far‑field monopole pressure (free‑field, Jones eq. 2.15 adapted)
|
|
||||||
double pressure = smoothDMdt * scaleFactor * Gain;
|
|
||||||
|
|
||||||
// Soft clip to ±1
|
|
||||||
return (float)pressure;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 sub‑steps.
|
|
||||||
/// </summary>
|
|
||||||
void UpdateState(double dt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
Program.cs
30
Program.cs
@@ -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; // 0‑1, set by arrow keys
|
private static float _throttleTarget = 1.0f; // 0‑1, 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
152
Scenarios/HelmholtzScenario.cs
Normal file
152
Scenarios/HelmholtzScenario.cs
Normal 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 sub‑step 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 pre‑fill
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
// Plenum‑to‑runner 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;
|
|
||||||
|
|
||||||
// Stub‑to‑collector orifices
|
|
||||||
private OrificeLink stubToCollector1, stubToCollector2, stubToCollector3, stubToCollector4;
|
|
||||||
|
|
||||||
// Collector‑to‑tailpipe 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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), // ← pressure‑based
|
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); // ← pressure‑based
|
};
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
220
Scenarios/SingleCylScenario.cs
Normal file
220
Scenarios/SingleCylScenario.cs
Normal 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 open‑end 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("4‑Stroke 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user