Files
FluidSim/Scenarios/EngineScenario.cs
2026-05-05 11:24:32 +02:00

344 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using FluidSim.Components;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Volume0D cylinder;
private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
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;
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)
{
Gamma = 1.4,
GasConstant = 287.0
};
// Exhaust pipe (2.5 m long, 3 cm radius)
double pipeLength = 2.5;
double pipeRadius = 0.03;
double pipeArea = Math.PI * pipeRadius * pipeRadius;
maxOrificeArea = pipeArea;
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 100);
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
// Coupling (valve initially closed)
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(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;
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));
}
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;
// ---- 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);
// 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);
// Set compressed state
cylinder.Volume = V_clear;
cylinder.Mass = freshMass;
cylinder.InternalEnergy = p2 * V_clear / (gamma - 1.0); // consistent with pressure/temperature
// 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 ----
float massFlow = solver.Step();
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
// ---- Audio (no filter, feed raw pressure) ----
float audioSample = soundProcessor.Process(massFlow, endPressure);
// Log occasionally
time += dt;
stepCount++;
if (stepCount % LogStepInterval == 0)
{
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}");
}
return audioSample;
}
// ---- Drawing (unchanged) ----
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
const float T_ambient = 293.15f;
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;
float NormaliseTemperature(double T)
{
double t;
if (T >= T_ambient)
t = (T - T_ambient) / deltaHot;
else
t = (T - T_ambient) / deltaCold;
return (float)Math.Clamp(t, -1.0, 1.0);
}
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(40f, centerY - cylH / 2f);
double tempCyl = cylinder.Temperature; // Volume0D now has 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);
target.Draw(cylRect);
int n = exhaustPipe.GetCellCount();
float pipeStartX = 120f, pipeEndX = winW - 60f;
float pipeLen = pipeEndX - pipeStartX;
float dx = pipeLen / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
float ambientPressure = 101325f;
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = exhaustPipe.GetCellPressure(i);
double rho = exhaustPipe.GetCellDensity(i);
double T = p / (rho * R);
float r = baseRadius * 0.1f * (float)(1.0 + (p - ambientPressure) / ambientPressure);
if (r < 2f) r = 2f;
float tn = NormaliseTemperature(T);
byte rCol = (byte)(tn > 0 ? 255 * tn : 0);
byte bCol = (byte)(tn < 0 ? -255 * tn : 0);
byte gCol = (byte)(255 * (1 - Math.Abs(tn)));
var col = new Color(rCol, gCol, bCol);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
}
}
}