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; // Crankshaft private double crankAngle = 0.0; private const double TargetRPM = 4000.0; private double angularVelocity; // Combustion private const double CombustionPressure = 8.0 * 101325.0; private const double CombustionTemperature = 1800.0; // Valve timing private const double ValveOpenStart = 120.0 * Math.PI / 180.0; private const double ValveOpenEnd = 480.0 * Math.PI / 180.0; private const double ValveRampWidth = 30.0 * Math.PI / 180.0; private double maxOrificeArea; // Misfire private Random rand = new Random(); private const double MisfireProbability = 0.02; private bool isMisfiring = false; // Low‑pass filter for pressure private double lastFilteredPressure; private const double PressureCutoffHz = 50.0; // Logging private int stepCount = 0; private const int LogStepInterval = 1000; 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; // Cylinder: 0.5 litre, initially at ambient double cylVolume = 0.5e-3; double initialPressure = ambientPressure; double initialTemperature = 300.0; cylinder = new Volume0D(cylVolume, initialPressure, initialTemperature, sampleRate) { Gamma = 1.4, GasConstant = 287.0 }; // Exhaust pipe: length 2.5 m, radius 2 cm double pipeLength = 2.5; double pipeRadius = 0.02; double pipeArea = Math.PI * pipeRadius * pipeRadius; maxOrificeArea = pipeArea; exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 70); exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure); // Coupling (valve starts 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); // Use ZeroPressureOpen for strong reflections solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.ZeroPressureOpen, ambientPressure); // Sound processor (tuned to pipe length) soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f); soundProcessor.MasterGain = 0.02f; // boosted from 0.0008 soundProcessor.PressureGain = 4.0f; // boosted from6 0.12 soundProcessor.TurbulenceGain = 0.0002f; // reduced from 0.02 soundProcessor.SetAmbientPressure(ambientPressure); lastFilteredPressure = ambientPressure; Console.WriteLine("=== EngineScenario (ZeroPressureOpen, boosted gains) ==="); Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%"); Console.WriteLine($"Pipe length: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz"); 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($"Sample rate: {sampleRate} Hz, dt = {dt*1000:F3} ms"); Console.WriteLine("Time[s] Crank[°] Valve[%] MassFlow[kg/s] Comb# Misfire"); Console.WriteLine("---------------------------------------------------------"); } private double ValveOpenRatio(double crankRad) { double cycleAngle = crankRad % (4.0 * Math.PI); double openStart = ValveOpenStart; double openEnd = ValveOpenEnd; if (cycleAngle < openStart || cycleAngle > openEnd) return 0.0; double fullOpenWindow = openEnd - openStart; double closedWindow = 2.0 * ValveRampWidth; if (fullOpenWindow <= closedWindow) return 1.0; double tmid = (openStart + openEnd) / 2.0; double dist = Math.Abs(cycleAngle - tmid); double rampHalf = (fullOpenWindow - closedWindow) / 2.0; if (dist <= rampHalf) return 1.0; else { double frac = (dist - rampHalf) / ValveRampWidth; frac = Math.Clamp(frac, 0.0, 1.0); double lift = Math.Cos(frac * Math.PI / 2.0); return lift * lift; } } public override float Process() { // Update crank angle crankAngle += angularVelocity * dt; if (crankAngle >= 2.0 * Math.PI) { crankAngle -= 2.0 * Math.PI; isMisfiring = rand.NextDouble() < MisfireProbability; } // Power stroke at TDC if (crankAngle < angularVelocity * dt && crankAngle >= 0.0) { if (isMisfiring) { double vol = cylinder.Volume; double R = cylinder.GasConstant; double T0 = 300.0; double newMass = ambientPressure * vol / (R * T0); double newInternalEnergy = ambientPressure * vol / (cylinder.Gamma - 1.0); cylinder.Mass = newMass; cylinder.InternalEnergy = newInternalEnergy; misfireCount++; } else { double volume = cylinder.Volume; double gamma = cylinder.Gamma; double newInternalEnergy = CombustionPressure * volume / (gamma - 1.0); double R = cylinder.GasConstant; double newMass = CombustionPressure * volume / (R * CombustionTemperature); cylinder.InternalEnergy = newInternalEnergy; cylinder.Mass = newMass; combustionCount++; } } // Update valve area double valveOpen = ValveOpenRatio(crankAngle); coupling.OrificeArea = maxOrificeArea * valveOpen; float massFlow = solver.Step(); float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1); // Low‑pass filter the pressure (emphasise low frequencies) double rc = 1.0 / (2.0 * Math.PI * PressureCutoffHz); double alpha = dt / (rc + dt); double filteredPressure = alpha * endPressure + (1.0 - alpha) * lastFilteredPressure; lastFilteredPressure = filteredPressure; float audioSample = soundProcessor.Process(massFlow, (float)filteredPressure); time += dt; stepCount++; // Logging if (stepCount % LogStepInterval == 0 || (crankAngle < angularVelocity * dt * 2 && !isMisfiring && combustionCount > 0)) { Console.WriteLine($"{time,7:F3} {crankAngle * 180.0 / Math.PI,6:F1} " + $"{valveOpen * 100,6:F1} {massFlow,10:F4} " + $"{combustionCount,3} {(isMisfiring ? "X" : "")}"); } return audioSample; } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float centerY = winH / 2f; float cylW = 80f, cylH = 150f; var cylRect = new RectangleShape(new Vector2f(cylW, cylH)); cylRect.Position = new Vector2f(40f, centerY - cylH / 2f); double pNorm = (cylinder.Pressure - ambientPressure) / ambientPressure; if (double.IsNaN(pNorm)) pNorm = 0; byte red = (byte)(Math.Clamp(pNorm * 128, 0, 255)); byte blue = (byte)(Math.Clamp(-pNorm * 128, 0, 255)); cylRect.FillColor = new Color(red, 0, blue); 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]; for (int i = 0; i < n; i++) { float x = pipeStartX + i * dx; double p = exhaustPipe.GetCellPressure(i); float r = baseRadius * (float)(1.0 + (p - ambientPressure) / ambientPressure); if (r < 2f) r = 2f; double t = (p - ambientPressure) / ambientPressure; t = Math.Clamp(t, -1.0, 1.0); byte rCol = (byte)(t > 0 ? 255 * t : 0); byte bCol = (byte)(t < 0 ? -255 * t : 0); byte gCol = (byte)(255 * (1 - Math.Abs(t))); 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); } } }