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

View File

@@ -8,7 +8,8 @@ namespace FluidSim.Core
public class EngineScenario : Scenario
{
private Solver solver;
private Volume0D cylinder;
private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
@@ -16,259 +17,98 @@ namespace FluidSim.Core
private double dt;
private double ambientPressure = 101325.0;
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 const int LogStepInterval = 10000;
private int combustionCount = 0;
private int misfireCount = 0;
private const int LogInterval = 10000;
// 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)
{
dt = 1.0 / sampleRate;
angularVelocity = TargetRPM * 2.0 * Math.PI / 60.0;
// Displacement volume
V_disp = (Math.PI / 4.0) * bore * bore * stroke;
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)
// Crankshaft (inertia + friction)
crankshaft = new Crankshaft(initialRPM: 100.0) // starter speed
{
Gamma = 1.4,
GasConstant = 287.0
Inertia = 0.05,
FrictionConstant = 1.0,
FrictionViscous = 0.01
};
// Exhaust pipe (2.5 m long, 3 cm radius)
double pipeLength = 2.5;
double pipeRadius = 0.03;
// Pipe
double pipeLength = 0.5;
double pipeRadius = 0.1;
double pipeArea = Math.PI * pipeRadius * pipeRadius;
maxOrificeArea = pipeArea;
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 100);
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 60);
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
exhaustPipe.EnergyRelaxationRate = 0f;
exhaustPipe.DampingMultiplier = 0;
// Coupling (valve initially closed)
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0);
// Cylinder (coupled to crankshaft)
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.SetTimeStep(dt);
solver.AddVolume(cylinder);
solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe);
solver.AddConnection(coupling);
// Open end with characteristic radiation (softer reflections)
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, ambientPressure);
// Sound processor (keep your carefully tuned gains)
soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f);
soundProcessor.MasterGain = 0.0002f;
soundProcessor.PressureGain = 10.0f;
soundProcessor.TurbulenceGain = 0.00005f;
// Sound (your tuned gains)
soundProcessor = new SoundProcessor(sampleRate, pipeRadius * 2, reverbTimeMs: 500.0f);
soundProcessor.MasterGain = 0.0f; //0.00001f;
soundProcessor.PressureGain = 0.1f;
soundProcessor.TurbulenceGain = 0.0f;
soundProcessor.Turbulence = 0.001f;
soundProcessor.SetAmbientPressure(ambientPressure);
// Log startup info
Console.WriteLine("=== EngineScenario (improved physics) ===");
Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%");
Console.WriteLine($"Bore x Stroke: {bore*1000:F0} x {stroke*1000:F0} mm, CR: {compressionRatio:F1}");
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));
Console.WriteLine("=== EngineScenario (torquedriven RPM, throttle = pressure) ===");
Console.WriteLine($"Crankshaft inertia: {crankshaft.Inertia}, friction: {crankshaft.FrictionConstant} + {crankshaft.FrictionViscous}*ω");
Console.WriteLine($"Throttle range: {IdlePeakPressure/101325:F0} {MaxPeakPressure/101325:F0} bar");
Console.WriteLine($"Pipe: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
}
public override float Process()
{
// Advance cycle crank angle
cycleCrankAngle += angularVelocity * dt;
if (cycleCrankAngle >= 4.0 * Math.PI) // 720°
{
cycleCrankAngle -= 4.0 * Math.PI;
isMisfiring = rand.NextDouble() < MisfireProbability;
// 1. Map throttle to target peak pressure
double targetPressure = IdlePeakPressure + Throttle * (MaxPeakPressure - IdlePeakPressure);
engineCyl.TargetPeakPressure = targetPressure;
// ---- Prepare cylinder for new power stroke ----
// Fill cylinder with fresh charge at BDC, then compress isentropically to TDC clearance volume.
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);
// 2. Step the cylinder (adds torque to crankshaft, updates valve)
engineCyl.Step(dt);
// Compress isentropically to V_clear
double V1 = V_bdc, V2 = V_clear;
double gamma = 1.4;
double p2 = p_bdc * Math.Pow(V1 / V2, gamma);
double T2 = T_bdc * Math.Pow(V1 / V2, gamma - 1);
// 3. Integrate crankshaft (applies friction, updates RPM)
crankshaft.Step(dt);
// Set compressed state
cylinder.Volume = V_clear;
cylinder.Mass = freshMass;
cylinder.InternalEnergy = p2 * V_clear / (gamma - 1.0); // consistent with pressure/temperature
// 4. Set orifice area for coupling
coupling.OrificeArea = engineCyl.OrificeArea;
// Store preignition state for misfire recovery
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 ----
// 5. Fluid solver step
float massFlow = solver.Step();
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
// ---- Audio (no filter, feed raw pressure) ----
// 6. Audio
float audioSample = soundProcessor.Process(massFlow, endPressure);
// Log occasionally
time += dt;
stepCount++;
if (stepCount % LogStepInterval == 0)
if (stepCount % LogInterval == 0) {
Console.WriteLine(audioSample);
}
if (stepCount % 1000 == 0 && false)
{
double crankDeg = cycleCrankAngle * 180.0 / Math.PI;
double volCC = cylinder.Volume * 1e6; // cc
Console.WriteLine($"{time,5:F3} {crankDeg,7:F1}° {volCC,5:F1} {massFlow,14:E4} {combustionCount,4} {misfireCount,4}");
Console.WriteLine($"{time,5:F3} {crankshaft.AngularVelocity*60/(2*Math.PI),5:F0} RPM " +
$"Thr:{Throttle:F2} P_target:{targetPressure/101325:F1} bar " +
$"mflow:{massFlow,14:E4} Comb#{engineCyl.CombustionCount} Mis#{engineCyl.MisfireCount}");
}
return audioSample;
@@ -285,7 +125,6 @@ namespace FluidSim.Core
const float T_hot = 1500f;
const float T_cold = 0f;
const float R = 287.05f;
float deltaHot = T_hot - T_ambient;
float deltaCold = T_ambient - T_cold;
@@ -303,12 +142,12 @@ namespace FluidSim.Core
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
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);
byte redCyl = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte blueCyl = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte greenCyl = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(redCyl, greenCyl, blueCyl);
byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(rC, gC, bC);
target.Draw(cylRect);
int n = exhaustPipe.GetCellCount();
@@ -317,7 +156,7 @@ namespace FluidSim.Core
float dx = pipeLen / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
float ambientPressure = 101325f;
float ambPress = 101325f;
for (int i = 0; i < n; i++)
{
@@ -326,7 +165,7 @@ namespace FluidSim.Core
double rho = exhaustPipe.GetCellDensity(i);
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;
float tn = NormaliseTemperature(T);