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; // ---- 4‑stroke 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; // Pre‑ignition 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; // Slider‑crank 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 pre‑ignition 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 pre‑ignition (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); } } }