This commit is contained in:
2026-05-05 14:02:07 +02:00
parent f16a1aa763
commit 547e8706f1
6 changed files with 425 additions and 355 deletions

52
Components/Crankshaft.cs Normal file
View File

@@ -0,0 +1,52 @@
// Components/Crankshaft.cs
using System;
namespace FluidSim.Components
{
public class Crankshaft
{
public double AngularVelocity { get; set; } // rad/s
public double CrankAngle { get; set; } // rad, 0 … 4π (fourstroke cycle)
public double PreviousAngle { get; private set; } // for TDC detection
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
private double externalTorque;
/// <param name="initialRPM">Idle speed before any combustion torque is applied.</param>
public Crankshaft(double initialRPM = 400.0)
{
AngularVelocity = initialRPM * 2.0 * Math.PI / 60.0;
CrankAngle = 0.0;
PreviousAngle = 0.0;
}
public void AddTorque(double torque) => externalTorque += torque;
public void Step(double dt)
{
// Save previous angle
PreviousAngle = CrankAngle;
// Friction
double friction = FrictionConstant * Math.Sign(AngularVelocity) + FrictionViscous * AngularVelocity;
double netTorque = externalTorque - friction;
double alpha = netTorque / Inertia;
AngularVelocity += alpha * dt;
if (AngularVelocity < 0) AngularVelocity = 0; // stall
CrankAngle += AngularVelocity * dt;
// Wrap to [0, 4π)
if (CrankAngle >= 4.0 * Math.PI)
CrankAngle -= 4.0 * Math.PI;
else if (CrankAngle < 0)
CrankAngle += 4.0 * Math.PI;
externalTorque = 0.0;
}
}
}

View File

@@ -0,0 +1,215 @@
// EngineCylinder.cs (in Core namespace)
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
public class EngineCylinder
{
public Volume0D Cylinder { get; private set; }
private Crankshaft crankshaft;
private double bore, stroke, conRodLength, compressionRatio;
private double pistonArea;
private double V_disp, V_clear;
private double maxOrificeArea;
private double valveOpenStart = 120.0 * Math.PI / 180.0;
private double valveOpenEnd = 480.0 * Math.PI / 180.0;
private double valveRampWidth = 30.0 * Math.PI / 180.0;
public double OrificeArea => ValveLift() * maxOrificeArea;
public double TargetPeakPressure { get; set; } = 50.0 * 101325.0;
private const double PeakTemperature = 2500.0;
private bool burnInProgress = false;
private double burnStartAngle; // full cycle angle when ignition began
private double burnDuration = 40.0 * Math.PI / 180.0;
private double targetBurnEnergy;
private double totalBurnMass;
private double preIgnitionMass, preIgnitionInternalEnergy;
private Random rand = new Random();
public double MisfireProbability { get; set; } = 0.02;
private bool misfireCurrent = false;
public int CombustionCount { get; private set; }
public int MisfireCount { get; private set; }
public EngineCylinder(Crankshaft crankshaft,
double bore, double stroke, double compressionRatio,
double pipeArea, int sampleRate)
{
this.crankshaft = crankshaft;
this.bore = bore;
this.stroke = stroke;
conRodLength = 2.0 * stroke;
this.compressionRatio = compressionRatio;
maxOrificeArea = pipeArea;
pistonArea = Math.PI / 4.0 * bore * bore;
V_disp = pistonArea * stroke;
V_clear = V_disp / (compressionRatio - 1.0);
// Initial compressed charge at TDC (no burn)
double T_bdc = 300.0;
double p_bdc = 101325.0;
double V_bdc = V_clear + V_disp;
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, 1.4);
Cylinder = new Volume0D(V_clear, p_tdc, T_bdc * Math.Pow(V_bdc / V_clear, 1.4 - 1.0), sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
Cylinder.Volume = V_clear;
Cylinder.Mass = freshMass;
Cylinder.InternalEnergy = p_tdc * V_clear / (1.4 - 1.0);
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
}
// ---- Piston kinematics (uses full cycle angle for position) ----
private (double volume, double dvdt) PistonKinematics(double cycleAngle)
{
// Slider-crank uses 02π, but we want the same motion for 02π (power/exhaust) and 2π4π (intake/compression)
double theta = cycleAngle % (2.0 * Math.PI);
double R = stroke / 2.0;
double cosT = Math.Cos(theta);
double sinT = Math.Sin(theta);
double L = conRodLength;
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
double V = V_clear + pistonArea * s;
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
double dVdθ = pistonArea * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
double dvdt = dVdθ * crankshaft.AngularVelocity;
return (V, dvdt);
}
// ---- Valve lift ----
private double ValveLift()
{
double cycleRad = crankshaft.CrankAngle;
if (cycleRad < valveOpenStart || cycleRad > valveOpenEnd)
return 0.0;
double duration = valveOpenEnd - valveOpenStart;
double ramp = valveRampWidth;
double t = (cycleRad - valveOpenStart) / duration;
double rampFrac = ramp / duration;
if (t < rampFrac)
return t / rampFrac;
else if (t > 1.0 - rampFrac)
return (1.0 - t) / rampFrac;
else
return 1.0;
}
private double WiebeFraction(double angleFromIgnition)
{
if (angleFromIgnition >= burnDuration) return 1.0;
double a = 5.0, m = 2.0;
double x = angleFromIgnition / burnDuration;
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
}
// ---- Torque from pressure ----
private double ComputeTorque()
{
double p = Cylinder.Pressure;
double ambient = 101325.0;
double force = (p - ambient) * pistonArea;
if (force <= 0) return 0.0;
double theta = crankshaft.CrankAngle % (2.0 * Math.PI);
double R = stroke / 2.0;
double L = conRodLength;
double sinT = Math.Sin(theta);
double cosT = Math.Cos(theta);
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
double lever = R * sinT * (1.0 + (R * cosT) / sqrtTerm);
return force * lever;
}
public void Step(double dt)
{
double cycleAngle = crankshaft.CrankAngle;
double prevAngle = crankshaft.PreviousAngle;
// ----- TDC crossing detection (power stroke) -----
// Power stroke TDC occurs at angle 0 (mod 4π). We detect when PreviousAngle was near 4π and CrankAngle wraps to near 0.
bool crossingTDC = (prevAngle > 3.8 * Math.PI && cycleAngle < 0.2 * Math.PI) // normal forward
|| (prevAngle < 0.2 * Math.PI && cycleAngle > 3.8 * Math.PI); // (rare backward, ignore)
if (crossingTDC)
{
misfireCurrent = rand.NextDouble() < MisfireProbability;
// Fresh charge: trapped at BDC, compressed isentropically to V_clear
double T_bdc = 300.0;
double p_bdc = 101325.0;
double V_bdc = V_clear + V_disp;
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
double gamma = 1.4;
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, gamma);
Cylinder.Volume = V_clear;
Cylinder.Mass = freshMass;
Cylinder.InternalEnergy = p_tdc * V_clear / (gamma - 1.0);
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
if (misfireCurrent)
{
MisfireCount++;
}
else
{
double V = V_clear;
targetBurnEnergy = TargetPeakPressure * V / (gamma - 1.0);
totalBurnMass = TargetPeakPressure * V / (287.0 * PeakTemperature);
burnInProgress = true;
burnStartAngle = cycleAngle;
CombustionCount++;
}
}
// ----- Burn progress -----
if (burnInProgress)
{
double angleFromIgnition = cycleAngle - burnStartAngle;
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI; // wrap if needed
if (angleFromIgnition >= burnDuration)
{
Cylinder.Mass = totalBurnMass;
Cylinder.InternalEnergy = targetBurnEnergy;
burnInProgress = false;
}
else
{
double fraction = WiebeFraction(angleFromIgnition);
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction) + targetBurnEnergy * fraction;
Cylinder.Mass = preIgnitionMass * (1.0 - fraction) + totalBurnMass * fraction;
}
}
// ----- Piston motion -----
var (vol, dvdt) = PistonKinematics(cycleAngle);
Cylinder.Volume = vol;
Cylinder.Dvdt = dvdt;
// ----- Torque contribution -----
double torque = ComputeTorque();
crankshaft.AddTorque(torque);
}
}
}

