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 Inertia { get; set; } = 0.2;
|
||||
public double FrictionConstant { get; set; } = 2.0; // N·m
|
||||
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
|
||||
public double FrictionConstant { get; set; } = 0.0; // N·m
|
||||
public double FrictionViscous { get; set; } = 0.000; // N·m per rad/s
|
||||
|
||||
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.Diagnostics;
|
||||
using FluidSim.Interfaces;
|
||||
|
||||
namespace FluidSim.Components
|
||||
@@ -9,6 +10,9 @@ namespace FluidSim.Components
|
||||
/// </summary>
|
||||
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 PortB { get; }
|
||||
public double Area { get; }
|
||||
@@ -32,7 +36,7 @@ namespace FluidSim.Components
|
||||
private readonly double _gamma = 1.4;
|
||||
|
||||
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 _rhoGhostR, _uGhostR, _pGhostR;
|
||||
@@ -41,6 +45,14 @@ namespace FluidSim.Components
|
||||
private double _laminarCoeff;
|
||||
private double _ambientEnergyReference;
|
||||
|
||||
// ---------- Profiling accumulators ----------
|
||||
private long _profPrecomputeTicks;
|
||||
private long _profLeftFluxTicks;
|
||||
private long _profInteriorLoopTicks;
|
||||
private long _profRightFluxTicks;
|
||||
private long _profPortUpdateTicks;
|
||||
private long _profCallCount;
|
||||
|
||||
public Pipe1D(double length, double area, int cellCount)
|
||||
{
|
||||
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
|
||||
@@ -128,84 +140,142 @@ namespace FluidSim.Components
|
||||
|
||||
double dt = dtSub;
|
||||
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 coeff = _laminarCoeff * DampingMultiplier;
|
||||
double relaxRate = EnergyRelaxationRate;
|
||||
double gamma = _gamma;
|
||||
double gm1 = gamma - 1.0;
|
||||
|
||||
// ---------- Profiling start ----------
|
||||
long t0 = 0, t1 = 0;
|
||||
if (EnableDetailedProfiling)
|
||||
{
|
||||
t0 = Stopwatch.GetTimestamp();
|
||||
_profCallCount++;
|
||||
}
|
||||
|
||||
// ---------- Phase 1: Pre‑compute pressure and speed of sound ----------
|
||||
double[] p = new double[n];
|
||||
double[] c = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double rho = Math.Max(_rho[i], 1e-12);
|
||||
double u = _rhou[i] / rho;
|
||||
p[i] = gm1 * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho);
|
||||
c[i] = Math.Sqrt(gamma * p[i] / rho);
|
||||
}
|
||||
|
||||
if (EnableDetailedProfiling)
|
||||
{
|
||||
t1 = Stopwatch.GetTimestamp();
|
||||
_profPrecomputeTicks += (t1 - t0);
|
||||
t0 = t1;
|
||||
}
|
||||
|
||||
// ---------- 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 ru = _rhou[i];
|
||||
double E = _E[i];
|
||||
|
||||
double dM = _fluxM[i + 1] - _fluxM[i];
|
||||
double dP = _fluxP[i + 1] - _fluxP[i];
|
||||
double dE_flux = _fluxE[i + 1] - _fluxE[i];
|
||||
|
||||
double newR = r - dt_dx * dM;
|
||||
double newRu = ru - dt_dx * dP;
|
||||
double newE = E - dt_dx * dE_flux;
|
||||
double newR = r - dt_dx * (fluxM_right - fluxM_prev);
|
||||
double newRu = ru - dt_dx * (fluxP_right - fluxP_prev);
|
||||
double newE = E - dt_dx * (fluxE_right - fluxE_prev);
|
||||
|
||||
double 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 / (_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);
|
||||
|
||||
_rho[i] = newR;
|
||||
@@ -213,43 +283,68 @@ namespace FluidSim.Components
|
||||
_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();
|
||||
PortA.Pressure = pA; PortA.Density = rhoA;
|
||||
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();
|
||||
PortB.Pressure = pB; PortB.Density = rhoB;
|
||||
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,
|
||||
double rR, double uR, double pR, double eR,
|
||||
out double fm, out double fp, out double fe)
|
||||
{
|
||||
// Primitive states
|
||||
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;
|
||||
|
||||
// Conserved vectors U = (ρ, ρu, E)
|
||||
// Flux F = (ρu, ρu²+p, (E+p)u)
|
||||
double Fm_L = rhoL * uL;
|
||||
double Fp_L = rhoL * uL * uL + pL;
|
||||
double Fe_L = (EL + pL) * uL;
|
||||
|
||||
double Fm_R = rhoR * uR;
|
||||
double Fp_R = rhoR * uR * uR + pR;
|
||||
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 cR = Math.Sqrt(_gamma * pR / rR);
|
||||
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
|
||||
|
||||
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);
|
||||
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);
|
||||
_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 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 LastFaceVelocity { get; private set; }
|
||||
public double LastFacePressure { get; private set; }
|
||||
@@ -41,10 +42,10 @@ namespace FluidSim.Core
|
||||
}
|
||||
|
||||
// Gather states
|
||||
double volP = VolumePort.Pressure;
|
||||
double volP = VolumePort.Pressure;
|
||||
double volRho = VolumePort.Density;
|
||||
double volT = VolumePort.Temperature;
|
||||
double volH = VolumePort.SpecificEnthalpy;
|
||||
double volT = VolumePort.Temperature;
|
||||
double volH = VolumePort.SpecificEnthalpy;
|
||||
|
||||
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
@@ -52,25 +53,34 @@ namespace FluidSim.Core
|
||||
|
||||
double pipeT = pipeP / Math.Max(pipeRho * 287.0, 1e-12);
|
||||
double gamma = 1.4;
|
||||
double R = 287.0;
|
||||
double R = 287.0;
|
||||
|
||||
// ---- 1. Steady‑state nozzle solution (gives correct exit pressure) ----
|
||||
double mdotSS;
|
||||
// ---- Steady‑state nozzle solution (gives correct exit state) ----
|
||||
double mdotSS; // positive = volume → pipe
|
||||
double rhoFace0, uFace0, pFace0;
|
||||
if (volP >= pipeP)
|
||||
{
|
||||
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
|
||||
out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
|
||||
mdotSS = mdotUpToDown; // volume → pipe
|
||||
mdotSS = mdotUpToDown;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
|
||||
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)
|
||||
{
|
||||
double rhoUp = _mdot >= 0 ? volRho : pipeRho;
|
||||
@@ -85,39 +95,39 @@ namespace FluidSim.Core
|
||||
_mdot = mdotSS;
|
||||
}
|
||||
|
||||
// Clamp outflow to available mass
|
||||
// Clamp outflow to available mass (if finite volume)
|
||||
if (VolumePort.Owner is Volume0D vol)
|
||||
{
|
||||
double maxOut = vol.Mass / dtSub;
|
||||
if (_mdot > maxOut) _mdot = maxOut;
|
||||
}
|
||||
|
||||
// ---- 3. Ghost state (use nozzle‑exit pressure!) ----
|
||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho; // upstream density
|
||||
double pFace = pFace0; // correct exit pressure (choked/subsonic)
|
||||
// ---- Ghost state ----
|
||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho;
|
||||
double pFace = pFace0;
|
||||
double mdotMag = Math.Abs(_mdot);
|
||||
double uFace = mdotMag / (rhoFace * area);
|
||||
double uFace = mdotMag / (rhoFace * area);
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
uFace = _mdot >= 0 ? uFace : -uFace; // left: +u into pipe
|
||||
uFace = _mdot >= 0 ? uFace : -uFace;
|
||||
else
|
||||
uFace = _mdot >= 0 ? -uFace : uFace; // right: +u out of pipe
|
||||
uFace = _mdot >= 0 ? -uFace : uFace;
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
||||
|
||||
// Store for monitoring
|
||||
double mdotIntoVolume = -_mdot;
|
||||
LastMassFlowRate = mdotIntoVolume;
|
||||
LastFaceDensity = rhoFace;
|
||||
// Store results (positive = into volume)
|
||||
LastMassFlowRate = -_mdot;
|
||||
LastFaceDensity = rhoFace;
|
||||
LastFaceVelocity = uFace;
|
||||
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);
|
||||
VolumePort.SpecificEnthalpy = hPipe;
|
||||
@@ -140,7 +150,7 @@ namespace FluidSim.Core
|
||||
Pipe.SetGhostRight(rInt, -uInt, pInt);
|
||||
|
||||
LastMassFlowRate = 0.0;
|
||||
LastFaceDensity = rInt;
|
||||
LastFaceDensity = rInt;
|
||||
LastFaceVelocity = 0.0;
|
||||
LastFacePressure = pInt;
|
||||
if (VolumePort != null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using FluidSim.Components;
|
||||
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>
|
||||
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 AddComponent(IComponent component) => _components.Add(component);
|
||||
@@ -30,11 +45,16 @@ namespace FluidSim.Core
|
||||
var pipes = _components.OfType<Pipe1D>().ToList();
|
||||
if (pipes.Count == 0) return;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// CFL count
|
||||
int nSub = 1;
|
||||
foreach (var p in pipes)
|
||||
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt, CflTarget));
|
||||
double dtSub = _dt / nSub;
|
||||
|
||||
_timeCFL += sw.Elapsed.TotalSeconds;
|
||||
|
||||
const int maxSubSteps = 10000;
|
||||
if (nSub > maxSubSteps)
|
||||
{
|
||||
@@ -44,21 +64,86 @@ namespace FluidSim.Core
|
||||
|
||||
for (int sub = 0; sub < nSub; sub++)
|
||||
{
|
||||
double t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var link in _orificeLinks)
|
||||
link.Resolve(dtSub);
|
||||
_timeOrifice += sw.Elapsed.TotalSeconds - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var link in _openEndLinks)
|
||||
link.Resolve(dtSub);
|
||||
_timeOpenEnd += sw.Elapsed.TotalSeconds - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var junc in _junctions)
|
||||
junc.Resolve(dtSub);
|
||||
_timeJunction += sw.Elapsed.TotalSeconds - t0;
|
||||
|
||||
t0 = sw.Elapsed.TotalSeconds;
|
||||
foreach (var p in pipes)
|
||||
p.SimulateSingleStep(dtSub);
|
||||
_timePipe += sw.Elapsed.TotalSeconds - t0;
|
||||
}
|
||||
|
||||
double tCG = sw.Elapsed.TotalSeconds;
|
||||
foreach (var p in pipes)
|
||||
p.ClearGhostFlags();
|
||||
_timeClearGhosts += sw.Elapsed.TotalSeconds - tCG;
|
||||
|
||||
double tUS = sw.Elapsed.TotalSeconds;
|
||||
foreach (var comp in _components)
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
// 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>
|
||||
<None Update="fonts\FiraCodeNerdFont-Medium.ttf">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="fonts\LiberationMono-Regular.ttf">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
||||
311
Program.cs
311
Program.cs
@@ -1,9 +1,13 @@
|
||||
using SFML.Graphics;
|
||||
using SFML.Window;
|
||||
using SFML.System;
|
||||
using System.Diagnostics;
|
||||
using FluidSim.Audio;
|
||||
using FluidSim.Core;
|
||||
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;
|
||||
|
||||
@@ -11,190 +15,211 @@ public class Program
|
||||
{
|
||||
private const int SampleRate = 44100;
|
||||
private const double DrawFrequency = 60.0;
|
||||
private static Scenario scenario;
|
||||
|
||||
// Speed control
|
||||
private static double desiredSpeed = 0.01;
|
||||
private static double currentSpeed = desiredSpeed;
|
||||
// Playback speed
|
||||
private static double _desiredSpeed = 0.01;
|
||||
private static double _currentDisplaySpeed = _desiredSpeed;
|
||||
private const double MinSpeed = 0.0001;
|
||||
private const double MaxSpeed = 1.0;
|
||||
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 bool isRealTime = false;
|
||||
private static volatile bool _timeWarpActive;
|
||||
|
||||
// Throttle smoothing (unused but kept)
|
||||
private static double targetThrottle = 0.0;
|
||||
private static double currentThrottle = 0.0;
|
||||
private const double ThrottleSmoothing = 20.0;
|
||||
// Thread load tracking
|
||||
private static ThreadLoadTracker _loadTracker = new ThreadLoadTracker();
|
||||
|
||||
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 ----
|
||||
private static Font? overlayFont;
|
||||
private static Text? overlayText;
|
||||
// Throttle control
|
||||
private static double _throttleTarget = 1.0; // 0‑1, set by arrow keys
|
||||
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()
|
||||
{
|
||||
var mode = new VideoMode(new Vector2u(1280, 720));
|
||||
var window = new RenderWindow(mode, "FluidSim");
|
||||
window.SetVerticalSyncEnabled(true);
|
||||
window.Closed += (_, _) => { running = false; window.Close(); };
|
||||
window.MouseWheelScrolled += OnMouseWheel;
|
||||
window.KeyPressed += OnKeyPressed;
|
||||
var window = CreateWindow();
|
||||
LoadFont();
|
||||
_scenario = (TestScenario)InitializeScenario();
|
||||
_lastThrottleUpdateTime = 0.0;
|
||||
|
||||
// ---- Load font ----
|
||||
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
|
||||
}
|
||||
_simRingBuffer = new SimulationRingBuffer(131072);
|
||||
_soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 };
|
||||
_soundEngine.Start();
|
||||
|
||||
if (overlayFont != null)
|
||||
{
|
||||
// SFML 3 Text(font, character size in pixels)
|
||||
overlayText = new Text(overlayFont)
|
||||
{
|
||||
FillColor = Color.White,
|
||||
Position = new Vector2f(10, 10)
|
||||
};
|
||||
}
|
||||
|
||||
var soundEngine = new SoundEngine(bufferCapacity: 16384);
|
||||
soundEngine.Volume = 100;
|
||||
soundEngine.Start();
|
||||
|
||||
scenario = new TestScenario();
|
||||
scenario.Initialize(SampleRate);
|
||||
var cts = new CancellationTokenSource();
|
||||
Task.Run(() => SimulationLoop(cts.Token), cts.Token);
|
||||
|
||||
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();
|
||||
|
||||
double currentRealTime = stopwatch.Elapsed.TotalSeconds;
|
||||
double dtClock = currentRealTime - lastSpeedUpdateTime;
|
||||
lastSpeedUpdateTime = currentRealTime;
|
||||
double now = stopwatch.Elapsed.TotalSeconds;
|
||||
|
||||
// Smooth simulation speed
|
||||
double speedSmoothing = 8.0;
|
||||
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
|
||||
// ---- Playback speed smoothing ----
|
||||
double targetSpeed = _timeWarpActive ? 1.0 : _desiredSpeed;
|
||||
_currentDisplaySpeed += (targetSpeed - _currentDisplaySpeed) *
|
||||
(1.0 - Math.Exp(-8.0 * (now - lastDrawTime)));
|
||||
_soundEngine.Speed = _currentDisplaySpeed;
|
||||
|
||||
// Generate audio
|
||||
double targetAudioClock = currentRealTime + 0.05;
|
||||
while (totalOutputSamples < targetAudioClock * SampleRate && running)
|
||||
// ---- 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)
|
||||
{
|
||||
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;
|
||||
_throttleCurrent = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double smoothing = 1.0 - Math.Exp(-ThrottleLerpRate * dtThrottle);
|
||||
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
|
||||
}
|
||||
|
||||
// Drawing
|
||||
if (currentRealTime - lastDrawTime >= drawInterval)
|
||||
{
|
||||
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
|
||||
double realtimePercent = totalOutputSamples / (currentRealTime * SampleRate) * 100.0;
|
||||
double actualArea = ThrottleMinArea + (ThrottleMaxArea - ThrottleMinArea) * _throttleCurrent;
|
||||
_scenario.ThrottleArea = actualArea;
|
||||
|
||||
// Update overlay text
|
||||
if (overlayText != null)
|
||||
// ---- Drawing ----
|
||||
if (now - lastDrawTime >= 1.0 / DrawFrequency)
|
||||
{
|
||||
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}%";
|
||||
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);
|
||||
|
||||
// Draw the overlay on top
|
||||
if (overlayText != null)
|
||||
window.Draw(overlayText);
|
||||
|
||||
_scenario.Draw(window);
|
||||
if (_overlayText != null) window.Draw(_overlayText);
|
||||
window.Display();
|
||||
lastDrawTime = currentRealTime;
|
||||
lastDrawTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
soundEngine.Dispose();
|
||||
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 window = new RenderWindow(mode, "FluidSim");
|
||||
window.SetVerticalSyncEnabled(false);
|
||||
window.SetFramerateLimit(60);
|
||||
window.Closed += (_, _) => window.Close();
|
||||
window.MouseWheelScrolled += OnMouseWheel;
|
||||
window.KeyPressed += OnKeyPressed;
|
||||
window.KeyReleased += OnKeyReleased;
|
||||
return window;
|
||||
}
|
||||
|
||||
private static void LoadFont()
|
||||
{
|
||||
try { _overlayFont = new Font("fonts/FiraCodeNerdFont-Medium.ttf"); }
|
||||
catch { _overlayFont = null; }
|
||||
|
||||
if (_overlayFont != null)
|
||||
_overlayText = new Text(_overlayFont)
|
||||
{
|
||||
FillColor = Color.White,
|
||||
Position = new Vector2f(10, 10)
|
||||
};
|
||||
}
|
||||
|
||||
private static Scenario InitializeScenario()
|
||||
{
|
||||
var sc = new TestScenario();
|
||||
sc.Initialize(SampleRate);
|
||||
return sc;
|
||||
}
|
||||
|
||||
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
||||
{
|
||||
bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
|
||||
if (e.Delta > 0)
|
||||
desiredSpeed *= ScrollFactor;
|
||||
else if (e.Delta < 0)
|
||||
desiredSpeed /= ScrollFactor;
|
||||
|
||||
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;
|
||||
if (_timeWarpActive) return;
|
||||
if (e.Delta > 0) _desiredSpeed *= ScrollFactor;
|
||||
else if (e.Delta < 0) _desiredSpeed /= ScrollFactor;
|
||||
_desiredSpeed = Math.Clamp(_desiredSpeed, MinSpeed, MaxSpeed);
|
||||
_lastNormalSpeed = _desiredSpeed;
|
||||
_isRealTime = Math.Abs(_desiredSpeed - 1.0) < 1e-6;
|
||||
}
|
||||
|
||||
private static void OnKeyPressed(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Code == Keyboard.Key.Space)
|
||||
switch (e.Code)
|
||||
{
|
||||
if (isRealTime)
|
||||
desiredSpeed = lastDesiredSpeed;
|
||||
else
|
||||
desiredSpeed = 1.0;
|
||||
isRealTime = !isRealTime;
|
||||
case Keyboard.Key.Space:
|
||||
_timeWarpActive = !_timeWarpActive;
|
||||
if (!_timeWarpActive)
|
||||
{
|
||||
_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
|
||||
{
|
||||
/// <summary>Initialize the scenario with a given audio sample rate.</summary>
|
||||
public abstract void Initialize(int sampleRate);
|
||||
|
||||
/// <summary>Advance one simulation step and return an audio sample.</summary>
|
||||
public abstract float Process();
|
||||
|
||||
/// <summary>Draw the current simulation state onto the given SFML render target.</summary>
|
||||
public abstract void Draw(RenderWindow target);
|
||||
|
||||
// ---------- Shared drawing helpers ----------
|
||||
|
||||
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)
|
||||
{
|
||||
// Clamp to the range we want to display
|
||||
double t = Math.Clamp(temperature, 0.0, 2000.0);
|
||||
|
||||
byte r, g, b;
|
||||
if (t < AmbientTemperature)
|
||||
{
|
||||
// Blue → Green
|
||||
double factor = t / AmbientTemperature; // 0 at 0 K, 1 at 300 K
|
||||
double factor = t / AmbientTemperature;
|
||||
r = 0;
|
||||
g = (byte)(255 * factor);
|
||||
b = (byte)(255 * (1.0 - factor));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Green → Red
|
||||
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); // 0 at 300 K, 1 at 2000 K
|
||||
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature);
|
||||
r = (byte)(255 * factor);
|
||||
g = (byte)(255 * (1.0 - factor));
|
||||
b = 0;
|
||||
@@ -47,30 +36,84 @@ namespace FluidSim.Tests
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the pipe as a smooth triangle‑strip whose radius varies with cell pressure (for visibility),
|
||||
/// but colored by temperature.
|
||||
/// </summary>
|
||||
// ---------- Draw a generic volume (e.g. plenum) ----------
|
||||
protected void DrawVolume(RenderWindow target, Volume0D volume,
|
||||
float centerX, float topY, float width, float height)
|
||||
{
|
||||
var rect = new RectangleShape(new Vector2f(width, height))
|
||||
{
|
||||
FillColor = 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)
|
||||
{
|
||||
int n = pipe.CellCount;
|
||||
if (n < 2) return;
|
||||
|
||||
float pipeLengthPx = pipeEndX - pipeStartX;
|
||||
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
|
||||
float dx = pipeLengthPx / (n - 1);
|
||||
|
||||
float baseRadius = 25f;
|
||||
float rangeFactor = 2f;
|
||||
float scaleFactor = 2f;
|
||||
|
||||
// ----- smoothstep helper -----
|
||||
static float SmoothStep(float edge0, float edge1, float x)
|
||||
{
|
||||
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
// ----- Pre‑compute cell positions, radii, and temperatures -----
|
||||
var centers = new float[n];
|
||||
var radii = new float[n];
|
||||
var temperatures = new double[n];
|
||||
@@ -80,7 +123,7 @@ namespace FluidSim.Tests
|
||||
{
|
||||
double p = pipe.GetCellPressure(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;
|
||||
|
||||
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
|
||||
@@ -89,7 +132,6 @@ namespace FluidSim.Tests
|
||||
centers[i] = pipeStartX + i * dx;
|
||||
}
|
||||
|
||||
// ----- Build triangle‑strip vertices -----
|
||||
int segmentsPerCell = 8;
|
||||
int totalPoints = n + (n - 1) * segmentsPerCell;
|
||||
Vertex[] stripVertices = new Vertex[totalPoints * 2];
|
||||
@@ -112,7 +154,7 @@ namespace FluidSim.Tests
|
||||
float st = SmoothStep(0f, 1f, t);
|
||||
float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
|
||||
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
|
||||
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; // linear interpolation
|
||||
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
|
||||
Color coli = TemperatureColor(Ti);
|
||||
|
||||
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
|
||||
|
||||
@@ -3,239 +3,218 @@ using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Core;
|
||||
using FluidSim.Utils;
|
||||
|
||||
namespace FluidSim.Tests
|
||||
{
|
||||
public class TestScenario : Scenario
|
||||
{
|
||||
// Simulation core
|
||||
private Solver solver;
|
||||
private double dt;
|
||||
// Engine
|
||||
private Cylinder cylinder;
|
||||
|
||||
// Engine components
|
||||
private Volume0D cylinder;
|
||||
// Intake side
|
||||
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 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;
|
||||
|
||||
// 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 double dt;
|
||||
private int stepCount;
|
||||
|
||||
public double ThrottleArea { get; set; } = 0.0; // controlled externally
|
||||
|
||||
public override void Initialize(int sampleRate)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
|
||||
// Audio
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||||
|
||||
// Solver
|
||||
solver = new Solver();
|
||||
solver.SetTimeStep(dt);
|
||||
solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses
|
||||
solver.CflTarget = 0.9;
|
||||
|
||||
// Compute engine volumes
|
||||
double boreArea = Math.PI * 0.25 * Bore * Bore;
|
||||
sweptVolume = boreArea * Stroke;
|
||||
clearanceVolume = sweptVolume / (CompressionRatio - 1.0);
|
||||
double initialVolume = clearanceVolume; // at TDC
|
||||
|
||||
// Cylinder
|
||||
cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
|
||||
// ---- Cylinder (no valve overlap to avoid backflow) ----
|
||||
double bore = 0.056, stroke = 0.050, conRod = 0.110, compRatio = 10.0;
|
||||
double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0;
|
||||
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, 1000)
|
||||
{
|
||||
Dvdt = 0.0
|
||||
MaxIntakeArea = 0.00037,
|
||||
MaxExhaustArea = 0.00037,
|
||||
};
|
||||
solver.AddComponent(cylinder);
|
||||
|
||||
// Exhaust pipe (1 m, 1 cm², 100 cells)
|
||||
exhaustPipe = new Pipe1D(0.5, 10e-4, 20);
|
||||
double pipeArea = 0.00037; // 3.7 cm²
|
||||
|
||||
// ---- 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);
|
||||
|
||||
// Exhaust port – orifice with variable area
|
||||
var cylPort = cylinder.CreatePort();
|
||||
exhaustPort = new OrificeLink(cylPort, exhaustPipe, isPipeLeftEnd: true,
|
||||
areaProvider: () => ComputeExhaustPortArea(crankAngle))
|
||||
{
|
||||
DischargeCoefficient = 0.8,
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(exhaustPort);
|
||||
// ---- Plenum (100 mL) ----
|
||||
intakePlenum = new Volume0D(0.0001, 101325.0, 300.0); // 0.0001 m³
|
||||
var plenumInlet = intakePlenum.CreatePort(); // from throttle
|
||||
var plenumOutlet = intakePlenum.CreatePort(); // to runner
|
||||
solver.AddComponent(intakePlenum);
|
||||
|
||||
// Pipe open end
|
||||
pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
|
||||
// ---- Intake open end (ambient → left end of first pipe) ----
|
||||
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
|
||||
{
|
||||
AmbientPressure = 101325.0,
|
||||
Gamma = 1.4
|
||||
};
|
||||
solver.AddOpenEndLink(pipeOpenEnd);
|
||||
solver.AddOpenEndLink(intakeOpenEnd);
|
||||
|
||||
// Crankshaft (3000 rpm)
|
||||
crankshaft = new Crankshaft(initialRPM: 10000.0);
|
||||
crankAngle = crankshaft.CrankAngle;
|
||||
engineSpeed = crankshaft.AngularVelocity;
|
||||
combustionPending = false; // first combustion will occur at next TDC
|
||||
// ---- Throttle orifice (first pipe right end → plenum inlet) ----
|
||||
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||||
areaProvider: () => ThrottleArea)
|
||||
{
|
||||
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;
|
||||
|
||||
Console.WriteLine("2‑Stroke engine test");
|
||||
Console.WriteLine($"Engine: {Bore*1000:F0} mm x {Stroke*1000:F0} mm, {sweptVolume*1e6:F0} cc");
|
||||
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);
|
||||
Console.WriteLine("4‑Stroke engine test (plenum + two pipes)");
|
||||
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
|
||||
Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)");
|
||||
}
|
||||
|
||||
public override float Process()
|
||||
{
|
||||
// Previous crank angle for detecting TDC crossing
|
||||
double prevAngle = crankshaft.CrankAngle;
|
||||
// 1. Advance crankshaft & pre‑step
|
||||
cylinder.Crankshaft.Step(dt);
|
||||
cylinder.PreStep(dt);
|
||||
|
||||
// Advance crankshaft
|
||||
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
|
||||
// 2. Run solver
|
||||
solver.Step();
|
||||
stepCount++;
|
||||
|
||||
// Log every 500 steps
|
||||
if (stepCount % 50000 == 0)
|
||||
// 3. Log every 200 steps
|
||||
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;
|
||||
double cylT_K = cylinder.Temperature;
|
||||
double cylVol_cc = cylinder.Volume * 1e6;
|
||||
|
||||
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"
|
||||
);
|
||||
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " +
|
||||
$"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " +
|
||||
$"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar");
|
||||
Console.WriteLine($"Throttle area = {ThrottleArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
|
||||
}
|
||||
|
||||
if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
|
||||
{
|
||||
Console.WriteLine("NaN detected – stopping.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// Audio from open end
|
||||
return soundProcessor.Process(pipeOpenEnd);
|
||||
return soundProcessor.Process(exhaustOpenEnd);
|
||||
}
|
||||
|
||||
public override void Draw(RenderWindow target)
|
||||
{
|
||||
float winWidth = target.GetView().Size.X;
|
||||
float winHeight = target.GetView().Size.Y;
|
||||
float pipeCenterY = winHeight / 2f;
|
||||
float margin = 60f;
|
||||
float pipeStartX = margin;
|
||||
float pipeEndX = winWidth - margin;
|
||||
DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
float winW = target.GetView().Size.X;
|
||||
float winH = target.GetView().Size.Y;
|
||||
|
||||
// Fixed vertical centres for intake and exhaust
|
||||
float intakeY = winH / 2f - 40f;
|
||||
float exhaustY = winH / 2f + 80f;
|
||||
|
||||
// ---- 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