engine almost working, backup before adding gas types.
This commit is contained in:
53
Audio/AudioOutputStream.cs
Normal file
53
Audio/AudioOutputStream.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using SFML.Audio;
|
||||||
|
using SFML.System;
|
||||||
|
|
||||||
|
namespace FluidSim.Audio
|
||||||
|
{
|
||||||
|
public class AudioOutputStream : SoundStream
|
||||||
|
{
|
||||||
|
private readonly SimulationRingBuffer _sourceBuffer;
|
||||||
|
private double _speed = 1.0; // non‑volatile, accessed with Volatile.Read/Write
|
||||||
|
|
||||||
|
public AudioOutputStream(SimulationRingBuffer sourceBuffer)
|
||||||
|
{
|
||||||
|
_sourceBuffer = sourceBuffer;
|
||||||
|
// 2 channels, 44.1 kHz, stereo
|
||||||
|
Initialize(2, 44100, new[] { SoundChannel.FrontLeft, SoundChannel.FrontRight });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Playback speed (0.01 … 1.0 or higher for catch‑up).</summary>
|
||||||
|
public double Speed
|
||||||
|
{
|
||||||
|
get => Volatile.Read(ref _speed);
|
||||||
|
set => Volatile.Write(ref _speed, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnGetData(out short[] samples)
|
||||||
|
{
|
||||||
|
const int monoBlockSize = 512;
|
||||||
|
float[] temp = new float[monoBlockSize];
|
||||||
|
|
||||||
|
int read = _sourceBuffer.ReadInterpolated(temp, monoBlockSize, Speed);
|
||||||
|
samples = new short[monoBlockSize * 2];
|
||||||
|
|
||||||
|
if (read > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < read; i++)
|
||||||
|
{
|
||||||
|
float clamped = Math.Clamp(temp[i], -1f, 1f);
|
||||||
|
short final = (short)(clamped * short.MaxValue);
|
||||||
|
samples[i * 2] = final; // left
|
||||||
|
samples[i * 2 + 1] = final; // right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fill rest with silence
|
||||||
|
for (int i = read * 2; i < samples.Length; i++)
|
||||||
|
samples[i] = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnSeek(Time timeOffset) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
98
Audio/SimulationRingBuffer.cs
Normal file
98
Audio/SimulationRingBuffer.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
namespace FluidSim.Audio
|
||||||
|
{
|
||||||
|
public class SimulationRingBuffer
|
||||||
|
{
|
||||||
|
private readonly float[] _buffer;
|
||||||
|
private readonly int _capacity;
|
||||||
|
private int _writeHead; // monotonic, producer only
|
||||||
|
private int _readHead; // monotonic, consumer advances after consumption
|
||||||
|
|
||||||
|
// Consumer interpolation state
|
||||||
|
private double _readPosFrac;
|
||||||
|
private bool _consumerInit;
|
||||||
|
|
||||||
|
// Events for signalling
|
||||||
|
private readonly AutoResetEvent _spaceAvailable = new AutoResetEvent(false);
|
||||||
|
private readonly AutoResetEvent _dataAvailable = new AutoResetEvent(false);
|
||||||
|
|
||||||
|
public SimulationRingBuffer(int capacity)
|
||||||
|
{
|
||||||
|
if ((capacity & (capacity - 1)) != 0)
|
||||||
|
throw new ArgumentException("Capacity must be a power of two.");
|
||||||
|
_capacity = capacity;
|
||||||
|
_buffer = new float[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Producer ----------
|
||||||
|
public int FreeSpace => _capacity - (_writeHead - Volatile.Read(ref _readHead));
|
||||||
|
|
||||||
|
/// <summary>Number of samples currently available for reading (integer count).</summary>
|
||||||
|
public int AvailableSamples => Volatile.Read(ref _writeHead) - Volatile.Read(ref _readHead);
|
||||||
|
|
||||||
|
public void Write(float sample)
|
||||||
|
{
|
||||||
|
while (FreeSpace == 0)
|
||||||
|
_spaceAvailable.WaitOne();
|
||||||
|
|
||||||
|
int w = _writeHead;
|
||||||
|
int mask = _capacity - 1;
|
||||||
|
_buffer[w & mask] = sample;
|
||||||
|
Volatile.Write(ref _writeHead, w + 1);
|
||||||
|
_dataAvailable.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Write(float[] data, int count)
|
||||||
|
{
|
||||||
|
int free = FreeSpace;
|
||||||
|
int toWrite = Math.Min(count, free);
|
||||||
|
int w = _writeHead;
|
||||||
|
int mask = _capacity - 1;
|
||||||
|
for (int i = 0; i < toWrite; i++)
|
||||||
|
_buffer[(w + i) & mask] = data[i];
|
||||||
|
Volatile.Write(ref _writeHead, w + toWrite);
|
||||||
|
if (toWrite > 0)
|
||||||
|
_dataAvailable.Set();
|
||||||
|
return toWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Consumer ----------
|
||||||
|
public void ResetConsumer() => _consumerInit = false;
|
||||||
|
|
||||||
|
public int ReadInterpolated(float[] dest, int destCount, double speed)
|
||||||
|
{
|
||||||
|
if (!_consumerInit)
|
||||||
|
{
|
||||||
|
_readPosFrac = Volatile.Read(ref _readHead);
|
||||||
|
_consumerInit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mask = _capacity - 1;
|
||||||
|
int writeHead = Volatile.Read(ref _writeHead);
|
||||||
|
int produced = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < destCount; i++)
|
||||||
|
{
|
||||||
|
int idxFloor = (int)_readPosFrac;
|
||||||
|
int idxCeil = idxFloor + 1;
|
||||||
|
if (idxCeil >= writeHead)
|
||||||
|
break;
|
||||||
|
|
||||||
|
float y0 = _buffer[idxFloor & mask];
|
||||||
|
float y1 = _buffer[idxCeil & mask];
|
||||||
|
double frac = _readPosFrac - idxFloor;
|
||||||
|
dest[i] = (float)(y0 + (y1 - y0) * frac);
|
||||||
|
|
||||||
|
_readPosFrac += speed;
|
||||||
|
produced++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int newReadHead = (int)_readPosFrac;
|
||||||
|
if (newReadHead > Volatile.Read(ref _readHead))
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _readHead, newReadHead);
|
||||||
|
_spaceAvailable.Set();
|
||||||
|
}
|
||||||
|
return produced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Audio/SoundEngine.cs
Normal file
45
Audio/SoundEngine.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
namespace FluidSim.Audio
|
||||||
|
{
|
||||||
|
public class SoundEngine : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AudioOutputStream _stream;
|
||||||
|
private bool _isPlaying;
|
||||||
|
|
||||||
|
public SoundEngine(SimulationRingBuffer sourceBuffer, int bufferCapacity = 16384)
|
||||||
|
{
|
||||||
|
_stream = new AudioOutputStream(sourceBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isPlaying) return;
|
||||||
|
_stream.Play();
|
||||||
|
_isPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_isPlaying) return;
|
||||||
|
_stream.Stop();
|
||||||
|
_isPlaying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Speed
|
||||||
|
{
|
||||||
|
get => _stream.Speed;
|
||||||
|
set => _stream.Speed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Volume
|
||||||
|
{
|
||||||
|
get => _stream.Volume;
|
||||||
|
set => _stream.Volume = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
_stream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ namespace FluidSim.Components
|
|||||||
public double PreviousAngle { get; set; } // ← now has public setter
|
public double PreviousAngle { get; set; } // ← now has public setter
|
||||||
|
|
||||||
public double Inertia { get; set; } = 0.2;
|
public double Inertia { get; set; } = 0.2;
|
||||||
public double FrictionConstant { get; set; } = 2.0; // N·m
|
public double FrictionConstant { get; set; } = 0.0; // N·m
|
||||||
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
|
public double FrictionViscous { get; set; } = 0.000; // N·m per rad/s
|
||||||
|
|
||||||
private double externalTorque;
|
private double externalTorque;
|
||||||
|
|
||||||
|
|||||||
274
Components/Cylinder.cs
Normal file
274
Components/Cylinder.cs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FluidSim.Interfaces;
|
||||||
|
|
||||||
|
namespace FluidSim.Components
|
||||||
|
{
|
||||||
|
public class Cylinder : IComponent
|
||||||
|
{
|
||||||
|
// Public ports
|
||||||
|
public Port IntakePort { get; }
|
||||||
|
public Port ExhaustPort { get; }
|
||||||
|
public Crankshaft Crankshaft { get; }
|
||||||
|
|
||||||
|
private readonly Port[] _ports;
|
||||||
|
IReadOnlyList<Port> IComponent.Ports => _ports;
|
||||||
|
|
||||||
|
// Geometry
|
||||||
|
public double Bore { get; }
|
||||||
|
public double Stroke { get; }
|
||||||
|
public double ConRodLength { get; }
|
||||||
|
public double CompressionRatio { get; }
|
||||||
|
|
||||||
|
// Valve timings (degrees, 0 = TDC compression, 720° full cycle)
|
||||||
|
public double IVO { get; }
|
||||||
|
public double IVC { get; }
|
||||||
|
public double EVO { get; }
|
||||||
|
public double EVC { get; }
|
||||||
|
|
||||||
|
// Valve areas
|
||||||
|
public double MaxIntakeArea { get; set; } = 0.0005;
|
||||||
|
public double MaxExhaustArea { get; set; } = 0.0005;
|
||||||
|
|
||||||
|
// Ignition and combustion
|
||||||
|
public double SparkAdvance { get; set; } = 20.0; // °BTDC
|
||||||
|
public double WiebeA { get; set; } = 5.0;
|
||||||
|
public double WiebeM { get; set; } = 2.0;
|
||||||
|
public double WiebeDuration { get; set; } = 60.0; // degrees
|
||||||
|
public double WiebeStart { get; set; } = 5.0; // degrees after spark
|
||||||
|
|
||||||
|
// Fuel
|
||||||
|
public double StoichiometricAFR { get; set; } = 14.7;
|
||||||
|
public double FuelLowerHeatingValue { get; set; } = 44e6; // J/kg
|
||||||
|
|
||||||
|
// Heat loss
|
||||||
|
public double CylinderWallArea { get; set; } = 0.02; // m²
|
||||||
|
public double HeatTransferCoefficient { get; set; } = 100.0; // W/(m²·K)
|
||||||
|
public double AmbientTemperature { get; set; } = 300.0; // K
|
||||||
|
|
||||||
|
// 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 => cylinderMass / Math.Max(cylinderVolume, 1e-12);
|
||||||
|
public double Mass => cylinderMass;
|
||||||
|
public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;
|
||||||
|
|
||||||
|
private double cylinderVolume;
|
||||||
|
private double cylinderMass;
|
||||||
|
private double cylinderEnergy;
|
||||||
|
private double trappedAirMass;
|
||||||
|
private double fuelMass;
|
||||||
|
private double burnFraction; // 0–1
|
||||||
|
private bool combustionActive;
|
||||||
|
private bool fuelInjected;
|
||||||
|
|
||||||
|
// --- Debounce flag: allows combustion only below a certain temperature ---
|
||||||
|
private bool _canCombust = true;
|
||||||
|
private const double CombustionEnableTemperature = 800.0; // K – must cool below this to re‑arm
|
||||||
|
|
||||||
|
private const double Gamma = 1.4;
|
||||||
|
private const double GasConstant = 287.0;
|
||||||
|
|
||||||
|
// Absolute safety limits
|
||||||
|
private const double MaxPressurePa = 200e5; // 200 bar
|
||||||
|
private const double MaxTemperatureK = 3500.0; // 3500 K
|
||||||
|
|
||||||
|
public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
|
||||||
|
double ivo, double ivc, double evo, double evc, double initialRPM = 1000)
|
||||||
|
{
|
||||||
|
Bore = bore;
|
||||||
|
Stroke = stroke;
|
||||||
|
ConRodLength = conRodLength;
|
||||||
|
CompressionRatio = compressionRatio;
|
||||||
|
IVO = ivo;
|
||||||
|
IVC = ivc;
|
||||||
|
EVO = evo;
|
||||||
|
EVC = evc;
|
||||||
|
|
||||||
|
Crankshaft = new Crankshaft(initialRPM);
|
||||||
|
|
||||||
|
cylinderVolume = clearanceVolume;
|
||||||
|
cylinderMass = 1.225 * clearanceVolume;
|
||||||
|
cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0);
|
||||||
|
|
||||||
|
IntakePort = new Port { Owner = this };
|
||||||
|
ExhaustPort = new Port { Owner = this };
|
||||||
|
_ports = new[] { IntakePort, ExhaustPort };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived volumes
|
||||||
|
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
|
||||||
|
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
|
||||||
|
private double CrankRadius => Stroke / 2.0;
|
||||||
|
private double Obliquity => CrankRadius / ConRodLength;
|
||||||
|
|
||||||
|
// Crank angle in degrees (0‑720)
|
||||||
|
private double CrankDeg => (Crankshaft.CrankAngle % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
|
||||||
|
|
||||||
|
public double ComputeVolume(double thetaRad)
|
||||||
|
{
|
||||||
|
double r = CrankRadius;
|
||||||
|
double l = ConRodLength;
|
||||||
|
double cosTh = Math.Cos(thetaRad);
|
||||||
|
double sinTh = Math.Sin(thetaRad);
|
||||||
|
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
|
||||||
|
double x = r * (1.0 - cosTh) + l * (1.0 - term);
|
||||||
|
double area = Math.PI * 0.25 * Bore * Bore;
|
||||||
|
return clearanceVolume + area * x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double IntakeValveArea => ValveArea(CrankDeg, IVO, IVC, MaxIntakeArea);
|
||||||
|
public double ExhaustValveArea => ValveArea(CrankDeg, EVO, EVC, MaxExhaustArea);
|
||||||
|
|
||||||
|
private double ValveArea(double thetaDeg, double opens, double closes, double maxArea)
|
||||||
|
{
|
||||||
|
double deg = thetaDeg % 720.0;
|
||||||
|
if (deg < 0) deg += 720.0;
|
||||||
|
|
||||||
|
if (deg >= opens && deg <= closes)
|
||||||
|
{
|
||||||
|
double half = (closes - opens) * 0.5;
|
||||||
|
double mid = opens + half;
|
||||||
|
double frac = 1.0 - Math.Abs(deg - mid) / half;
|
||||||
|
frac = Math.Clamp(frac, 0.0, 1.0);
|
||||||
|
return maxArea * frac;
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double Wiebe(double angleSinceSpark)
|
||||||
|
{
|
||||||
|
if (angleSinceSpark < WiebeStart) return 0.0;
|
||||||
|
double phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
|
||||||
|
if (phi <= 0) return 0.0;
|
||||||
|
return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PreStep(double dt)
|
||||||
|
{
|
||||||
|
double prevVolume = cylinderVolume;
|
||||||
|
double crankAngleRad = Crankshaft.CrankAngle;
|
||||||
|
cylinderVolume = ComputeVolume(crankAngleRad);
|
||||||
|
|
||||||
|
// Volume work (done BY gas, positive when expanding)
|
||||||
|
double dV = cylinderVolume - prevVolume;
|
||||||
|
cylinderEnergy -= Pressure * dV;
|
||||||
|
|
||||||
|
double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0;
|
||||||
|
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
|
||||||
|
|
||||||
|
// ----- Intake closing: capture trapped air mass and compute fuel -----
|
||||||
|
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
|
||||||
|
{
|
||||||
|
trappedAirMass = cylinderMass;
|
||||||
|
fuelMass = trappedAirMass / StoichiometricAFR;
|
||||||
|
fuelInjected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Spark ignition (once per cycle, only if canCombust) -----
|
||||||
|
double sparkAngle = 0.0 - SparkAdvance;
|
||||||
|
if (sparkAngle < 0) sparkAngle += 720.0;
|
||||||
|
|
||||||
|
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
|
||||||
|
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
|
||||||
|
if (crossedSpark && !combustionActive && fuelInjected && _canCombust)
|
||||||
|
{
|
||||||
|
combustionActive = true;
|
||||||
|
burnFraction = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Combustion progress -----
|
||||||
|
if (combustionActive)
|
||||||
|
{
|
||||||
|
double angleSinceSpark = currDeg - sparkAngle;
|
||||||
|
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
|
||||||
|
double newFraction = Wiebe(angleSinceSpark);
|
||||||
|
|
||||||
|
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
|
||||||
|
{
|
||||||
|
newFraction = 1.0;
|
||||||
|
combustionActive = false;
|
||||||
|
_canCombust = false; // require cool‑down before next ignition
|
||||||
|
}
|
||||||
|
|
||||||
|
double dFraction = newFraction - burnFraction;
|
||||||
|
if (dFraction > 0)
|
||||||
|
{
|
||||||
|
double dQ = fuelMass * FuelLowerHeatingValue * dFraction;
|
||||||
|
cylinderEnergy += dQ;
|
||||||
|
cylinderMass += fuelMass * dFraction;
|
||||||
|
burnFraction = newFraction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Re‑arm combustion if temperature has dropped low enough -----
|
||||||
|
if (!combustionActive && !_canCombust && Temperature < CombustionEnableTemperature)
|
||||||
|
{
|
||||||
|
_canCombust = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Heat loss to cylinder walls -----
|
||||||
|
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
||||||
|
(Temperature - AmbientTemperature) * dt;
|
||||||
|
cylinderEnergy -= dQ_loss;
|
||||||
|
|
||||||
|
// Update port states
|
||||||
|
double p = Pressure, rho = Density, T = Temperature;
|
||||||
|
double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12);
|
||||||
|
IntakePort.Pressure = p;
|
||||||
|
IntakePort.Density = rho;
|
||||||
|
IntakePort.Temperature = T;
|
||||||
|
IntakePort.SpecificEnthalpy = h;
|
||||||
|
|
||||||
|
ExhaustPort.Pressure = p;
|
||||||
|
ExhaustPort.Density = rho;
|
||||||
|
ExhaustPort.Temperature = T;
|
||||||
|
ExhaustPort.SpecificEnthalpy = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateState(double dt)
|
||||||
|
{
|
||||||
|
double dm = 0.0;
|
||||||
|
double dE = 0.0;
|
||||||
|
|
||||||
|
foreach (var port in _ports)
|
||||||
|
{
|
||||||
|
dm += port.MassFlowRate * dt;
|
||||||
|
dE += port.MassFlowRate * port.SpecificEnthalpy * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
cylinderMass += dm;
|
||||||
|
cylinderEnergy += dE;
|
||||||
|
|
||||||
|
double V = Math.Max(cylinderVolume, 1e-12);
|
||||||
|
|
||||||
|
// --- Absolute pressure & temperature clamps ---
|
||||||
|
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
|
||||||
|
if (currentP > MaxPressurePa)
|
||||||
|
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
|
||||||
|
|
||||||
|
double currentRho = cylinderMass / V;
|
||||||
|
double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
|
||||||
|
if (currentT > MaxTemperatureK)
|
||||||
|
{
|
||||||
|
double pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
|
||||||
|
cylinderEnergy = pAtTlimit * V / (Gamma - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Existing safeguards
|
||||||
|
if (cylinderMass < 1e-9)
|
||||||
|
{
|
||||||
|
cylinderMass = 1e-9;
|
||||||
|
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
|
||||||
|
}
|
||||||
|
else if (cylinderEnergy < 0.0)
|
||||||
|
{
|
||||||
|
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cylinderMass < 0.0) cylinderMass = 1e-9;
|
||||||
|
if (cylinderEnergy < 0.0) cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using FluidSim.Interfaces;
|
using FluidSim.Interfaces;
|
||||||
|
|
||||||
namespace FluidSim.Components
|
namespace FluidSim.Components
|
||||||
@@ -9,6 +10,9 @@ namespace FluidSim.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Pipe1D : IComponent
|
public class Pipe1D : IComponent
|
||||||
{
|
{
|
||||||
|
// ---------- Compile‑time profiling flag ----------
|
||||||
|
public const bool EnableDetailedProfiling = false; // set to false in release builds
|
||||||
|
|
||||||
public Port PortA { get; }
|
public Port PortA { get; }
|
||||||
public Port PortB { get; }
|
public Port PortB { get; }
|
||||||
public double Area { get; }
|
public double Area { get; }
|
||||||
@@ -32,7 +36,7 @@ namespace FluidSim.Components
|
|||||||
private readonly double _gamma = 1.4;
|
private readonly double _gamma = 1.4;
|
||||||
|
|
||||||
private double[] _rho, _rhou, _E;
|
private double[] _rho, _rhou, _E;
|
||||||
private double[] _fluxM, _fluxP, _fluxE; // flux at cell faces (0.._n)
|
private double[] _fluxM, _fluxP, _fluxE; // flux at cell faces (0.._n) – kept for possible external use, not used internally anymore
|
||||||
|
|
||||||
private double _rhoGhostL, _uGhostL, _pGhostL;
|
private double _rhoGhostL, _uGhostL, _pGhostL;
|
||||||
private double _rhoGhostR, _uGhostR, _pGhostR;
|
private double _rhoGhostR, _uGhostR, _pGhostR;
|
||||||
@@ -41,6 +45,14 @@ namespace FluidSim.Components
|
|||||||
private double _laminarCoeff;
|
private double _laminarCoeff;
|
||||||
private double _ambientEnergyReference;
|
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)
|
public Pipe1D(double length, double area, int cellCount)
|
||||||
{
|
{
|
||||||
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
|
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
|
||||||
@@ -128,84 +140,142 @@ namespace FluidSim.Components
|
|||||||
|
|
||||||
double dt = dtSub;
|
double dt = dtSub;
|
||||||
int n = _n;
|
int n = _n;
|
||||||
|
|
||||||
// ---- Compute fluxes at all faces using Lax‑Friedrichs ----
|
|
||||||
// Left face (0): between ghostL and cell 0
|
|
||||||
double rL = Math.Max(_rhoGhostL, 1e-12);
|
|
||||||
double pL = _pGhostL;
|
|
||||||
double uL = _uGhostL;
|
|
||||||
double eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
|
|
||||||
|
|
||||||
double rR = Math.Max(_rho[0], 1e-12);
|
|
||||||
double pR = PressureScalar(0);
|
|
||||||
double uR = _rhou[0] / rR;
|
|
||||||
double eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
|
|
||||||
|
|
||||||
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
|
|
||||||
out _fluxM[0], out _fluxP[0], out _fluxE[0]);
|
|
||||||
|
|
||||||
// Internal faces (1 .. n-1)
|
|
||||||
for (int f = 1; f < n; f++)
|
|
||||||
{
|
|
||||||
int iL = f - 1;
|
|
||||||
int iR = f;
|
|
||||||
|
|
||||||
rL = Math.Max(_rho[iL], 1e-12);
|
|
||||||
pL = PressureScalar(iL);
|
|
||||||
uL = _rhou[iL] / rL;
|
|
||||||
eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
|
|
||||||
|
|
||||||
rR = Math.Max(_rho[iR], 1e-12);
|
|
||||||
pR = PressureScalar(iR);
|
|
||||||
uR = _rhou[iR] / rR;
|
|
||||||
eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
|
|
||||||
|
|
||||||
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
|
|
||||||
out _fluxM[f], out _fluxP[f], out _fluxE[f]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right face (n): between cell n-1 and ghostR
|
|
||||||
rL = Math.Max(_rho[n - 1], 1e-12);
|
|
||||||
pL = PressureScalar(n - 1);
|
|
||||||
uL = _rhou[n - 1] / rL;
|
|
||||||
eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
|
|
||||||
|
|
||||||
rR = Math.Max(_rhoGhostR, 1e-12);
|
|
||||||
pR = _pGhostR;
|
|
||||||
uR = _uGhostR;
|
|
||||||
eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
|
|
||||||
|
|
||||||
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
|
|
||||||
out _fluxM[n], out _fluxP[n], out _fluxE[n]);
|
|
||||||
|
|
||||||
// ---- Cell update ----
|
|
||||||
double dt_dx = dt / _dx;
|
double dt_dx = dt / _dx;
|
||||||
double coeff = _laminarCoeff * DampingMultiplier;
|
double coeff = _laminarCoeff * DampingMultiplier;
|
||||||
double relaxRate = EnergyRelaxationRate;
|
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++)
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 rR = Math.Max(_rho[iR], 1e-12);
|
||||||
|
double uR = _rhou[iR] / rR;
|
||||||
|
double pR = p[iR];
|
||||||
|
double cR = c[iR];
|
||||||
|
|
||||||
|
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR,
|
||||||
|
out double fluxM_right, out double fluxP_right, out double fluxE_right);
|
||||||
|
|
||||||
|
// Update cell i
|
||||||
double r = _rho[i];
|
double r = _rho[i];
|
||||||
double ru = _rhou[i];
|
double ru = _rhou[i];
|
||||||
double E = _E[i];
|
double E = _E[i];
|
||||||
|
|
||||||
double dM = _fluxM[i + 1] - _fluxM[i];
|
double newR = r - dt_dx * (fluxM_right - fluxM_prev);
|
||||||
double dP = _fluxP[i + 1] - _fluxP[i];
|
double newRu = ru - dt_dx * (fluxP_right - fluxP_prev);
|
||||||
double dE_flux = _fluxE[i + 1] - _fluxE[i];
|
double newE = E - dt_dx * (fluxE_right - fluxE_prev);
|
||||||
|
|
||||||
double newR = r - dt_dx * dM;
|
|
||||||
double newRu = ru - dt_dx * dP;
|
|
||||||
double newE = E - dt_dx * dE_flux;
|
|
||||||
|
|
||||||
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
|
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
|
||||||
newRu *= dampingFactor;
|
newRu *= dampingFactor;
|
||||||
|
|
||||||
double relaxFactor = Math.Exp(-relaxRate * dt);
|
double relaxFactor = Math.Exp(-relaxRate * dt);
|
||||||
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
|
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
|
||||||
|
|
||||||
newR = Math.Max(newR, 1e-12);
|
newR = Math.Max(newR, 1e-12);
|
||||||
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
|
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
|
||||||
double eMin = 100.0 / (_gamma - 1.0) + kin;
|
double eMin = 100.0 / gm1 + kin;
|
||||||
|
newE = Math.Max(newE, eMin);
|
||||||
|
|
||||||
|
_rho[i] = newR;
|
||||||
|
_rhou[i] = newRu;
|
||||||
|
_E[i] = newE;
|
||||||
|
|
||||||
|
fluxM_prev = fluxM_right;
|
||||||
|
fluxP_prev = fluxP_right;
|
||||||
|
fluxE_prev = fluxE_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);
|
||||||
|
|
||||||
|
LaxFlux(_rho[n - 1], _rhou[n - 1] / Math.Max(_rho[n - 1], 1e-12), 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);
|
||||||
|
|
||||||
|
// Update last cell (identical to interior, but with final fluxes)
|
||||||
|
{
|
||||||
|
int i = n - 1;
|
||||||
|
double r = _rho[i];
|
||||||
|
double ru = _rhou[i];
|
||||||
|
double E = _E[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 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);
|
newE = Math.Max(newE, eMin);
|
||||||
|
|
||||||
_rho[i] = newR;
|
_rho[i] = newR;
|
||||||
@@ -213,43 +283,68 @@ namespace FluidSim.Components
|
|||||||
_E[i] = newE;
|
_E[i] = newE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update port states
|
if (EnableDetailedProfiling)
|
||||||
|
{
|
||||||
|
t1 = Stopwatch.GetTimestamp();
|
||||||
|
_profRightFluxTicks += (t1 - t0);
|
||||||
|
t0 = t1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Phase 5: Update port states ----------
|
||||||
(double rhoA, double uA, double pA) = GetInteriorStateLeft();
|
(double rhoA, double uA, double pA) = GetInteriorStateLeft();
|
||||||
PortA.Pressure = pA; PortA.Density = rhoA;
|
PortA.Pressure = pA; PortA.Density = rhoA;
|
||||||
PortA.Temperature = pA / (rhoA * 287.0);
|
PortA.Temperature = pA / (rhoA * 287.0);
|
||||||
PortA.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pA / rhoA;
|
PortA.SpecificEnthalpy = gm1 / (gamma - 1.0) * pA / rhoA;
|
||||||
|
|
||||||
(double rhoB, double uB, double pB) = GetInteriorStateRight();
|
(double rhoB, double uB, double pB) = GetInteriorStateRight();
|
||||||
PortB.Pressure = pB; PortB.Density = rhoB;
|
PortB.Pressure = pB; PortB.Density = rhoB;
|
||||||
PortB.Temperature = pB / (rhoB * 287.0);
|
PortB.Temperature = pB / (rhoB * 287.0);
|
||||||
PortB.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pB / rhoB;
|
PortB.SpecificEnthalpy = gm1 / (gamma - 1.0) * pB / rhoB;
|
||||||
|
|
||||||
|
if (EnableDetailedProfiling)
|
||||||
|
{
|
||||||
|
t1 = Stopwatch.GetTimestamp();
|
||||||
|
_profPortUpdateTicks += (t1 - t0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Lax‑Friedrichs flux ----------
|
// ---------- Local Lax‑Friedrichs flux function ----------
|
||||||
|
private 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 gm1 = _gamma - 1.0;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original LaxFriedrichsFlux (kept for compatibility, can be removed if unused)
|
||||||
private void LaxFriedrichsFlux(double rL, double uL, double pL, double eL,
|
private void LaxFriedrichsFlux(double rL, double uL, double pL, double eL,
|
||||||
double rR, double uR, double pR, double eR,
|
double rR, double uR, double pR, double eR,
|
||||||
out double fm, out double fp, out double fe)
|
out double fm, out double fp, out double fe)
|
||||||
{
|
{
|
||||||
// Primitive states
|
|
||||||
double rhoL = rL, rhoR = rR;
|
double rhoL = rL, rhoR = rR;
|
||||||
double EL = rhoL * eL; // total energy per volume = rho * (specific total energy)
|
double EL = rhoL * eL;
|
||||||
double ER = rhoR * eR;
|
double ER = rhoR * eR;
|
||||||
|
|
||||||
// Conserved vectors U = (ρ, ρu, E)
|
|
||||||
// Flux F = (ρu, ρu²+p, (E+p)u)
|
|
||||||
double Fm_L = rhoL * uL;
|
double Fm_L = rhoL * uL;
|
||||||
double Fp_L = rhoL * uL * uL + pL;
|
double Fp_L = rhoL * uL * uL + pL;
|
||||||
double Fe_L = (EL + pL) * uL;
|
double Fe_L = (EL + pL) * uL;
|
||||||
|
|
||||||
double Fm_R = rhoR * uR;
|
double Fm_R = rhoR * uR;
|
||||||
double Fp_R = rhoR * uR * uR + pR;
|
double Fp_R = rhoR * uR * uR + pR;
|
||||||
double Fe_R = (ER + pR) * uR;
|
double Fe_R = (ER + pR) * uR;
|
||||||
|
|
||||||
// Lax‑Friedrichs dissipation coefficient α = max(|u|+c) over whole domain, but here we use local max to be simple:
|
|
||||||
double cL = Math.Sqrt(_gamma * pL / rL);
|
double cL = Math.Sqrt(_gamma * pL / rL);
|
||||||
double cR = Math.Sqrt(_gamma * pR / rR);
|
double cR = Math.Sqrt(_gamma * pR / rR);
|
||||||
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
|
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
|
||||||
|
|
||||||
fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rhoR - rhoL);
|
fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rhoR - rhoL);
|
||||||
fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rhoR * uR - rhoL * uL);
|
fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rhoR * uR - rhoL * uL);
|
||||||
fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (ER - EL);
|
fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (ER - EL);
|
||||||
@@ -291,5 +386,42 @@ namespace FluidSim.Components
|
|||||||
double e = p / ((_gamma - 1.0) * rho);
|
double e = p / ((_gamma - 1.0) * rho);
|
||||||
_E[i] = rho * e + 0.5 * rho * u * u;
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,11 +13,12 @@ namespace FluidSim.Core
|
|||||||
public double DischargeCoefficient { get; set; } = 0.62;
|
public double DischargeCoefficient { get; set; } = 0.62;
|
||||||
|
|
||||||
public double EffectiveLength { get; set; } = 0.001;
|
public double EffectiveLength { get; set; } = 0.001;
|
||||||
public bool UseInertance { get; set; } = true;
|
public bool UseInertance { get; set; } = false;
|
||||||
|
|
||||||
private double _mdot; // positive = volume → pipe
|
// Current mass flow (kg/s, positive = volume → pipe)
|
||||||
|
private double _mdot;
|
||||||
|
|
||||||
public double LastMassFlowRate { get; private set; }
|
public double LastMassFlowRate { get; private set; } // positive = into volume
|
||||||
public double LastFaceDensity { get; private set; }
|
public double LastFaceDensity { get; private set; }
|
||||||
public double LastFaceVelocity { get; private set; }
|
public double LastFaceVelocity { get; private set; }
|
||||||
public double LastFacePressure { get; private set; }
|
public double LastFacePressure { get; private set; }
|
||||||
@@ -54,23 +55,32 @@ namespace FluidSim.Core
|
|||||||
double gamma = 1.4;
|
double gamma = 1.4;
|
||||||
double R = 287.0;
|
double R = 287.0;
|
||||||
|
|
||||||
// ---- 1. Steady‑state nozzle solution (gives correct exit pressure) ----
|
// ---- Steady‑state nozzle solution (gives correct exit state) ----
|
||||||
double mdotSS;
|
double mdotSS; // positive = volume → pipe
|
||||||
double rhoFace0, uFace0, pFace0;
|
double rhoFace0, uFace0, pFace0;
|
||||||
if (volP >= pipeP)
|
if (volP >= pipeP)
|
||||||
{
|
{
|
||||||
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
|
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
|
||||||
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
||||||
mdotSS = mdotUpToDown; // volume → pipe
|
mdotSS = mdotUpToDown;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
|
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
|
||||||
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
||||||
mdotSS = -mdotUpToDown; // pipe → volume → negative for volume→pipe convention
|
mdotSS = -mdotUpToDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 2. Inertance dynamics ----
|
// ====== Hard physical cap: max sonic flow × 1.1 ======
|
||||||
|
double upRho = mdotSS >= 0 ? volRho : pipeRho;
|
||||||
|
double upT = mdotSS >= 0 ? volT : pipeT;
|
||||||
|
double upC = Math.Sqrt(gamma * R * upT);
|
||||||
|
double maxFlow = upRho * upC * area * 1.1;
|
||||||
|
if (Math.Abs(mdotSS) > maxFlow)
|
||||||
|
mdotSS = Math.Sign(mdotSS) * maxFlow;
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
// ---- Dynamic update ----
|
||||||
if (UseInertance)
|
if (UseInertance)
|
||||||
{
|
{
|
||||||
double rhoUp = _mdot >= 0 ? volRho : pipeRho;
|
double rhoUp = _mdot >= 0 ? volRho : pipeRho;
|
||||||
@@ -85,39 +95,39 @@ namespace FluidSim.Core
|
|||||||
_mdot = mdotSS;
|
_mdot = mdotSS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp outflow to available mass
|
// Clamp outflow to available mass (if finite volume)
|
||||||
if (VolumePort.Owner is Volume0D vol)
|
if (VolumePort.Owner is Volume0D vol)
|
||||||
{
|
{
|
||||||
double maxOut = vol.Mass / dtSub;
|
double maxOut = vol.Mass / dtSub;
|
||||||
if (_mdot > maxOut) _mdot = maxOut;
|
if (_mdot > maxOut) _mdot = maxOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 3. Ghost state (use nozzle‑exit pressure!) ----
|
// ---- Ghost state ----
|
||||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho; // upstream density
|
double rhoFace = _mdot >= 0 ? volRho : pipeRho;
|
||||||
double pFace = pFace0; // correct exit pressure (choked/subsonic)
|
double pFace = pFace0;
|
||||||
double mdotMag = Math.Abs(_mdot);
|
double mdotMag = Math.Abs(_mdot);
|
||||||
double uFace = mdotMag / (rhoFace * area);
|
double uFace = mdotMag / (rhoFace * area);
|
||||||
|
|
||||||
if (IsPipeLeftEnd)
|
if (IsPipeLeftEnd)
|
||||||
uFace = _mdot >= 0 ? uFace : -uFace; // left: +u into pipe
|
uFace = _mdot >= 0 ? uFace : -uFace;
|
||||||
else
|
else
|
||||||
uFace = _mdot >= 0 ? -uFace : uFace; // right: +u out of pipe
|
uFace = _mdot >= 0 ? -uFace : uFace;
|
||||||
|
|
||||||
if (IsPipeLeftEnd)
|
if (IsPipeLeftEnd)
|
||||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
||||||
else
|
else
|
||||||
Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
||||||
|
|
||||||
// Store for monitoring
|
// Store results (positive = into volume)
|
||||||
double mdotIntoVolume = -_mdot;
|
LastMassFlowRate = -_mdot;
|
||||||
LastMassFlowRate = mdotIntoVolume;
|
|
||||||
LastFaceDensity = rhoFace;
|
LastFaceDensity = rhoFace;
|
||||||
LastFaceVelocity = uFace;
|
LastFaceVelocity = uFace;
|
||||||
LastFacePressure = pFace;
|
LastFacePressure = pFace;
|
||||||
|
|
||||||
VolumePort.MassFlowRate = mdotIntoVolume;
|
VolumePort.MassFlowRate = -_mdot;
|
||||||
|
|
||||||
if (mdotIntoVolume >= 0)
|
// Enthalpy transport
|
||||||
|
if (-_mdot >= 0) // inflow → pipe enthalpy
|
||||||
{
|
{
|
||||||
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
|
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
|
||||||
VolumePort.SpecificEnthalpy = hPipe;
|
VolumePort.SpecificEnthalpy = hPipe;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluidSim.Components;
|
using FluidSim.Components;
|
||||||
using FluidSim.Interfaces;
|
using FluidSim.Interfaces;
|
||||||
@@ -18,6 +19,20 @@ namespace FluidSim.Core
|
|||||||
/// <summary>CFL target for sub‑stepping (0.3‑0.8). Lower values are safer for shocks.</summary>
|
/// <summary>CFL target for sub‑stepping (0.3‑0.8). Lower values are safer for shocks.</summary>
|
||||||
public double CflTarget { get; set; } = 0.8;
|
public double CflTarget { get; set; } = 0.8;
|
||||||
|
|
||||||
|
// ---------- Timing accumulators (reset every LogInterval steps) ----------
|
||||||
|
private long _stepCount;
|
||||||
|
private double _timeTotal;
|
||||||
|
private double _timeCFL;
|
||||||
|
private double _timeOrifice;
|
||||||
|
private double _timeOpenEnd;
|
||||||
|
private double _timeJunction;
|
||||||
|
private double _timePipe;
|
||||||
|
private double _timeClearGhosts;
|
||||||
|
private double _timeUpdateState;
|
||||||
|
|
||||||
|
private const int LogInterval = 5000; // print once per second (at 44.1 kHz)
|
||||||
|
private const bool EnableLogging = false;
|
||||||
|
|
||||||
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);
|
||||||
@@ -30,11 +45,16 @@ namespace FluidSim.Core
|
|||||||
var pipes = _components.OfType<Pipe1D>().ToList();
|
var pipes = _components.OfType<Pipe1D>().ToList();
|
||||||
if (pipes.Count == 0) return;
|
if (pipes.Count == 0) return;
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
// CFL count
|
||||||
int nSub = 1;
|
int nSub = 1;
|
||||||
foreach (var p in pipes)
|
foreach (var p in pipes)
|
||||||
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt, CflTarget));
|
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt, CflTarget));
|
||||||
double dtSub = _dt / nSub;
|
double dtSub = _dt / nSub;
|
||||||
|
|
||||||
|
_timeCFL += sw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
const int maxSubSteps = 10000;
|
const int maxSubSteps = 10000;
|
||||||
if (nSub > maxSubSteps)
|
if (nSub > maxSubSteps)
|
||||||
{
|
{
|
||||||
@@ -44,21 +64,86 @@ namespace FluidSim.Core
|
|||||||
|
|
||||||
for (int sub = 0; sub < nSub; sub++)
|
for (int sub = 0; sub < nSub; sub++)
|
||||||
{
|
{
|
||||||
|
double t0;
|
||||||
|
|
||||||
|
t0 = sw.Elapsed.TotalSeconds;
|
||||||
foreach (var link in _orificeLinks)
|
foreach (var link in _orificeLinks)
|
||||||
link.Resolve(dtSub);
|
link.Resolve(dtSub);
|
||||||
|
_timeOrifice += sw.Elapsed.TotalSeconds - t0;
|
||||||
|
|
||||||
|
t0 = sw.Elapsed.TotalSeconds;
|
||||||
foreach (var link in _openEndLinks)
|
foreach (var link in _openEndLinks)
|
||||||
link.Resolve(dtSub);
|
link.Resolve(dtSub);
|
||||||
|
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
|
||||||
|
|
||||||
|
t0 = sw.Elapsed.TotalSeconds;
|
||||||
foreach (var junc in _junctions)
|
foreach (var junc in _junctions)
|
||||||
junc.Resolve(dtSub);
|
junc.Resolve(dtSub);
|
||||||
|
_timeJunction += sw.Elapsed.TotalSeconds - t0;
|
||||||
|
|
||||||
|
t0 = sw.Elapsed.TotalSeconds;
|
||||||
foreach (var p in pipes)
|
foreach (var p in pipes)
|
||||||
p.SimulateSingleStep(dtSub);
|
p.SimulateSingleStep(dtSub);
|
||||||
|
_timePipe += sw.Elapsed.TotalSeconds - t0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double tCG = sw.Elapsed.TotalSeconds;
|
||||||
foreach (var p in pipes)
|
foreach (var p in pipes)
|
||||||
p.ClearGhostFlags();
|
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(_dt);
|
||||||
|
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS;
|
||||||
|
|
||||||
|
// accumulate total step time (includes CFL, sub‑steps, clear ghosts, update state)
|
||||||
|
_timeTotal += sw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
|
// ---------- Periodic report ----------
|
||||||
|
_stepCount++;
|
||||||
|
if (_stepCount % LogInterval == 0 && EnableLogging)
|
||||||
|
{
|
||||||
|
if (_timeTotal > 0)
|
||||||
|
{
|
||||||
|
double totalMs = _timeTotal * 1000.0;
|
||||||
|
double avgUs = (_timeTotal / LogInterval) * 1e6; // µs per step
|
||||||
|
double stepsPerSec = LogInterval / _timeTotal; // steps per second
|
||||||
|
|
||||||
|
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---");
|
||||||
|
Console.WriteLine($" Steps per second: {stepsPerSec:F1}");
|
||||||
|
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
|
||||||
|
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} % ({_timeCFL * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" Sub‑step loop:");
|
||||||
|
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} % ({_timeOrifice * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} % ({_timeOpenEnd * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" Junctions: {_timeJunction / _timeTotal * 100:F1} % ({_timeJunction * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} % ({_timePipe * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} % ({_timeClearGhosts * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} % ({_timeUpdateState * 1e6 / LogInterval:F1} µs/step)");
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// ---------- Optional detailed pipe profiling ----------
|
||||||
|
if (Pipe1D.EnableDetailedProfiling)
|
||||||
|
{
|
||||||
|
foreach (var pipe in pipes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(pipe.GetDetailProfileReport());
|
||||||
|
pipe.ResetDetailCounters();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset accumulators for next interval
|
||||||
|
_timeTotal = 0;
|
||||||
|
_timeCFL = 0;
|
||||||
|
_timeOrifice = 0;
|
||||||
|
_timeOpenEnd = 0;
|
||||||
|
_timeJunction = 0;
|
||||||
|
_timePipe = 0;
|
||||||
|
_timeClearGhosts = 0;
|
||||||
|
_timeUpdateState = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
using SFML.Audio;
|
|
||||||
using SFML.System;
|
|
||||||
|
|
||||||
namespace FluidSim;
|
|
||||||
|
|
||||||
#region Lock‑free ring buffer (unchanged)
|
|
||||||
internal class RingBuffer
|
|
||||||
{
|
|
||||||
private readonly float[] buffer;
|
|
||||||
private volatile int readPos;
|
|
||||||
private volatile int writePos;
|
|
||||||
|
|
||||||
public RingBuffer(int capacity)
|
|
||||||
{
|
|
||||||
if ((capacity & (capacity - 1)) != 0)
|
|
||||||
throw new ArgumentException("Capacity must be a power of two.");
|
|
||||||
buffer = new float[capacity];
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count => (writePos - readPos) & (buffer.Length - 1);
|
|
||||||
public int Space => (readPos - writePos - 1) & (buffer.Length - 1);
|
|
||||||
|
|
||||||
public int Write(float[] data, int count)
|
|
||||||
{
|
|
||||||
int space = Space;
|
|
||||||
int toWrite = Math.Min(count, space);
|
|
||||||
int mask = buffer.Length - 1;
|
|
||||||
for (int i = 0; i < toWrite; i++)
|
|
||||||
buffer[(writePos + i) & mask] = data[i];
|
|
||||||
writePos = (writePos + toWrite) & mask;
|
|
||||||
return toWrite;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Read(float[] destination, int count)
|
|
||||||
{
|
|
||||||
int available = Count;
|
|
||||||
int toRead = Math.Min(count, available);
|
|
||||||
int mask = buffer.Length - 1;
|
|
||||||
for (int i = 0; i < toRead; i++)
|
|
||||||
destination[i] = buffer[(readPos + i) & mask];
|
|
||||||
readPos = (readPos + toRead) & mask;
|
|
||||||
return toRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Stereo stream that consumes the ring buffer
|
|
||||||
internal class RingBufferStream : SoundStream
|
|
||||||
{
|
|
||||||
private readonly RingBuffer ringBuffer;
|
|
||||||
|
|
||||||
public RingBufferStream(RingBuffer buffer)
|
|
||||||
{
|
|
||||||
ringBuffer = buffer;
|
|
||||||
// 2 channels, 44.1 kHz, standard stereo mapping
|
|
||||||
Initialize(2, 44100, new[] { SoundChannel.FrontLeft, SoundChannel.FrontRight });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnGetData(out short[] samples)
|
|
||||||
{
|
|
||||||
const int monoBlockSize = 512; // number of mono samples we'll read
|
|
||||||
float[] temp = new float[monoBlockSize];
|
|
||||||
int read = ringBuffer.Read(temp, monoBlockSize);
|
|
||||||
samples = new short[monoBlockSize * 2];
|
|
||||||
|
|
||||||
if (read > 0)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < read; i++)
|
|
||||||
{
|
|
||||||
float clamped = Math.Clamp(temp[i], -1f, 1f);
|
|
||||||
short final = (short)(clamped * short.MaxValue);
|
|
||||||
samples[i * 2] = final; // left
|
|
||||||
samples[i * 2 + 1] = final; // right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = read * 2; i < samples.Length; i++)
|
|
||||||
samples[i] = 0;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnSeek(Time timeOffset) =>
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public sound engine API (unchanged)
|
|
||||||
public class SoundEngine : IDisposable
|
|
||||||
{
|
|
||||||
private readonly RingBuffer ringBuffer;
|
|
||||||
private readonly RingBufferStream stream;
|
|
||||||
private bool isPlaying;
|
|
||||||
|
|
||||||
public SoundEngine(int bufferCapacity = 16384)
|
|
||||||
{
|
|
||||||
ringBuffer = new RingBuffer(bufferCapacity);
|
|
||||||
stream = new RingBufferStream(ringBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (isPlaying) return;
|
|
||||||
stream.Play();
|
|
||||||
isPlaying = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if (!isPlaying) return;
|
|
||||||
stream.Stop();
|
|
||||||
isPlaying = false;
|
|
||||||
float[] drain = new float[ringBuffer.Count];
|
|
||||||
ringBuffer.Read(drain, drain.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int WriteSamples(float[] data, int count) =>
|
|
||||||
ringBuffer.Write(data, count);
|
|
||||||
|
|
||||||
public float Volume
|
|
||||||
{
|
|
||||||
get => stream.Volume;
|
|
||||||
set => stream.Volume = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
stream.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
@@ -23,7 +23,7 @@ namespace FluidSim.Core
|
|||||||
scaleFactor = 1.0 / (4.0 * Math.PI * listenerDistanceMeters);
|
scaleFactor = 1.0 / (4.0 * Math.PI * listenerDistanceMeters);
|
||||||
|
|
||||||
// Smoothing time constant for the derivative: 10 ms (much smoother)
|
// Smoothing time constant for the derivative: 10 ms (much smoother)
|
||||||
double tau = 0.010; // 10 ms
|
double tau = 0.005; // 10 ms
|
||||||
alpha = Math.Exp(-dt / tau);
|
alpha = Math.Exp(-dt / tau);
|
||||||
|
|
||||||
// Low‑pass time constant for the mass flow: 5 ms (kneecap high‑freq directly)
|
// Low‑pass time constant for the mass flow: 5 ms (kneecap high‑freq directly)
|
||||||
@@ -49,7 +49,7 @@ namespace FluidSim.Core
|
|||||||
double pressure = smoothDMdt * scaleFactor * Gain;
|
double pressure = smoothDMdt * scaleFactor * Gain;
|
||||||
|
|
||||||
// Soft clip to ±1 (should rarely trigger now)
|
// Soft clip to ±1 (should rarely trigger now)
|
||||||
return (float)Math.Tanh(pressure);
|
return (float)pressure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
34
Core/ThreadLoadTracker.cs
Normal file
34
Core/ThreadLoadTracker.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace FluidSim
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks the duty cycle of a worker thread using an exponential moving average.
|
||||||
|
/// Thread‑safe: one writer (the sim thread), any reader (UI thread).
|
||||||
|
/// </summary>
|
||||||
|
public class ThreadLoadTracker
|
||||||
|
{
|
||||||
|
private double _loadPercent; // 0 .. 100, accessed with Volatile.Read/Write
|
||||||
|
private const double Alpha = 0.1; // smoothing factor (higher = faster response)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the load percentage with a new observation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="busyMs">Time spent on real work in the last cycle.</param>
|
||||||
|
/// <param name="totalMs">Total time of the last cycle (work + idle). If zero, ignored.</param>
|
||||||
|
public void Record(double busyMs, double totalMs)
|
||||||
|
{
|
||||||
|
if (totalMs <= 0) return;
|
||||||
|
double instantLoad = busyMs / totalMs * 100.0;
|
||||||
|
|
||||||
|
// Exponential moving average
|
||||||
|
double old = Volatile.Read(ref _loadPercent);
|
||||||
|
double newLoad = old + Alpha * (instantLoad - old);
|
||||||
|
Volatile.Write(ref _loadPercent, newLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Current smoothed load percentage (0‑100).</summary>
|
||||||
|
public double LoadPercent => Volatile.Read(ref _loadPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Update="fonts\FiraCodeNerdFont-Medium.ttf">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Update="fonts\LiberationMono-Regular.ttf">
|
<None Update="fonts\LiberationMono-Regular.ttf">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
333
Program.cs
333
Program.cs
@@ -1,9 +1,13 @@
|
|||||||
using SFML.Graphics;
|
using FluidSim.Audio;
|
||||||
using SFML.Window;
|
|
||||||
using SFML.System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using FluidSim.Core;
|
using FluidSim.Core;
|
||||||
using FluidSim.Tests;
|
using FluidSim.Tests;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using SFML.Window;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace FluidSim;
|
namespace FluidSim;
|
||||||
|
|
||||||
@@ -11,190 +15,211 @@ public class Program
|
|||||||
{
|
{
|
||||||
private const int SampleRate = 44100;
|
private const int SampleRate = 44100;
|
||||||
private const double DrawFrequency = 60.0;
|
private const double DrawFrequency = 60.0;
|
||||||
private static Scenario scenario;
|
|
||||||
|
|
||||||
// Speed control
|
// Playback speed
|
||||||
private static double desiredSpeed = 0.01;
|
private static double _desiredSpeed = 0.01;
|
||||||
private static double currentSpeed = 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;
|
||||||
private const double ScrollFactor = 1.1;
|
private const double ScrollFactor = 1.1;
|
||||||
|
private static double _lastNormalSpeed = 0.1;
|
||||||
|
private static bool _isRealTime = false;
|
||||||
|
|
||||||
private static double lastDesiredSpeed = 0.1;
|
private static volatile bool _timeWarpActive;
|
||||||
private static bool isRealTime = false;
|
|
||||||
|
|
||||||
// Throttle smoothing (unused but kept)
|
// Thread load tracking
|
||||||
private static double targetThrottle = 0.0;
|
private static ThreadLoadTracker _loadTracker = new ThreadLoadTracker();
|
||||||
private static double currentThrottle = 0.0;
|
|
||||||
private const double ThrottleSmoothing = 20.0;
|
|
||||||
|
|
||||||
private static volatile bool running = true;
|
// Audio & simulation
|
||||||
|
private static SimulationRingBuffer _simRingBuffer = null!;
|
||||||
|
private static SoundEngine _soundEngine = null!;
|
||||||
|
private static TestScenario _scenario = null!; // cast to access ThrottleArea
|
||||||
|
private static Font? _overlayFont;
|
||||||
|
private static Text? _overlayText;
|
||||||
|
|
||||||
// ---- Overlay text ----
|
// Throttle control
|
||||||
private static Font? overlayFont;
|
private static double _throttleTarget = 1.0; // 0‑1, set by arrow keys
|
||||||
private static Text? overlayText;
|
private static double _throttleCurrent = 0.0; // actual current fraction (lerped)
|
||||||
|
private const double ThrottleLerpRate = 5.0; // times per second (speed of movement)
|
||||||
|
private const double ThrottleMinArea = 0.0000000000001; // 3.7e-5 m² ≈ 0.37 cm² (10% of pipe)
|
||||||
|
private const double ThrottleMaxArea = 0.00000000001; // 3.7 cm² (full open)
|
||||||
|
private static bool _wKeyHeld = false;
|
||||||
|
private static double _lastThrottleUpdateTime;
|
||||||
|
|
||||||
|
private const int TargetMaxFill = (int)(SampleRate * 0.2);
|
||||||
|
|
||||||
public static void Main()
|
public static void Main()
|
||||||
|
{
|
||||||
|
var window = CreateWindow();
|
||||||
|
LoadFont();
|
||||||
|
_scenario = (TestScenario)InitializeScenario();
|
||||||
|
_lastThrottleUpdateTime = 0.0;
|
||||||
|
|
||||||
|
_simRingBuffer = new SimulationRingBuffer(131072);
|
||||||
|
_soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 };
|
||||||
|
_soundEngine.Start();
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
Task.Run(() => SimulationLoop(cts.Token), cts.Token);
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
double lastDrawTime = 0.0;
|
||||||
|
|
||||||
|
while (window.IsOpen)
|
||||||
|
{
|
||||||
|
window.DispatchEvents();
|
||||||
|
|
||||||
|
double now = stopwatch.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
|
// ---- Playback speed smoothing ----
|
||||||
|
double targetSpeed = _timeWarpActive ? 1.0 : _desiredSpeed;
|
||||||
|
_currentDisplaySpeed += (targetSpeed - _currentDisplaySpeed) *
|
||||||
|
(1.0 - Math.Exp(-8.0 * (now - lastDrawTime)));
|
||||||
|
_soundEngine.Speed = _currentDisplaySpeed;
|
||||||
|
|
||||||
|
// ---- Throttle update ----
|
||||||
|
double dtThrottle = now - _lastThrottleUpdateTime;
|
||||||
|
_lastThrottleUpdateTime = now;
|
||||||
|
|
||||||
|
double throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0;
|
||||||
|
|
||||||
|
// Snap to zero instantly when target is zero (key released)
|
||||||
|
if (throttleDesiredFraction == 0.0)
|
||||||
|
{
|
||||||
|
_throttleCurrent = 0.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double smoothing = 1.0 - Math.Exp(-ThrottleLerpRate * dtThrottle);
|
||||||
|
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
double actualArea = ThrottleMinArea + (ThrottleMaxArea - ThrottleMinArea) * _throttleCurrent;
|
||||||
|
_scenario.ThrottleArea = actualArea;
|
||||||
|
|
||||||
|
// ---- Drawing ----
|
||||||
|
if (now - lastDrawTime >= 1.0 / DrawFrequency)
|
||||||
|
{
|
||||||
|
if (_overlayText != null)
|
||||||
|
{
|
||||||
|
string toggleHint = _isRealTime ? "[Space] slow mo" : "[Space] real time";
|
||||||
|
_overlayText.DisplayedString =
|
||||||
|
$"{toggleHint} Speed: {_currentDisplaySpeed:F3}x RT: {(_currentDisplaySpeed * 100.0):F1}% Sim load: {_loadTracker.LoadPercent:F0}%\n" +
|
||||||
|
$"Throttle: {_throttleCurrent * 100:F0}% Target: {_throttleTarget * 100:F0}% [W] {(_wKeyHeld ? "BLIP" : "---")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Clear(Color.Black);
|
||||||
|
_scenario.Draw(window);
|
||||||
|
if (_overlayText != null) window.Draw(_overlayText);
|
||||||
|
window.Display();
|
||||||
|
lastDrawTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Cancel();
|
||||||
|
_soundEngine.Dispose();
|
||||||
|
window.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SimulationLoop(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
long cycleStart = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
long workStart = Stopwatch.GetTimestamp();
|
||||||
|
float sample = _scenario.Process();
|
||||||
|
_simRingBuffer.Write(sample);
|
||||||
|
long workEnd = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
while (_simRingBuffer.AvailableSamples > TargetMaxFill &&
|
||||||
|
!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
long cycleEnd = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
|
double busyMs = (workEnd - workStart) / (double)Stopwatch.Frequency * 1000.0;
|
||||||
|
double totalMs = (cycleEnd - cycleStart) / (double)Stopwatch.Frequency * 1000.0;
|
||||||
|
_loadTracker.Record(busyMs, totalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Window & input ----------
|
||||||
|
private static RenderWindow CreateWindow()
|
||||||
{
|
{
|
||||||
var mode = new VideoMode(new Vector2u(1280, 720));
|
var mode = new VideoMode(new Vector2u(1280, 720));
|
||||||
var window = new RenderWindow(mode, "FluidSim");
|
var window = new RenderWindow(mode, "FluidSim");
|
||||||
window.SetVerticalSyncEnabled(true);
|
window.SetVerticalSyncEnabled(false);
|
||||||
window.Closed += (_, _) => { running = false; window.Close(); };
|
window.SetFramerateLimit(60);
|
||||||
|
window.Closed += (_, _) => window.Close();
|
||||||
window.MouseWheelScrolled += OnMouseWheel;
|
window.MouseWheelScrolled += OnMouseWheel;
|
||||||
window.KeyPressed += OnKeyPressed;
|
window.KeyPressed += OnKeyPressed;
|
||||||
|
window.KeyReleased += OnKeyReleased;
|
||||||
// ---- Load font ----
|
return window;
|
||||||
try
|
|
||||||
{
|
|
||||||
overlayFont = new Font("fonts/FiraCodeNerdFont-Medium.ttf");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Failed to load font 'fonts/LiberationMono-Regular.ttf': {ex.Message}");
|
|
||||||
overlayFont = null; // will skip text drawing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlayFont != null)
|
private static void LoadFont()
|
||||||
{
|
{
|
||||||
// SFML 3 Text(font, character size in pixels)
|
try { _overlayFont = new Font("fonts/FiraCodeNerdFont-Medium.ttf"); }
|
||||||
overlayText = new Text(overlayFont)
|
catch { _overlayFont = null; }
|
||||||
|
|
||||||
|
if (_overlayFont != null)
|
||||||
|
_overlayText = new Text(_overlayFont)
|
||||||
{
|
{
|
||||||
FillColor = Color.White,
|
FillColor = Color.White,
|
||||||
Position = new Vector2f(10, 10)
|
Position = new Vector2f(10, 10)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var soundEngine = new SoundEngine(bufferCapacity: 16384);
|
private static Scenario InitializeScenario()
|
||||||
soundEngine.Volume = 100;
|
|
||||||
soundEngine.Start();
|
|
||||||
|
|
||||||
scenario = new TestScenario();
|
|
||||||
scenario.Initialize(SampleRate);
|
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
double lastDrawTime = 0.0;
|
|
||||||
double drawInterval = 1.0 / DrawFrequency;
|
|
||||||
double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds;
|
|
||||||
|
|
||||||
var simBuffer = new List<float>(4096);
|
|
||||||
double readIndex = 0.0;
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
simBuffer.Add(scenario.Process());
|
|
||||||
|
|
||||||
long totalSimSteps = simBuffer.Count;
|
|
||||||
long totalOutputSamples = 0;
|
|
||||||
const int outputChunk = 256;
|
|
||||||
float[] outputBuf = new float[outputChunk];
|
|
||||||
|
|
||||||
while (window.IsOpen)
|
|
||||||
{
|
{
|
||||||
window.DispatchEvents();
|
var sc = new TestScenario();
|
||||||
|
sc.Initialize(SampleRate);
|
||||||
double currentRealTime = stopwatch.Elapsed.TotalSeconds;
|
return sc;
|
||||||
double dtClock = currentRealTime - lastSpeedUpdateTime;
|
|
||||||
lastSpeedUpdateTime = currentRealTime;
|
|
||||||
|
|
||||||
// Smooth simulation speed
|
|
||||||
double speedSmoothing = 8.0;
|
|
||||||
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
|
|
||||||
|
|
||||||
// Generate audio
|
|
||||||
double targetAudioClock = currentRealTime + 0.05;
|
|
||||||
while (totalOutputSamples < targetAudioClock * SampleRate && running)
|
|
||||||
{
|
|
||||||
int toGenerate = (int)Math.Min(
|
|
||||||
(long)outputChunk,
|
|
||||||
(long)(targetAudioClock * SampleRate) - totalOutputSamples
|
|
||||||
);
|
|
||||||
if (toGenerate <= 0) break;
|
|
||||||
|
|
||||||
double maxIndex = readIndex + (toGenerate - 1) * currentSpeed + 2;
|
|
||||||
int requiredSimIndex = (int)Math.Ceiling(maxIndex);
|
|
||||||
while (simBuffer.Count - 1 < requiredSimIndex)
|
|
||||||
{
|
|
||||||
simBuffer.Add(scenario.Process());
|
|
||||||
totalSimSteps++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < toGenerate; i++)
|
|
||||||
{
|
|
||||||
int i0 = (int)readIndex;
|
|
||||||
int i1 = i0 + 1;
|
|
||||||
double frac = readIndex - i0;
|
|
||||||
float y0 = simBuffer[Math.Clamp(i0, 0, simBuffer.Count - 1)];
|
|
||||||
float y1 = simBuffer[Math.Clamp(i1, 0, simBuffer.Count - 1)];
|
|
||||||
outputBuf[i] = (float)(y0 + (y1 - y0) * frac);
|
|
||||||
readIndex += currentSpeed;
|
|
||||||
|
|
||||||
while (readIndex >= 1.0 && simBuffer.Count > 2)
|
|
||||||
{
|
|
||||||
simBuffer.RemoveAt(0);
|
|
||||||
readIndex -= 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int accepted = soundEngine.WriteSamples(outputBuf, toGenerate);
|
|
||||||
totalOutputSamples += accepted;
|
|
||||||
if (accepted < toGenerate)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drawing
|
|
||||||
if (currentRealTime - lastDrawTime >= drawInterval)
|
|
||||||
{
|
|
||||||
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
|
|
||||||
double realtimePercent = totalOutputSamples / (currentRealTime * SampleRate) * 100.0;
|
|
||||||
|
|
||||||
// Update overlay text
|
|
||||||
if (overlayText != null)
|
|
||||||
{
|
|
||||||
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
|
|
||||||
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
|
|
||||||
overlayText.DisplayedString =
|
|
||||||
$"{toggleHint} {throttleHint} " +
|
|
||||||
$"Speed: {currentSpeed:F3}x " +
|
|
||||||
$"RT: {realtimePercent:F1}%";
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Clear(Color.Black);
|
|
||||||
scenario.Draw(window);
|
|
||||||
|
|
||||||
// Draw the overlay on top
|
|
||||||
if (overlayText != null)
|
|
||||||
window.Draw(overlayText);
|
|
||||||
|
|
||||||
window.Display();
|
|
||||||
lastDrawTime = currentRealTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
soundEngine.Dispose();
|
|
||||||
window.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
||||||
{
|
{
|
||||||
bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
|
if (_timeWarpActive) return;
|
||||||
if (e.Delta > 0)
|
if (e.Delta > 0) _desiredSpeed *= ScrollFactor;
|
||||||
desiredSpeed *= ScrollFactor;
|
else if (e.Delta < 0) _desiredSpeed /= ScrollFactor;
|
||||||
else if (e.Delta < 0)
|
_desiredSpeed = Math.Clamp(_desiredSpeed, MinSpeed, MaxSpeed);
|
||||||
desiredSpeed /= ScrollFactor;
|
_lastNormalSpeed = _desiredSpeed;
|
||||||
|
_isRealTime = Math.Abs(_desiredSpeed - 1.0) < 1e-6;
|
||||||
desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed);
|
|
||||||
if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6)
|
|
||||||
lastDesiredSpeed = desiredSpeed;
|
|
||||||
isRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnKeyPressed(object? sender, KeyEventArgs e)
|
private static void OnKeyPressed(object? sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Code == Keyboard.Key.Space)
|
switch (e.Code)
|
||||||
{
|
{
|
||||||
if (isRealTime)
|
case Keyboard.Key.Space:
|
||||||
desiredSpeed = lastDesiredSpeed;
|
_timeWarpActive = !_timeWarpActive;
|
||||||
else
|
if (!_timeWarpActive)
|
||||||
desiredSpeed = 1.0;
|
{
|
||||||
isRealTime = !isRealTime;
|
_desiredSpeed = _lastNormalSpeed;
|
||||||
|
_isRealTime = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keyboard.Key.W:
|
||||||
|
_wKeyHeld = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keyboard.Key.Up:
|
||||||
|
_throttleTarget = Math.Min(1.0, _throttleTarget + 0.05);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Keyboard.Key.Down:
|
||||||
|
_throttleTarget = Math.Max(0.0, _throttleTarget - 0.05);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnKeyReleased(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Code == Keyboard.Key.W)
|
||||||
|
_wKeyHeld = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
Report20260507-1444 (2).diagsession
Normal file
BIN
Report20260507-1444 (2).diagsession
Normal file
Binary file not shown.
@@ -7,39 +7,28 @@ namespace FluidSim.Tests
|
|||||||
{
|
{
|
||||||
public abstract class Scenario
|
public abstract class Scenario
|
||||||
{
|
{
|
||||||
/// <summary>Initialize the scenario with a given audio sample rate.</summary>
|
|
||||||
public abstract void Initialize(int sampleRate);
|
public abstract void Initialize(int sampleRate);
|
||||||
|
|
||||||
/// <summary>Advance one simulation step and return an audio sample.</summary>
|
|
||||||
public abstract float Process();
|
public abstract float Process();
|
||||||
|
|
||||||
/// <summary>Draw the current simulation state onto the given SFML render target.</summary>
|
|
||||||
public abstract void Draw(RenderWindow target);
|
public abstract void Draw(RenderWindow target);
|
||||||
|
|
||||||
// ---------- Shared drawing helpers ----------
|
|
||||||
|
|
||||||
protected const double AmbientPressure = 101325.0;
|
protected const double AmbientPressure = 101325.0;
|
||||||
protected const double AmbientTemperature = 300.0; // K
|
protected const double AmbientTemperature = 300.0;
|
||||||
|
|
||||||
/// <summary>Map temperature [0 K … 2000 K] to a color: blue (0 K) → green (300 K) → red (2000 K).</summary>
|
// ---------- Color helper ----------
|
||||||
protected Color TemperatureColor(double temperature)
|
protected Color TemperatureColor(double temperature)
|
||||||
{
|
{
|
||||||
// Clamp to the range we want to display
|
|
||||||
double t = Math.Clamp(temperature, 0.0, 2000.0);
|
double t = Math.Clamp(temperature, 0.0, 2000.0);
|
||||||
|
|
||||||
byte r, g, b;
|
byte r, g, b;
|
||||||
if (t < AmbientTemperature)
|
if (t < AmbientTemperature)
|
||||||
{
|
{
|
||||||
// Blue → Green
|
double factor = t / AmbientTemperature;
|
||||||
double factor = t / AmbientTemperature; // 0 at 0 K, 1 at 300 K
|
|
||||||
r = 0;
|
r = 0;
|
||||||
g = (byte)(255 * factor);
|
g = (byte)(255 * factor);
|
||||||
b = (byte)(255 * (1.0 - factor));
|
b = (byte)(255 * (1.0 - factor));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Green → Red
|
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature);
|
||||||
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); // 0 at 300 K, 1 at 2000 K
|
|
||||||
r = (byte)(255 * factor);
|
r = (byte)(255 * factor);
|
||||||
g = (byte)(255 * (1.0 - factor));
|
g = (byte)(255 * (1.0 - factor));
|
||||||
b = 0;
|
b = 0;
|
||||||
@@ -47,30 +36,84 @@ namespace FluidSim.Tests
|
|||||||
return new Color(r, g, b);
|
return new Color(r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// ---------- Draw a generic volume (e.g. plenum) ----------
|
||||||
/// Draws the pipe as a smooth triangle‑strip whose radius varies with cell pressure (for visibility),
|
protected void DrawVolume(RenderWindow target, Volume0D volume,
|
||||||
/// but colored by temperature.
|
float centerX, float topY, float width, float height)
|
||||||
/// </summary>
|
{
|
||||||
|
var rect = new RectangleShape(new Vector2f(width, height))
|
||||||
|
{
|
||||||
|
FillColor = TemperatureColor(volume.Temperature),
|
||||||
|
Position = new Vector2f(centerX - width / 2f, topY)
|
||||||
|
};
|
||||||
|
target.Draw(rect);
|
||||||
|
var border = new RectangleShape(new Vector2f(width, height))
|
||||||
|
{
|
||||||
|
FillColor = Color.Transparent,
|
||||||
|
OutlineColor = Color.White,
|
||||||
|
OutlineThickness = 1f,
|
||||||
|
Position = new Vector2f(centerX - width / 2f, topY)
|
||||||
|
};
|
||||||
|
target.Draw(border);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Draw an engine cylinder ----------
|
||||||
|
protected void DrawCylinder(RenderWindow target, Cylinder cylinder,
|
||||||
|
float centerX, float topY, float width, float maxHeight)
|
||||||
|
{
|
||||||
|
double fraction = cylinder.PistonFraction; // 0 = TDC, 1 = BDC
|
||||||
|
float currentHeight = (float)(maxHeight * fraction);
|
||||||
|
|
||||||
|
// Walls
|
||||||
|
var wall = new RectangleShape(new Vector2f(width, maxHeight));
|
||||||
|
wall.FillColor = new Color(60, 60, 60);
|
||||||
|
wall.Position = new Vector2f(centerX - width / 2f, topY);
|
||||||
|
target.Draw(wall);
|
||||||
|
|
||||||
|
// Gas
|
||||||
|
float gasTop = topY;
|
||||||
|
var gasRect = new RectangleShape(new Vector2f(width, currentHeight));
|
||||||
|
gasRect.FillColor = TemperatureColor(cylinder.Temperature);
|
||||||
|
gasRect.Position = new Vector2f(centerX - width / 2f, gasTop);
|
||||||
|
target.Draw(gasRect);
|
||||||
|
|
||||||
|
// Piston line
|
||||||
|
var pistonLine = new RectangleShape(new Vector2f(width, 4f));
|
||||||
|
pistonLine.FillColor = Color.White;
|
||||||
|
pistonLine.Position = new Vector2f(centerX - width / 2f, topY + currentHeight);
|
||||||
|
target.Draw(pistonLine);
|
||||||
|
|
||||||
|
// Valve indicators
|
||||||
|
float valveW = 6f, valveH = 10f, valveY = topY + 4f;
|
||||||
|
var intakeValve = new RectangleShape(new Vector2f(valveW, valveH));
|
||||||
|
intakeValve.FillColor = cylinder.IntakeValveArea > 0 ? Color.Green : Color.Red;
|
||||||
|
intakeValve.Position = new Vector2f(centerX - width / 2f - valveW - 2f, valveY);
|
||||||
|
target.Draw(intakeValve);
|
||||||
|
|
||||||
|
var exhaustValve = new RectangleShape(new Vector2f(valveW, valveH));
|
||||||
|
exhaustValve.FillColor = cylinder.ExhaustValveArea > 0 ? Color.Green : Color.Red;
|
||||||
|
exhaustValve.Position = new Vector2f(centerX + width / 2f + 2f, valveY);
|
||||||
|
target.Draw(exhaustValve);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Draw a pipe ----------
|
||||||
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
|
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
|
||||||
{
|
{
|
||||||
int n = pipe.CellCount;
|
int n = pipe.CellCount;
|
||||||
if (n < 2) return;
|
if (n < 2) return;
|
||||||
|
|
||||||
float pipeLengthPx = pipeEndX - pipeStartX;
|
float pipeLengthPx = pipeEndX - pipeStartX;
|
||||||
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
|
float dx = pipeLengthPx / (n - 1);
|
||||||
|
|
||||||
float baseRadius = 25f;
|
float baseRadius = 25f;
|
||||||
float rangeFactor = 2f;
|
float rangeFactor = 2f;
|
||||||
float scaleFactor = 2f;
|
float scaleFactor = 2f;
|
||||||
|
|
||||||
// ----- smoothstep helper -----
|
|
||||||
static float SmoothStep(float edge0, float edge1, float x)
|
static float SmoothStep(float edge0, float edge1, float x)
|
||||||
{
|
{
|
||||||
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
|
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
|
||||||
return t * t * (3f - 2f * t);
|
return t * t * (3f - 2f * t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Pre‑compute cell positions, radii, and temperatures -----
|
|
||||||
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 temperatures = new double[n];
|
||||||
@@ -80,7 +123,7 @@ namespace FluidSim.Tests
|
|||||||
{
|
{
|
||||||
double p = pipe.GetCellPressure(i);
|
double p = pipe.GetCellPressure(i);
|
||||||
double rho = pipe.GetCellDensity(i);
|
double rho = pipe.GetCellDensity(i);
|
||||||
double T = p / Math.Max(rho * R_gas, 1e-12); // ideal gas
|
double T = p / Math.Max(rho * R_gas, 1e-12);
|
||||||
temperatures[i] = T;
|
temperatures[i] = T;
|
||||||
|
|
||||||
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
|
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
|
||||||
@@ -89,7 +132,6 @@ namespace FluidSim.Tests
|
|||||||
centers[i] = pipeStartX + i * dx;
|
centers[i] = pipeStartX + i * dx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Build triangle‑strip vertices -----
|
|
||||||
int segmentsPerCell = 8;
|
int segmentsPerCell = 8;
|
||||||
int totalPoints = n + (n - 1) * segmentsPerCell;
|
int totalPoints = n + (n - 1) * segmentsPerCell;
|
||||||
Vertex[] stripVertices = new Vertex[totalPoints * 2];
|
Vertex[] stripVertices = new Vertex[totalPoints * 2];
|
||||||
@@ -112,7 +154,7 @@ namespace FluidSim.Tests
|
|||||||
float st = SmoothStep(0f, 1f, t);
|
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]) * st;
|
||||||
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; // linear interpolation
|
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
|
||||||
Color coli = TemperatureColor(Ti);
|
Color coli = TemperatureColor(Ti);
|
||||||
|
|
||||||
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
|
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
|
||||||
|
|||||||
@@ -3,239 +3,218 @@ using SFML.Graphics;
|
|||||||
using SFML.System;
|
using SFML.System;
|
||||||
using FluidSim.Components;
|
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
|
||||||
{
|
{
|
||||||
// Simulation core
|
// Engine
|
||||||
private Solver solver;
|
private Cylinder cylinder;
|
||||||
private double dt;
|
|
||||||
|
|
||||||
// Engine components
|
// Intake side
|
||||||
private Volume0D cylinder;
|
private Pipe1D intakePipeBeforeThrottle; // pipe from ambient to plenum
|
||||||
|
private Volume0D intakePlenum; // plenum (100 mL)
|
||||||
|
private Pipe1D intakeRunner; // pipe from plenum to cylinder
|
||||||
|
|
||||||
|
// Exhaust side
|
||||||
private Pipe1D exhaustPipe;
|
private Pipe1D exhaustPipe;
|
||||||
private OrificeLink exhaustPort;
|
|
||||||
private OpenEndLink pipeOpenEnd;
|
|
||||||
private Crankshaft crankshaft;
|
|
||||||
|
|
||||||
// Audio
|
// Links
|
||||||
|
private OpenEndLink intakeOpenEnd; // ambient → left end of first pipe
|
||||||
|
private OrificeLink throttleOrifice; // first pipe right end → plenum inlet (variable area)
|
||||||
|
private OrificeLink plenumToRunner; // plenum outlet → runner left end (fixed area)
|
||||||
|
private OrificeLink intakeValve; // runner right end → cylinder intake port
|
||||||
|
|
||||||
|
private OrificeLink exhaustValve;
|
||||||
|
private OpenEndLink exhaustOpenEnd;
|
||||||
|
|
||||||
|
private Solver solver;
|
||||||
private SoundProcessor soundProcessor;
|
private SoundProcessor soundProcessor;
|
||||||
|
private double dt;
|
||||||
// Engine geometry (Suzuki TS125 – Jones Appendix 1)
|
|
||||||
private const double Bore = 0.056; // m
|
|
||||||
private const double Stroke = 0.050; // m
|
|
||||||
private const double ConRodLength = 0.110; // m (typical)
|
|
||||||
private const double CrankRadius = Stroke / 2.0;
|
|
||||||
private const double Obliquity = CrankRadius / ConRodLength;
|
|
||||||
private const double CompressionRatio = 6.7; // from Jones
|
|
||||||
|
|
||||||
// Derived volumes
|
|
||||||
private double sweptVolume;
|
|
||||||
private double clearanceVolume;
|
|
||||||
|
|
||||||
// Port timing (degrees from TDC)
|
|
||||||
private const double ExhaustPortOpens = 98.0; // °ATDC
|
|
||||||
private const double ExhaustPortCloses = 262.0; // °ATDC
|
|
||||||
private const double PortWidth = 0.025; // m (estimated)
|
|
||||||
private const double MaxPortArea = 0.001; // m² (fully open)
|
|
||||||
|
|
||||||
// Engine state
|
|
||||||
private double crankAngle; // rad
|
|
||||||
private double engineSpeed; // rad/s
|
|
||||||
private bool combustionPending; // true when ready to fire at TDC
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
private int stepCount;
|
private int stepCount;
|
||||||
|
|
||||||
|
public double ThrottleArea { get; set; } = 0.0; // controlled externally
|
||||||
|
|
||||||
public override void Initialize(int sampleRate)
|
public override void Initialize(int sampleRate)
|
||||||
{
|
{
|
||||||
dt = 1.0 / sampleRate;
|
dt = 1.0 / sampleRate;
|
||||||
|
|
||||||
// Audio
|
|
||||||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||||||
|
|
||||||
// Solver
|
|
||||||
solver = new Solver();
|
solver = new Solver();
|
||||||
solver.SetTimeStep(dt);
|
solver.SetTimeStep(dt);
|
||||||
solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses
|
solver.CflTarget = 0.9;
|
||||||
|
|
||||||
// Compute engine volumes
|
// ---- Cylinder (no valve overlap to avoid backflow) ----
|
||||||
double boreArea = Math.PI * 0.25 * Bore * Bore;
|
double bore = 0.056, stroke = 0.050, conRod = 0.110, compRatio = 10.0;
|
||||||
sweptVolume = boreArea * Stroke;
|
double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0;
|
||||||
clearanceVolume = sweptVolume / (CompressionRatio - 1.0);
|
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, 1000)
|
||||||
double initialVolume = clearanceVolume; // at TDC
|
|
||||||
|
|
||||||
// Cylinder
|
|
||||||
cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
|
|
||||||
{
|
{
|
||||||
Dvdt = 0.0
|
MaxIntakeArea = 0.00037,
|
||||||
|
MaxExhaustArea = 0.00037,
|
||||||
};
|
};
|
||||||
solver.AddComponent(cylinder);
|
solver.AddComponent(cylinder);
|
||||||
|
|
||||||
// Exhaust pipe (1 m, 1 cm², 100 cells)
|
double pipeArea = 0.00037; // 3.7 cm²
|
||||||
exhaustPipe = new Pipe1D(0.5, 10e-4, 20);
|
|
||||||
|
// ---- Pipes ----
|
||||||
|
intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); // short pipe before throttle
|
||||||
|
intakeRunner = new Pipe1D(0.10, pipeArea, 5); // runner after plenum
|
||||||
|
exhaustPipe = new Pipe1D(1.00, pipeArea, 80);
|
||||||
|
solver.AddComponent(intakePipeBeforeThrottle);
|
||||||
|
solver.AddComponent(intakeRunner);
|
||||||
solver.AddComponent(exhaustPipe);
|
solver.AddComponent(exhaustPipe);
|
||||||
|
|
||||||
// Exhaust port – orifice with variable area
|
// ---- Plenum (100 mL) ----
|
||||||
var cylPort = cylinder.CreatePort();
|
intakePlenum = new Volume0D(0.0001, 101325.0, 300.0); // 0.0001 m³
|
||||||
exhaustPort = new OrificeLink(cylPort, exhaustPipe, isPipeLeftEnd: true,
|
var plenumInlet = intakePlenum.CreatePort(); // from throttle
|
||||||
areaProvider: () => ComputeExhaustPortArea(crankAngle))
|
var plenumOutlet = intakePlenum.CreatePort(); // to runner
|
||||||
{
|
solver.AddComponent(intakePlenum);
|
||||||
DischargeCoefficient = 0.8,
|
|
||||||
UseInertance = false
|
|
||||||
};
|
|
||||||
solver.AddOrificeLink(exhaustPort);
|
|
||||||
|
|
||||||
// Pipe open end
|
// ---- Intake open end (ambient → left end of first pipe) ----
|
||||||
pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
|
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
|
||||||
{
|
{
|
||||||
AmbientPressure = 101325.0,
|
AmbientPressure = 101325.0,
|
||||||
Gamma = 1.4
|
Gamma = 1.4
|
||||||
};
|
};
|
||||||
solver.AddOpenEndLink(pipeOpenEnd);
|
solver.AddOpenEndLink(intakeOpenEnd);
|
||||||
|
|
||||||
// Crankshaft (3000 rpm)
|
// ---- Throttle orifice (first pipe right end → plenum inlet) ----
|
||||||
crankshaft = new Crankshaft(initialRPM: 10000.0);
|
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||||||
crankAngle = crankshaft.CrankAngle;
|
areaProvider: () => ThrottleArea)
|
||||||
engineSpeed = crankshaft.AngularVelocity;
|
{
|
||||||
combustionPending = false; // first combustion will occur at next TDC
|
DischargeCoefficient = 0.1, // realistic throttle Cd
|
||||||
|
UseInertance = false
|
||||||
|
};
|
||||||
|
solver.AddOrificeLink(throttleOrifice);
|
||||||
|
|
||||||
|
// ---- Plenum → runner (fixed area = pipe area) ----
|
||||||
|
plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true,
|
||||||
|
areaProvider: () => pipeArea)
|
||||||
|
{
|
||||||
|
DischargeCoefficient = 1.0,
|
||||||
|
UseInertance = false
|
||||||
|
};
|
||||||
|
solver.AddOrificeLink(plenumToRunner);
|
||||||
|
|
||||||
|
// ---- Intake valve (runner right end → cylinder intake port) ----
|
||||||
|
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);
|
||||||
|
|
||||||
stepCount = 0;
|
stepCount = 0;
|
||||||
|
Console.WriteLine("4‑Stroke engine test (plenum + two pipes)");
|
||||||
Console.WriteLine("2‑Stroke engine test");
|
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
|
||||||
Console.WriteLine($"Engine: {Bore*1000:F0} mm x {Stroke*1000:F0} mm, {sweptVolume*1e6:F0} cc");
|
Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)");
|
||||||
Console.WriteLine($"Compression ratio: {CompressionRatio:F1}, clearance volume: {clearanceVolume*1e6:F2} cc");
|
|
||||||
Console.WriteLine($"Exhaust port opens at {ExhaustPortOpens}° ATDC, closes at {ExhaustPortCloses}° ATDC");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Port area vs crank angle (linear ramp, symmetric) ----
|
|
||||||
private double ComputeExhaustPortArea(double thetaRad)
|
|
||||||
{
|
|
||||||
double thetaDeg = thetaRad * 180.0 / Math.PI;
|
|
||||||
|
|
||||||
// Wrap to [0,360) for easier logic
|
|
||||||
thetaDeg %= 360.0;
|
|
||||||
|
|
||||||
// Exhaust open period
|
|
||||||
if (thetaDeg >= ExhaustPortOpens && thetaDeg <= ExhaustPortCloses)
|
|
||||||
{
|
|
||||||
// Ramp up from 0 to Max, then back down
|
|
||||||
double halfPeriod = (ExhaustPortCloses - ExhaustPortOpens) / 2.0;
|
|
||||||
double midPoint = ExhaustPortOpens + halfPeriod;
|
|
||||||
double distFromMid = Math.Abs(thetaDeg - midPoint) / halfPeriod;
|
|
||||||
double fraction = 1.0 - distFromMid;
|
|
||||||
fraction = Math.Clamp(fraction, 0.0, 1.0);
|
|
||||||
return MaxPortArea * fraction;
|
|
||||||
}
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Cylinder volume vs crank angle (slider‑crank) ----
|
|
||||||
private double ComputeCylinderVolume(double thetaRad)
|
|
||||||
{
|
|
||||||
// thetaRad = crank angle from TDC (0 at TDC)
|
|
||||||
double r = CrankRadius;
|
|
||||||
double l = ConRodLength;
|
|
||||||
double cosTh = Math.Cos(thetaRad);
|
|
||||||
double sinTh = Math.Sin(thetaRad);
|
|
||||||
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
|
|
||||||
double x = r * (1.0 - cosTh) + l * (1.0 - term);
|
|
||||||
double area = Math.PI * 0.25 * Bore * Bore;
|
|
||||||
double deltaV = area * x;
|
|
||||||
return clearanceVolume + deltaV;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Combustion: set cylinder pressure AND temperature ----
|
|
||||||
private void Combustion()
|
|
||||||
{
|
|
||||||
double peakPressure = 20.0 * Units.atm; // 30 bar
|
|
||||||
double peakTemperature = 2000.0; // K
|
|
||||||
cylinder.SetPressure(peakPressure, peakTemperature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override float Process()
|
public override float Process()
|
||||||
{
|
{
|
||||||
// Previous crank angle for detecting TDC crossing
|
// 1. Advance crankshaft & pre‑step
|
||||||
double prevAngle = crankshaft.CrankAngle;
|
cylinder.Crankshaft.Step(dt);
|
||||||
|
cylinder.PreStep(dt);
|
||||||
|
|
||||||
// Advance crankshaft
|
// 2. Run solver
|
||||||
crankshaft.Step(dt);
|
|
||||||
crankAngle = crankshaft.CrankAngle;
|
|
||||||
engineSpeed = crankshaft.AngularVelocity;
|
|
||||||
|
|
||||||
// Update cylinder volume to match current crank angle
|
|
||||||
double newVolume = ComputeCylinderVolume(crankAngle);
|
|
||||||
cylinder.Dvdt = (newVolume - cylinder.Volume) / dt;
|
|
||||||
cylinder.Volume = newVolume;
|
|
||||||
|
|
||||||
// ----- Ignition (once per revolution at TDC) -----
|
|
||||||
const double TwoPi = 2.0 * Math.PI;
|
|
||||||
double prevMod = prevAngle % TwoPi;
|
|
||||||
double currMod = crankAngle % TwoPi;
|
|
||||||
|
|
||||||
// Detect crossing of 0 mod 2π (TDC) – going from near 2π to near 0
|
|
||||||
if (prevMod > Math.PI * 1.8 && currMod < Math.PI * 0.2)
|
|
||||||
{
|
|
||||||
if (!combustionPending)
|
|
||||||
{
|
|
||||||
Combustion();
|
|
||||||
combustionPending = true; // prevent multiple firings during the crossing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (currMod > Math.PI * 0.2 && currMod < Math.PI * 1.8)
|
|
||||||
{
|
|
||||||
combustionPending = false; // reset flag once clear of TDC
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run solver
|
|
||||||
solver.Step();
|
solver.Step();
|
||||||
stepCount++;
|
stepCount++;
|
||||||
|
|
||||||
// Log every 500 steps
|
// 3. Log every 200 steps
|
||||||
if (stepCount % 50000 == 0)
|
if (stepCount % 200 == 0)
|
||||||
{
|
{
|
||||||
int midCell = exhaustPipe.CellCount / 2;
|
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 cylP_bar = cylinder.Pressure / 1e5;
|
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " +
|
||||||
double cylT_K = cylinder.Temperature;
|
$"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " +
|
||||||
double cylVol_cc = cylinder.Volume * 1e6;
|
$"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar");
|
||||||
|
Console.WriteLine($"Throttle area = {ThrottleArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
|
||||||
double pipeL_bar = exhaustPipe.GetCellPressure(0) / 1e5;
|
|
||||||
double pipeM_bar = exhaustPipe.GetCellPressure(midCell) / 1e5;
|
|
||||||
double pipeR_bar = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
|
|
||||||
|
|
||||||
double mdotExh = exhaustPort.LastMassFlowRate; // kg/s, positive into cylinder
|
|
||||||
double mdotOpen = pipeOpenEnd.LastMassFlowRate; // kg/s, positive out
|
|
||||||
|
|
||||||
Console.WriteLine(
|
|
||||||
$"Step {stepCount}: Angle={crankAngle*180.0/Math.PI % 360.0:F1}°, " +
|
|
||||||
$"CylP={cylP_bar:F2} bar, CylT={cylT_K:F0} K, Vol={cylVol_cc:F1} cc, " +
|
|
||||||
$"PipeL={pipeL_bar:F2} bar, PipeM={pipeM_bar:F2} bar, PipeR={pipeR_bar:F2} bar, " +
|
|
||||||
$"mdot_exh={mdotExh:E4} kg/s, mdot_open={mdotOpen:E4} kg/s"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
|
return soundProcessor.Process(exhaustOpenEnd);
|
||||||
{
|
|
||||||
Console.WriteLine("NaN detected – stopping.");
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio from open end
|
|
||||||
return soundProcessor.Process(pipeOpenEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw(RenderWindow target)
|
public override void Draw(RenderWindow target)
|
||||||
{
|
{
|
||||||
float winWidth = target.GetView().Size.X;
|
float winW = target.GetView().Size.X;
|
||||||
float winHeight = target.GetView().Size.Y;
|
float winH = target.GetView().Size.Y;
|
||||||
float pipeCenterY = winHeight / 2f;
|
|
||||||
float margin = 60f;
|
// Fixed vertical centres for intake and exhaust
|
||||||
float pipeStartX = margin;
|
float intakeY = winH / 2f - 40f;
|
||||||
float pipeEndX = winWidth - margin;
|
float exhaustY = winH / 2f + 80f;
|
||||||
DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
|
|
||||||
|
// ---- 1. Open end (ambient air source) ----
|
||||||
|
float openEndX = 40f;
|
||||||
|
var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan };
|
||||||
|
openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f);
|
||||||
|
target.Draw(openEndMark);
|
||||||
|
|
||||||
|
// ---- 2. First intake pipe (ambient → throttle) ----
|
||||||
|
float pipe1StartX = openEndX;
|
||||||
|
float pipe1EndX = pipe1StartX + 120f;
|
||||||
|
DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX);
|
||||||
|
|
||||||
|
// ---- 3. Throttle (symbolic restriction) ----
|
||||||
|
float throttleX = pipe1EndX + 5f;
|
||||||
|
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
|
||||||
|
{
|
||||||
|
FillColor = Color.Yellow,
|
||||||
|
Position = new Vector2f(throttleX, intakeY - 15f)
|
||||||
|
};
|
||||||
|
target.Draw(throttleRect);
|
||||||
|
|
||||||
|
// ---- 4. 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);
|
||||||
|
|
||||||
|
// ---- 5. Runner pipe (plenum → cylinder) ----
|
||||||
|
float runnerStartX = plenLeftX + plenW + 5f;
|
||||||
|
float runnerEndX = runnerStartX + 100f;
|
||||||
|
DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX);
|
||||||
|
|
||||||
|
// ---- 6. Cylinder ----
|
||||||
|
float cylCX = runnerEndX + 50f; // center X
|
||||||
|
float cylTopY = intakeY - 120f; // top of cylinder (so it sits above the pipe)
|
||||||
|
float cylW = 80f, cylMaxH = 240f;
|
||||||
|
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
|
||||||
|
|
||||||
|
// ---- 7. Exhaust pipe (cylinder → open end) ----
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
trace.nettrace
BIN
trace.nettrace
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user