View File

@@ -53,6 +53,10 @@ namespace FluidSim.Components
// Precomputed for damping // Precomputed for damping
private float _laminarCoeff; // 8*mu / r^2, then multiplied by DampingMultiplier private float _laminarCoeff; // 8*mu / r^2, then multiplied by DampingMultiplier
// ---- Energy loss (Newton cooling) ----
private float _ambientEnergyReference; // total energy density at ambient (Pamb / (γ-1))
public float EnergyRelaxationRate { get; set; } = 0.0f; // 1/s
public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0) public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0)
{ {
float dtGlobal = 1f / sampleRate; float dtGlobal = 1f / sampleRate;
@@ -89,12 +93,14 @@ namespace FluidSim.Components
float radius = _diameter * 0.5f; float radius = _diameter * 0.5f;
_laminarCoeff = 8f * mu_air / (radius * radius); // will be multiplied by DampingMultiplier at each step _laminarCoeff = 8f * mu_air / (radius * radius); // will be multiplied by DampingMultiplier at each step
// Ambient reference energy (internal energy per unit volume at 101325 Pa)
_ambientEnergyReference = 101325f / (_gamma - 1f); // ≈ 253312.5 J/m³
PortA = new Port(); PortA = new Port();
PortB = new Port(); PortB = new Port();
} }
// ==================== PUBLIC API (unchanged) ============================ // ==================== PUBLIC API ============================
public void SetABoundaryType(BoundaryType type) => _aBCType = type; public void SetABoundaryType(BoundaryType type) => _aBCType = type;
public void SetBBoundaryType(BoundaryType type) => _bBCType = type; public void SetBBoundaryType(BoundaryType type) => _bBCType = type;
public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p; public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p;
@@ -237,8 +243,6 @@ namespace FluidSim.Components
// --- 2. Internal faces (1 .. n-1) SIMD --------------------------- // --- 2. Internal faces (1 .. n-1) SIMD ---------------------------
int vectorSize = Vector<float>.Count; int vectorSize = Vector<float>.Count;
int lastSimdFace = n - vectorSize; // highest face index that starts a full vector block int lastSimdFace = n - vectorSize; // highest face index that starts a full vector block
// Face index f is between cell f-1 (left) and cell f (right).
// We want to cover faces 1..n-1.
for (int f = 1; f <= lastSimdFace; f += vectorSize) for (int f = 1; f <= lastSimdFace; f += vectorSize)
{ {
SimdInternalFluxBlock(f, vectorSize); SimdInternalFluxBlock(f, vectorSize);
@@ -262,7 +266,7 @@ namespace FluidSim.Components
float pR = PressureScalar(n - 1); float pR = PressureScalar(n - 1);
ComputeRightBoundaryFlux(rhoR, uR, pR, out _fluxM[n], out _fluxP[n], out _fluxE[n]); ComputeRightBoundaryFlux(rhoR, uR, pR, out _fluxM[n], out _fluxP[n], out _fluxE[n]);
// --- 4. Cell update + damping SIMD ------------------------------ // --- 4. Cell update + damping + energy loss SIMD -----------------
SimdCellUpdate(dt); SimdCellUpdate(dt);
} }
@@ -417,14 +421,9 @@ namespace FluidSim.Components
// ==================== SIMD INTERNAL FACE ROUTINE ======================== // ==================== SIMD INTERNAL FACE ROUTINE ========================
private void SimdInternalFluxBlock(int startFace, int count) private void SimdInternalFluxBlock(int startFace, int count)
{ {
// startFace is the first face index; we process 'count' consecutive faces.
// For face f, left cell = f-1, right cell = f.
// We load left and right states for faces [startFace .. startFace+count-1].
int leftIdx = startFace - 1; int leftIdx = startFace - 1;
int rightIdx = startFace; int rightIdx = startFace;
// Load conserved variables for left cells and right cells as vectors.
Vector<float> rL = new Vector<float>(_rho, leftIdx); Vector<float> rL = new Vector<float>(_rho, leftIdx);
Vector<float> ruL = new Vector<float>(_rhou, leftIdx); Vector<float> ruL = new Vector<float>(_rhou, leftIdx);
Vector<float> EL = new Vector<float>(_E, leftIdx); Vector<float> EL = new Vector<float>(_E, leftIdx);
@@ -433,7 +432,6 @@ namespace FluidSim.Components
Vector<float> ruR = new Vector<float>(_rhou, rightIdx); Vector<float> ruR = new Vector<float>(_rhou, rightIdx);
Vector<float> ER = new Vector<float>(_E, rightIdx); Vector<float> ER = new Vector<float>(_E, rightIdx);
// Derived quantities: u = ru / r, p = (gamma-1)*(E - 0.5*ru^2 / r)
Vector<float> uL = ruL / rL; Vector<float> uL = ruL / rL;
Vector<float> uR = ruR / rR; Vector<float> uR = ruR / rR;
@@ -444,35 +442,30 @@ namespace FluidSim.Components
Vector<float> pL = gammaMinus1 * (EL - half * ruL * ruL / rL); Vector<float> pL = gammaMinus1 * (EL - half * ruL * ruL / rL);
Vector<float> pR = gammaMinus1 * (ER - half * ruR * ruR / rR); Vector<float> pR = gammaMinus1 * (ER - half * ruR * ruR / rR);
// Sound speeds
Vector<float> cL = Vector.SquareRoot(gammaVec * pL / rL); Vector<float> cL = Vector.SquareRoot(gammaVec * pL / rL);
Vector<float> cR = Vector.SquareRoot(gammaVec * pR / rR); Vector<float> cR = Vector.SquareRoot(gammaVec * pR / rR);
// Wave speeds
Vector<float> SL = Vector.Min(uL - cL, uR - cR); Vector<float> SL = Vector.Min(uL - cL, uR - cR);
Vector<float> SR = Vector.Max(uL + cL, uR + cR); Vector<float> SR = Vector.Max(uL + cL, uR + cR);
// Star speed
Vector<float> num = (pR - pL) + rL * uL * (SL - uL) - rR * uR * (SR - uR); Vector<float> num = (pR - pL) + rL * uL * (SL - uL) - rR * uR * (SR - uR);
Vector<float> den = rL * (SL - uL) - rR * (SR - uR); Vector<float> den = rL * (SL - uL) - rR * (SR - uR);
Vector<float> Ss = num / den; Vector<float> Ss = num / den;
// Total energy per unit mass (E/rho) for left/right (needed for star region)
Vector<float> eL = EL / rL; Vector<float> eL = EL / rL;
Vector<float> eR = ER / rR; Vector<float> eR = ER / rR;
// --- Compute all four possible flux vectors ---
// Left flux // Left flux
Vector<float> Fm_L = ruL; Vector<float> Fm_L = ruL;
Vector<float> Fp_L = ruL * uL + pL; Vector<float> Fp_L = ruL * uL + pL;
Vector<float> Fe_L = (EL + pL) * uL; // (r*E + p)*u Vector<float> Fe_L = (EL + pL) * uL;
// Right flux // Right flux
Vector<float> Fm_R = ruR; Vector<float> Fm_R = ruR;
Vector<float> Fp_R = ruR * uR + pR; Vector<float> Fp_R = ruR * uR + pR;
Vector<float> Fe_R = (ER + pR) * uR; Vector<float> Fe_R = (ER + pR) * uR;
// Starleft fluxes (when SL < 0 < Ss) // Starleft fluxes
Vector<float> diffL = SL - uL; Vector<float> diffL = SL - uL;
Vector<float> dL_den = SL - Ss; Vector<float> dL_den = SL - Ss;
Vector<float> rsL = rL * diffL / dL_den; Vector<float> rsL = rL * diffL / dL_den;
@@ -482,7 +475,7 @@ namespace FluidSim.Components
Vector<float> Fp_starL = rsL * Ss * Ss + psSL; Vector<float> Fp_starL = rsL * Ss * Ss + psSL;
Vector<float> Fe_starL = (rsL * EsL + psSL) * Ss; Vector<float> Fe_starL = (rsL * EsL + psSL) * Ss;
// Starright fluxes (when Ss < 0 < SR) // Starright fluxes
Vector<float> diffR = SR - uR; Vector<float> diffR = SR - uR;
Vector<float> dR_den = SR - Ss; Vector<float> dR_den = SR - Ss;
Vector<float> rsR = rR * diffR / dR_den; Vector<float> rsR = rR * diffR / dR_den;
@@ -492,14 +485,12 @@ namespace FluidSim.Components
Vector<float> Fp_starR = rsR * Ss * Ss + psSR; Vector<float> Fp_starR = rsR * Ss * Ss + psSR;
Vector<float> Fe_starR = (rsR * EsR + psSR) * Ss; Vector<float> Fe_starR = (rsR * EsR + psSR) * Ss;
// --- Select the correct flux based on wave signs ---
var maskSLge0 = Vector.GreaterThanOrEqual(SL, Vector<float>.Zero); var maskSLge0 = Vector.GreaterThanOrEqual(SL, Vector<float>.Zero);
var maskSRle0 = Vector.LessThanOrEqual(SR, Vector<float>.Zero); var maskSRle0 = Vector.LessThanOrEqual(SR, Vector<float>.Zero);
var maskMiddle = ~(maskSLge0 | maskSRle0); // SL<0 && SR>0 var maskMiddle = ~(maskSLge0 | maskSRle0);
var maskStarL = maskMiddle & Vector.GreaterThanOrEqual(Ss, Vector<float>.Zero); var maskStarL = maskMiddle & Vector.GreaterThanOrEqual(Ss, Vector<float>.Zero);
var maskStarR = maskMiddle & Vector.LessThan(Ss, Vector<float>.Zero); var maskStarR = maskMiddle & Vector.LessThan(Ss, Vector<float>.Zero);
// Start with left flux, override with right/star as needed
Vector<float> fm = Vector.ConditionalSelect(maskSLge0, Fm_L, Vector<float> fm = Vector.ConditionalSelect(maskSLge0, Fm_L,
Vector.ConditionalSelect(maskSRle0, Fm_R, Vector.ConditionalSelect(maskSRle0, Fm_R,
Vector.ConditionalSelect(maskStarL, Fm_starL, Vector.ConditionalSelect(maskStarL, Fm_starL,
@@ -515,13 +506,12 @@ namespace FluidSim.Components
Vector.ConditionalSelect(maskStarL, Fe_starL, Vector.ConditionalSelect(maskStarL, Fe_starL,
Vector.ConditionalSelect(maskStarR, Fe_starR, Vector<float>.Zero)))); Vector.ConditionalSelect(maskStarR, Fe_starR, Vector<float>.Zero))));
// Store to flux arrays at indices startFace .. startFace+count-1
fm.CopyTo(_fluxM, startFace); fm.CopyTo(_fluxM, startFace);
fp.CopyTo(_fluxP, startFace); fp.CopyTo(_fluxP, startFace);
fe.CopyTo(_fluxE, startFace); fe.CopyTo(_fluxE, startFace);
} }
// ==================== SIMD CELL UPDATE + DAMPING ======================== // ==================== SIMD CELL UPDATE + DAMPING + ENERGY LOSS =========
private void SimdCellUpdate(float dt) private void SimdCellUpdate(float dt)
{ {
float dt_dx = dt / _dx; float dt_dx = dt / _dx;
@@ -534,9 +524,11 @@ namespace FluidSim.Components
int n = _n; int n = _n;
int lastSimdCell = n - vectorSize; int lastSimdCell = n - vectorSize;
// Predefine constants used in clamping // Predefined constants used in clamping
Vector<float> half = new Vector<float>(0.5f); Vector<float> half = new Vector<float>(0.5f);
Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f); Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f);
Vector<float> ambientEnergyVec = new Vector<float>(_ambientEnergyReference);
Vector<float> energyRelaxRateVec = new Vector<float>(EnergyRelaxationRate);
for (int i = 0; i <= lastSimdCell; i += vectorSize) for (int i = 0; i <= lastSimdCell; i += vectorSize)
{ {
@@ -559,8 +551,12 @@ namespace FluidSim.Components
Vector<float> newE = E - vDtDx * (flxE_R - flxE_L); Vector<float> newE = E - vDtDx * (flxE_R - flxE_L);
// Damping // Damping
Vector<float> factor = Vector.Exp(-vCoeff / r * vDt); Vector<float> dampingFactor = Vector.Exp(-vCoeff / r * vDt);
newRu *= factor; newRu *= dampingFactor;
// Energy loss (Newton cooling toward ambient)
Vector<float> relaxFactor = Vector.Exp(-energyRelaxRateVec * vDt);
newE = ambientEnergyVec + (newE - ambientEnergyVec) * relaxFactor;
// Clamp density // Clamp density
newR = Vector.Max(newR, new Vector<float>(1e-12f)); newR = Vector.Max(newR, new Vector<float>(1e-12f));
@@ -576,6 +572,7 @@ namespace FluidSim.Components
} }
// Scalar remainder // Scalar remainder
float relaxRate = EnergyRelaxationRate;
for (int i = Math.Max(0, lastSimdCell + 1); i < n; i++) for (int i = Math.Max(0, lastSimdCell + 1); i < n; i++)
{ {
float r = _rho[i]; float r = _rho[i];
@@ -584,15 +581,19 @@ namespace FluidSim.Components
float dM = _fluxM[i + 1] - _fluxM[i]; float dM = _fluxM[i + 1] - _fluxM[i];
float dP = _fluxP[i + 1] - _fluxP[i]; float dP = _fluxP[i + 1] - _fluxP[i];
float dE = _fluxE[i + 1] - _fluxE[i]; float dE_flux = _fluxE[i + 1] - _fluxE[i];
float newR = r - dt_dx * dM; float newR = r - dt_dx * dM;
float newRu = ru - dt_dx * dP; float newRu = ru - dt_dx * dP;
float newE = E - dt_dx * dE; float newE = E - dt_dx * dE_flux;
// Damping // Damping
float factor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt); float dampingFactor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
newRu *= factor; newRu *= dampingFactor;
// Energy loss
float relaxFactor = MathF.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
// Clamps // Clamps
if (newR < 1e-12f) newR = 1e-12f; if (newR < 1e-12f) newR = 1e-12f;

View File

@@ -4,14 +4,20 @@ namespace FluidSim.Core
{ {
public class SoundProcessor public class SoundProcessor
{ {
// Monopole state
private double lastMassFlow = 0.0;
private double dt; private double dt;
private double pipeArea;
private double ambientPressure = 101325.0;
// Resonant bandpass filter (secondorder) // Monopole source state
private double b0, b1, b2, a1, a2; private double lastMassFlow = 0.0;
private double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
private double pipeLength; // Gains
private float masterGain = 0.0005f;
private float pressureGain = 0.12f;
private float turbulenceGain = 0.05f;
private float turbulence = 0.05f;
private PinkNoiseGenerator pinkNoise;
// Reverb (outdoor) // Reverb (outdoor)
private float[] delayLine; private float[] delayLine;
@@ -20,36 +26,11 @@ namespace FluidSim.Core
private float lowpassCoeff = 0.70f; private float lowpassCoeff = 0.70f;
private float lastFeedbackSample = 0f; private float lastFeedbackSample = 0f;
// Turbulence (pink noise scaled by U³) public SoundProcessor(int sampleRate, double pipeDiameterMeters, float reverbTimeMs = 200.0f)
private PinkNoiseGenerator pinkNoise;
private float turbulenceGain = 0.05f;
private double pipeArea;
private double ambientPressure = 101325.0;
// Gains
private float masterGain = 0.0005f;
private float pressureGain = 0.12f;
public SoundProcessor(int sampleRate, double pipeLengthMeters, double pipeDiameterMeters = 0.04, float reverbTimeMs = 200.0f)
{ {
dt = 1.0 / sampleRate; dt = 1.0 / sampleRate;
pipeLength = pipeLengthMeters;
pipeArea = Math.PI * Math.Pow(pipeDiameterMeters / 2.0, 2.0); pipeArea = Math.PI * Math.Pow(pipeDiameterMeters / 2.0, 2.0);
// Design resonant filter at pipe fundamental frequency
double c = 340.0;
double f0 = c / (4.0 * pipeLength);
double Q = 15.0;
double omega = 2.0 * Math.PI * f0;
double alpha = Math.Sin(omega * dt) / (2.0 * Q);
double norm = 1.0 / (1.0 + alpha);
b0 = alpha * norm;
b1 = 0.0;
b2 = -alpha * norm;
a1 = -2.0 * Math.Cos(omega * dt) * norm;
a2 = (1.0 - alpha) * norm;
// Reverb delay line
int delaySamples = (int)(sampleRate * reverbTimeMs / 1000.0); int delaySamples = (int)(sampleRate * reverbTimeMs / 1000.0);
delayLine = new float[delaySamples]; delayLine = new float[delaySamples];
writeIndex = 0; writeIndex = 0;
@@ -62,65 +43,60 @@ namespace FluidSim.Core
get => masterGain; get => masterGain;
set => masterGain = value; set => masterGain = value;
} }
public float PressureGain public float PressureGain
{ {
get => pressureGain; get => pressureGain;
set => pressureGain = value; set => pressureGain = value;
} }
public float TurbulenceGain public float TurbulenceGain
{ {
get => turbulenceGain; get => turbulenceGain;
set => turbulenceGain = value; set => turbulenceGain = value;
} }
public float Turbulence
{
get => turbulence;
set => turbulence = value;
}
public void SetAmbientPressure(double p) => ambientPressure = p; public void SetAmbientPressure(double p) => ambientPressure = p;
public void SetPipeDiameter(double diameterMeters) => pipeArea = Math.PI * Math.Pow(diameterMeters / 2.0, 2.0); public void SetPipeDiameter(double diameterMeters) =>
pipeArea = Math.PI * Math.Pow(diameterMeters / 2.0, 2.0);
public float Process(float massFlow, float pipeEndPressure) public float Process(float massFlow, float pipeEndPressure)
{ {
// 1. Monopole source: d(mdot)/dt // 1. Monopole: d(mdot)/dt
double derivative = (massFlow - lastMassFlow) / dt; double derivative = (massFlow - lastMassFlow) / dt;
derivative = Math.Clamp(derivative, -500, 500);
lastMassFlow = massFlow; lastMassFlow = massFlow;
float monopole = (float)(derivative * masterGain); float monopole = (float)(derivative * masterGain);
// 2. Pressure difference (lowfrequency component) // 2. Pressure component
float pressureDiff = (float)((pipeEndPressure - ambientPressure) / ambientPressure) * pressureGain; float pressureDiff = (float)((pipeEndPressure - ambientPressure) / ambientPressure) * pressureGain;
float mixed = monopole + pressureDiff; float mixed = monopole + pressureDiff;
// DO NOT clamp here let the filter and final clamp handle dynamics
// 3. Resonant bandpass filter // 3. Turbulence: amplitude ∝ U^8
double y = b0 * mixed + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1; x1 = mixed;
y2 = y1; y1 = y;
float resonant = (float)Math.Clamp(y, -1f, 1f);
// 4. Turbulence noise: amplitude ∝ U³ (empirical for low speeds)
double velocity = massFlow / (pipeArea * 1.225); double velocity = massFlow / (pipeArea * 1.225);
double Uref = 100.0; double turbulenceAmp = Math.Pow(Math.Abs(velocity) * turbulence, 3.0);
double turbulenceAmp = Math.Pow(Math.Abs(velocity) / Uref, 3.0);
float pink = pinkNoise.Next() * turbulenceGain * (float)turbulenceAmp; float pink = pinkNoise.Next() * turbulenceGain * (float)turbulenceAmp;
resonant += pink; float combined = mixed + pink;
resonant = Math.Clamp(resonant, -1f, 1f);
// 5. Outdoor reverb // 4. Outdoor reverb
float delayed = delayLine[writeIndex]; float delayed = delayLine[writeIndex];
float filteredDelay = delayed * lowpassCoeff + lastFeedbackSample * (1f - lowpassCoeff); float filteredDelay = delayed * lowpassCoeff + lastFeedbackSample * (1f - lowpassCoeff);
lastFeedbackSample = filteredDelay; lastFeedbackSample = filteredDelay;
float wet = delayed + filteredDelay * feedback; float wet = delayed + filteredDelay * feedback;
delayLine[writeIndex] = resonant + filteredDelay * feedback; delayLine[writeIndex] = combined + filteredDelay * feedback;
writeIndex = (writeIndex + 1) % delayLine.Length; writeIndex = (writeIndex + 1) % delayLine.Length;
// 6. Dry/wet mix // 5. Dry/wet mix
float output = resonant * 0.7f + wet * 0.3f; float output = combined * 0.7f + wet * 0.3f;
output = MathF.Tanh(output); return MathF.Tanh(output);
return output;
} }
} }
// PinkNoiseGenerator unchanged, same as before
internal class PinkNoiseGenerator internal class PinkNoiseGenerator
{ {
private readonly Random random = new Random(); private readonly Random random = new Random();

View File

@@ -12,24 +12,27 @@ public class Program
private const double DrawFrequency = 60.0; private const double DrawFrequency = 60.0;
private static Scenario scenario; private static Scenario scenario;
// Speed control // Speed control (existing + new throttle)
private static double desiredSpeed = 0.0001; private static double desiredSpeed = 0.01;
//private static double desiredSpeed = 1;
private static double currentSpeed = desiredSpeed; private static double currentSpeed = 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;
// Spacetoggle state private static double lastDesiredSpeed = 0.1;
private static double lastDesiredSpeed = 0.1; // remembers the last non1.0 speed private static bool isRealTime = false;
private static bool isRealTime = false; // starts in slowmo (desiredSpeed != 1.0)
// Throttle smoothing
private static double targetThrottle = 0.0; // 1.0 when W is pressed, 0.0 otherwise
private static double currentThrottle = 0.0;
private const double ThrottleSmoothing = 8.0; // rate of change
private static volatile bool running = true; private static volatile bool running = true;
public static void Main() public static void Main()
{ {
var mode = new VideoMode(new Vector2u(1280, 720)); var mode = new VideoMode(new Vector2u(1280, 720));
var window = new RenderWindow(mode, "FluidSim - Helmholtz Resonator"); var window = new RenderWindow(mode, "FluidSim - Engine (W = throttle)");
window.SetVerticalSyncEnabled(true); window.SetVerticalSyncEnabled(true);
window.Closed += (_, _) => { running = false; window.Close(); }; window.Closed += (_, _) => { running = false; window.Close(); };
window.MouseWheelScrolled += OnMouseWheel; window.MouseWheelScrolled += OnMouseWheel;
@@ -39,12 +42,7 @@ public class Program
soundEngine.Volume = 70; soundEngine.Volume = 70;
soundEngine.Start(); soundEngine.Start();
// Choose one scenario. The Helmholtz resonator is fully updated. scenario = new EngineScenario();
//scenario = new HelmholtzResonatorScenario();
//scenario = new PipeResonatorScenario(); // needs update to new API
//scenario = new SodShockTubeScenario(); // needs update to new API
scenario = new EngineScenario(); // also works (provided earlier)
scenario.Initialize(SampleRate); scenario.Initialize(SampleRate);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
@@ -52,18 +50,13 @@ public class Program
double drawInterval = 1.0 / DrawFrequency; double drawInterval = 1.0 / DrawFrequency;
double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds; double lastSpeedUpdateTime = stopwatch.Elapsed.TotalSeconds;
// Resampling buffer
var simBuffer = new List<float>(4096); var simBuffer = new List<float>(4096);
double readIndex = 0.0; double readIndex = 0.0;
// Prime the buffer with a few samples
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
simBuffer.Add(scenario.Process()); simBuffer.Add(scenario.Process());
long totalSimSteps = simBuffer.Count; long totalSimSteps = simBuffer.Count;
long totalOutputSamples = 0; long totalOutputSamples = 0;
double lastRealTime = stopwatch.Elapsed.TotalSeconds;
const int outputChunk = 256; const int outputChunk = 256;
float[] outputBuf = new float[outputChunk]; float[] outputBuf = new float[outputChunk];
@@ -72,16 +65,22 @@ public class Program
window.DispatchEvents(); window.DispatchEvents();
double currentRealTime = stopwatch.Elapsed.TotalSeconds; double currentRealTime = stopwatch.Elapsed.TotalSeconds;
double dtSpeed = currentRealTime - lastSpeedUpdateTime; double dtClock = currentRealTime - lastSpeedUpdateTime;
lastSpeedUpdateTime = currentRealTime; lastSpeedUpdateTime = currentRealTime;
// Smoothly transition currentSpeed → desiredSpeed // Smooth simulation speed
double smoothingRate = 8.0; // higher = faster catchup double speedSmoothing = 8.0;
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-smoothingRate * dtSpeed)); currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
// ---------- Generate audio ---------- // ---- THROTTLE INPUT ----
targetThrottle = Keyboard.IsKeyPressed(Keyboard.Key.W) ? 1.0 : 0.0;
currentThrottle += (targetThrottle - currentThrottle) * (1.0 - Math.Exp(-ThrottleSmoothing * dtClock));
// Push to engine scenario (if it's an EngineScenario)
if (scenario is EngineScenario engine)
engine.Throttle = currentThrottle;
// Generate audio
double targetAudioClock = currentRealTime + 0.05; double targetAudioClock = currentRealTime + 0.05;
while (totalOutputSamples < targetAudioClock * SampleRate && running) while (totalOutputSamples < targetAudioClock * SampleRate && running)
{ {
int toGenerate = (int)Math.Min( int toGenerate = (int)Math.Min(
@@ -103,11 +102,9 @@ public class Program
int i0 = (int)readIndex; int i0 = (int)readIndex;
int i1 = i0 + 1; int i1 = i0 + 1;
double frac = readIndex - i0; double frac = readIndex - i0;
float y0 = simBuffer[Math.Clamp(i0, 0, simBuffer.Count - 1)]; float y0 = simBuffer[Math.Clamp(i0, 0, simBuffer.Count - 1)];
float y1 = simBuffer[Math.Clamp(i1, 0, simBuffer.Count - 1)]; float y1 = simBuffer[Math.Clamp(i1, 0, simBuffer.Count - 1)];
outputBuf[i] = (float)(y0 + (y1 - y0) * frac); outputBuf[i] = (float)(y0 + (y1 - y0) * frac);
readIndex += currentSpeed; readIndex += currentSpeed;
while (readIndex >= 1.0 && simBuffer.Count > 2) while (readIndex >= 1.0 && simBuffer.Count > 2)
@@ -119,23 +116,23 @@ public class Program
int accepted = soundEngine.WriteSamples(outputBuf, toGenerate); int accepted = soundEngine.WriteSamples(outputBuf, toGenerate);
totalOutputSamples += accepted; totalOutputSamples += accepted;
if (accepted < toGenerate) if (accepted < toGenerate)
break; break;
} }
// ---------- Drawing & window title ---------- // Drawing & title
if (currentRealTime - lastDrawTime >= drawInterval) if (currentRealTime - lastDrawTime >= drawInterval)
{ {
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate); double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
double simTime = totalSimSteps / (double)SampleRate; double simTime = totalSimSteps / (double)SampleRate;
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time"; string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
window.SetTitle( window.SetTitle(
$"{toggleHint} Sim: {simTime:F2}s | " + $"{toggleHint} {throttleHint} " +
$"Speed: {currentSpeed:F4}x → {desiredSpeed:F4}x | " + $"Thr: {currentThrottle:F2} " +
$"Actual: {actualSpeed:F2}x" $"Speed: {currentSpeed:F3}x → {desiredSpeed:F3}x " +
$"Act: {actualSpeed:F2}x"
); );
window.Clear(Color.Black); window.Clear(Color.Black);
scenario.Draw(window); scenario.Draw(window);
window.Display(); window.Display();
@@ -147,22 +144,18 @@ public class Program
window.Dispose(); window.Dispose();
} }
// (Mouse wheel, space toggle unchanged)
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; bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
if (e.Delta > 0) if (e.Delta > 0)
desiredSpeed *= ScrollFactor; desiredSpeed *= ScrollFactor;
else if (e.Delta < 0) else if (e.Delta < 0)
desiredSpeed /= ScrollFactor; desiredSpeed /= ScrollFactor;
desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed); desiredSpeed = Math.Clamp(desiredSpeed, MinSpeed, MaxSpeed);
// Update the remembered slowmo speed (unless we are exactly at 1.0)
if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6) if (!wasRealTime || Math.Abs(desiredSpeed - 1.0) > 1e-6)
lastDesiredSpeed = desiredSpeed; lastDesiredSpeed = desiredSpeed;
// Update isRealTime flag
isRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6; isRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
} }
@@ -171,15 +164,9 @@ public class Program
if (e.Code == Keyboard.Key.Space) if (e.Code == Keyboard.Key.Space)
{ {
if (isRealTime) if (isRealTime)
{
// Switch to the remembered slow speed
desiredSpeed = lastDesiredSpeed; desiredSpeed = lastDesiredSpeed;
}
else else
{
// Switch back to real time
desiredSpeed = 1.0; desiredSpeed = 1.0;
}
isRealTime = !isRealTime; isRealTime = !isRealTime;
} }
} }

View File

@@ -8,7 +8,8 @@ namespace FluidSim.Core
public class EngineScenario : Scenario public class EngineScenario : Scenario
{ {
private Solver solver; private Solver solver;
private Volume0D cylinder; private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe; private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling; private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor; private SoundProcessor soundProcessor;
@@ -16,259 +17,98 @@ namespace FluidSim.Core
private double dt; private double dt;
private double ambientPressure = 101325.0; private double ambientPressure = 101325.0;
private double time; private double time;
// ---- 4stroke cycle angle (0 … 4π) ----
private double cycleCrankAngle = 0.0; // 0 to 4π, then resets
private const double TargetRPM = 1000.0;
private double angularVelocity; // rad/s of crankshaft
// ---- Engine geometry ----
private double bore = 0.065; // 65 mm
private double stroke = 0.0565; // 56.5 mm → 250 cc
private double conRodLength = 0.113; // roughly 2 * stroke
private double compressionRatio = 10.0;
private double V_disp; // displacement volume
private double V_clear; // clearance volume
// ---- Combustion ----
private const double CombustionPressure = 50.0 * 101325.0;
private const double CombustionTemperature = 2500.0;
private bool burnInProgress = false;
private double burnStartAngle; // cycle angle when ignition began
private const double BurnDurationDeg = 40.0;
private const double BurnDurationRad = BurnDurationDeg * Math.PI / 180.0;
private double targetBurnEnergy;
private double totalBurnMass;
// Preignition state (compressed fresh charge) for misfire restoration
private double preIgnitionMass;
private double preIgnitionInternalEnergy;
// ---- Valve timing ----
private const double ValveOpenStart = 120.0 * Math.PI / 180.0; // 120° after TDC power
private const double ValveOpenEnd = 480.0 * Math.PI / 180.0; // 480° ≈ 120° after TDC exhaust
private const double ValveRampWidth = 30.0 * Math.PI / 180.0; // 30° ramps
private double maxOrificeArea;
// ---- Misfire ----
private Random rand = new Random();
private const double MisfireProbability = 0.02;
private bool isMisfiring = false;
// ---- Logging ----
private int stepCount = 0; private int stepCount = 0;
private const int LogStepInterval = 10000; private const int LogInterval = 10000;
private int combustionCount = 0;
private int misfireCount = 0; // Throttle 0..1 → target combustion pressure
public double Throttle { get; set; } = 0.05; // tiny throttle to keep idle
private const double IdlePeakPressure = 5.0 * 101325.0; // 5 bar
private const double MaxPeakPressure = 50.0 * 101325.0; // 50 bar
public override void Initialize(int sampleRate) public override void Initialize(int sampleRate)
{ {
dt = 1.0 / sampleRate; dt = 1.0 / sampleRate;
angularVelocity = TargetRPM * 2.0 * Math.PI / 60.0;
// Displacement volume // Crankshaft (inertia + friction)
V_disp = (Math.PI / 4.0) * bore * bore * stroke; crankshaft = new Crankshaft(initialRPM: 100.0) // starter speed
V_clear = V_disp / (compressionRatio - 1.0);
// Cylinder (starts at TDC clearance volume with compressed ambient charge)
double initialPressure = ambientPressure * Math.Pow(compressionRatio, 1.4); // isentropic compression
double initialTemperature = 300.0 * Math.Pow(compressionRatio, 1.4 - 1.0);
double initialVolume = V_clear;
cylinder = new Volume0D(initialVolume, initialPressure, initialTemperature, sampleRate)
{ {
Gamma = 1.4, Inertia = 0.05,
GasConstant = 287.0 FrictionConstant = 1.0,
FrictionViscous = 0.01
}; };
// Exhaust pipe (2.5 m long, 3 cm radius) // Pipe
double pipeLength = 2.5; double pipeLength = 0.5;
double pipeRadius = 0.03; double pipeRadius = 0.1;
double pipeArea = Math.PI * pipeRadius * pipeRadius; double pipeArea = Math.PI * pipeRadius * pipeRadius;
maxOrificeArea = pipeArea; exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 60);
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 100);
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure); exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
exhaustPipe.EnergyRelaxationRate = 0f;
exhaustPipe.DampingMultiplier = 0;
// Coupling (valve initially closed) // Cylinder (coupled to crankshaft)
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0); engineCyl = new EngineCylinder(crankshaft,
bore: 0.065, stroke: 0.0565, compressionRatio: 10.0,
pipeArea: pipeArea, sampleRate: sampleRate);
// Coupling (valve → pipe)
coupling = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
// Solver
solver = new Solver(); solver = new Solver();
solver.SetTimeStep(dt); solver.SetTimeStep(dt);
solver.AddVolume(cylinder); solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe); solver.AddPipe(exhaustPipe);
solver.AddConnection(coupling); solver.AddConnection(coupling);
// Open end with characteristic radiation (softer reflections)
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, ambientPressure); solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, ambientPressure);
// Sound processor (keep your carefully tuned gains) // Sound (your tuned gains)
soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f); soundProcessor = new SoundProcessor(sampleRate, pipeRadius * 2, reverbTimeMs: 500.0f);
soundProcessor.MasterGain = 0.0002f; soundProcessor.MasterGain = 0.0f; //0.00001f;
soundProcessor.PressureGain = 10.0f; soundProcessor.PressureGain = 0.1f;
soundProcessor.TurbulenceGain = 0.00005f; soundProcessor.TurbulenceGain = 0.0f;
soundProcessor.Turbulence = 0.001f;
soundProcessor.SetAmbientPressure(ambientPressure); soundProcessor.SetAmbientPressure(ambientPressure);
// Log startup info Console.WriteLine("=== EngineScenario (torquedriven RPM, throttle = pressure) ===");
Console.WriteLine("=== EngineScenario (improved physics) ==="); Console.WriteLine($"Crankshaft inertia: {crankshaft.Inertia}, friction: {crankshaft.FrictionConstant} + {crankshaft.FrictionViscous}*ω");
Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%"); Console.WriteLine($"Throttle range: {IdlePeakPressure/101325:F0} {MaxPeakPressure/101325:F0} bar");
Console.WriteLine($"Bore x Stroke: {bore*1000:F0} x {stroke*1000:F0} mm, CR: {compressionRatio:F1}"); Console.WriteLine($"Pipe: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
Console.WriteLine($"Pipe length: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
Console.WriteLine($"Combustion: {CombustionPressure/101325:F0} bar, {CombustionTemperature} K");
Console.WriteLine($"Valve opens at {ValveOpenStart*180/Math.PI:F0}°, closes at {ValveOpenEnd*180/Math.PI:F0}° (ramp {ValveRampWidth*180/Math.PI:F0}°)");
Console.WriteLine($"Burn duration: {BurnDurationDeg}°");
Console.WriteLine("Time[s] Crank[°] Vol[cc] MassFlow[kg/s] Comb# Misfire");
Console.WriteLine("-------------------------------------------------------------");
}
// ---- Piston volume & derivative ----
private (double volume, double dvdt) PistonKinematics(double theta)
{
// theta = crankshaft angle (0 at TDC of power stroke)
double R = stroke / 2.0;
double cosT = Math.Cos(theta);
double sinT = Math.Sin(theta);
double L = conRodLength;
// Slidercrank position relative to TDC
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
double V = V_clear + (Math.PI / 4.0) * bore * bore * s;
// Derivative dV/dθ
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
double dVdθ = (Math.PI / 4.0) * bore * bore * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
double dvdt = dVdθ * angularVelocity; // rad/s → volume change rate
return (V, dvdt);
}
// ---- Valve lift (trapezoidal) ----
private double ValveOpenRatio(double cycleRad)
{
// cycleRad: 0 … 4π
if (cycleRad < ValveOpenStart || cycleRad > ValveOpenEnd)
return 0.0;
double duration = ValveOpenEnd - ValveOpenStart;
double ramp = ValveRampWidth;
double t = (cycleRad - ValveOpenStart) / duration;
if (t < ramp / duration)
return t / (ramp / duration);
else if (t > 1.0 - ramp / duration)
return (1.0 - t) / (ramp / duration);
else
return 1.0;
}
// ---- Wiebe burn fraction ----
private double WiebeFraction(double angleFromIgnition)
{
if (angleFromIgnition >= BurnDurationRad) return 1.0;
double a = 5.0, m = 2.0;
double x = angleFromIgnition / BurnDurationRad;
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
} }
public override float Process() public override float Process()
{ {
// Advance cycle crank angle // 1. Map throttle to target peak pressure
cycleCrankAngle += angularVelocity * dt; double targetPressure = IdlePeakPressure + Throttle * (MaxPeakPressure - IdlePeakPressure);
if (cycleCrankAngle >= 4.0 * Math.PI) // 720° engineCyl.TargetPeakPressure = targetPressure;
{
cycleCrankAngle -= 4.0 * Math.PI;
isMisfiring = rand.NextDouble() < MisfireProbability;
// ---- Prepare cylinder for new power stroke ---- // 2. Step the cylinder (adds torque to crankshaft, updates valve)
// Fill cylinder with fresh charge at BDC, then compress isentropically to TDC clearance volume. engineCyl.Step(dt);
double T_bdc = 300.0; // intake temperature
double p_bdc = ambientPressure; // intake pressure
double V_bdc = V_clear + V_disp; // volume at BDC (intake valve closing)
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
// Compress isentropically to V_clear // 3. Integrate crankshaft (applies friction, updates RPM)
double V1 = V_bdc, V2 = V_clear; crankshaft.Step(dt);
double gamma = 1.4;
double p2 = p_bdc * Math.Pow(V1 / V2, gamma);
double T2 = T_bdc * Math.Pow(V1 / V2, gamma - 1);
// Set compressed state // 4. Set orifice area for coupling
cylinder.Volume = V_clear; coupling.OrificeArea = engineCyl.OrificeArea;
cylinder.Mass = freshMass;
cylinder.InternalEnergy = p2 * V_clear / (gamma - 1.0); // consistent with pressure/temperature
// Store preignition state for misfire recovery // 5. Fluid solver step
preIgnitionMass = cylinder.Mass;
preIgnitionInternalEnergy = cylinder.InternalEnergy;
if (isMisfiring)
{
// No combustion just expand from compressed state
misfireCount++;
}
else
{
// Start Wiebe burn
double V = V_clear;
targetBurnEnergy = CombustionPressure * V / (gamma - 1.0);
double R = 287.0;
totalBurnMass = CombustionPressure * V / (R * CombustionTemperature);
burnInProgress = true;
burnStartAngle = cycleCrankAngle; // now = 0
combustionCount++;
}
}
// ---- Combustion progress (if active) ----
if (burnInProgress)
{
double angleFromIgnition = cycleCrankAngle - burnStartAngle;
if (angleFromIgnition >= BurnDurationRad)
{
// Burn complete
cylinder.Mass = totalBurnMass;
cylinder.InternalEnergy = targetBurnEnergy;
burnInProgress = false;
}
else
{
double fraction = WiebeFraction(angleFromIgnition);
// Interpolate between preignition (compressed charge) and final burned state
double gamma = 1.4;
double V = cylinder.Volume; // still near clearance
double baseEnergy = preIgnitionInternalEnergy;
double baseMass = preIgnitionMass;
cylinder.InternalEnergy = baseEnergy * (1.0 - fraction) + targetBurnEnergy * fraction;
cylinder.Mass = baseMass * (1.0 - fraction) + totalBurnMass * fraction;
}
}
// ---- Update cylinder volume from piston kinematics ----
double theta = cycleCrankAngle % (2.0 * Math.PI); // crank angle for piston position
var (vol, dvdt) = PistonKinematics(theta);
cylinder.Volume = vol;
cylinder.Dvdt = dvdt;
// ---- Valve lift & orifice area ----
double lift = ValveOpenRatio(cycleCrankAngle);
coupling.OrificeArea = maxOrificeArea * lift;
// ---- Solver step ----
float massFlow = solver.Step(); float massFlow = solver.Step();
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1); float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
// ---- Audio (no filter, feed raw pressure) ---- // 6. Audio
float audioSample = soundProcessor.Process(massFlow, endPressure); float audioSample = soundProcessor.Process(massFlow, endPressure);
// Log occasionally
time += dt; time += dt;
stepCount++; stepCount++;
if (stepCount % LogStepInterval == 0) if (stepCount % LogInterval == 0) {
Console.WriteLine(audioSample);
}
if (stepCount % 1000 == 0 && false)
{ {
double crankDeg = cycleCrankAngle * 180.0 / Math.PI; Console.WriteLine($"{time,5:F3} {crankshaft.AngularVelocity*60/(2*Math.PI),5:F0} RPM " +
double volCC = cylinder.Volume * 1e6; // cc $"Thr:{Throttle:F2} P_target:{targetPressure/101325:F1} bar " +
Console.WriteLine($"{time,5:F3} {crankDeg,7:F1}° {volCC,5:F1} {massFlow,14:E4} {combustionCount,4} {misfireCount,4}"); $"mflow:{massFlow,14:E4} Comb#{engineCyl.CombustionCount} Mis#{engineCyl.MisfireCount}");
} }
return audioSample; return audioSample;
@@ -285,7 +125,6 @@ namespace FluidSim.Core
const float T_hot = 1500f; const float T_hot = 1500f;
const float T_cold = 0f; const float T_cold = 0f;
const float R = 287.05f; const float R = 287.05f;
float deltaHot = T_hot - T_ambient; float deltaHot = T_hot - T_ambient;
float deltaCold = T_ambient - T_cold; float deltaCold = T_ambient - T_cold;
@@ -303,12 +142,12 @@ namespace FluidSim.Core
var cylRect = new RectangleShape(new Vector2f(cylW, cylH)); var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(40f, centerY - cylH / 2f); cylRect.Position = new Vector2f(40f, centerY - cylH / 2f);
double tempCyl = cylinder.Temperature; // Volume0D now has Temperature double tempCyl = engineCyl.Cylinder.Temperature;
float tnCyl = NormaliseTemperature(tempCyl); float tnCyl = NormaliseTemperature(tempCyl);
byte redCyl = (byte)(tnCyl > 0 ? 255 * tnCyl : 0); byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte blueCyl = (byte)(tnCyl < 0 ? -255 * tnCyl : 0); byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte greenCyl = (byte)(255 * (1 - Math.Abs(tnCyl))); byte gC = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(redCyl, greenCyl, blueCyl); cylRect.FillColor = new Color(rC, gC, bC);
target.Draw(cylRect); target.Draw(cylRect);
int n = exhaustPipe.GetCellCount(); int n = exhaustPipe.GetCellCount();
@@ -317,7 +156,7 @@ namespace FluidSim.Core
float dx = pipeLen / (n - 1); float dx = pipeLen / (n - 1);
float baseRadius = 20f; float baseRadius = 20f;
var vertices = new Vertex[n * 2]; var vertices = new Vertex[n * 2];
float ambientPressure = 101325f; float ambPress = 101325f;
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
@@ -326,7 +165,7 @@ namespace FluidSim.Core
double rho = exhaustPipe.GetCellDensity(i); double rho = exhaustPipe.GetCellDensity(i);
double T = p / (rho * R); double T = p / (rho * R);
float r = baseRadius * 0.1f * (float)(1.0 + (p - ambientPressure) / ambientPressure); float r = baseRadius * 0.3f * (float)(1.0 + (p - ambPress) / ambPress);
if (r < 2f) r = 2f; if (r < 2f) r = 2f;
float tn = NormaliseTemperature(T); float tn = NormaliseTemperature(T);