using System; using SFML.Graphics; using SFML.System; using FluidSim.Components; using FluidSim.Core; using FluidSim.Utils; namespace FluidSim.Tests { public class TestScenario : Scenario { // Simulation core private Solver solver; private double dt; // Engine components private Volume0D cylinder; private Pipe1D exhaustPipe; private OrificeLink exhaustPort; private OpenEndLink pipeOpenEnd; private Crankshaft crankshaft; // Audio private SoundProcessor soundProcessor; // Engine geometry (Suzuki TS125 – Jones Appendix 1) private const double Bore = 0.056; // m private const double Stroke = 0.050; // m private const double ConRodLength = 0.110; // m (typical) private const double CrankRadius = Stroke / 2.0; private const double Obliquity = CrankRadius / ConRodLength; private const double CompressionRatio = 6.7; // from Jones // Derived volumes private double sweptVolume; private double clearanceVolume; // Port timing (degrees from TDC) private const double ExhaustPortOpens = 98.0; // °ATDC private const double ExhaustPortCloses = 262.0; // °ATDC private const double PortWidth = 0.025; // m (estimated) private const double MaxPortArea = 0.001; // m² (fully open) // Engine state private double crankAngle; // rad private double engineSpeed; // rad/s private bool combustionPending; // true when ready to fire at TDC // Logging private int stepCount; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // Audio soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f }; // Solver solver = new Solver(); solver.SetTimeStep(dt); solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses // Compute engine volumes double boreArea = Math.PI * 0.25 * Bore * Bore; sweptVolume = boreArea * Stroke; clearanceVolume = sweptVolume / (CompressionRatio - 1.0); double initialVolume = clearanceVolume; // at TDC // Cylinder cylinder = new Volume0D(initialVolume, 101325.0, 300.0) { Dvdt = 0.0 }; solver.AddComponent(cylinder); // Exhaust pipe (1 m, 1 cm², 100 cells) exhaustPipe = new Pipe1D(0.5, 10e-4, 20); solver.AddComponent(exhaustPipe); // Exhaust port – orifice with variable area var cylPort = cylinder.CreatePort(); exhaustPort = new OrificeLink(cylPort, exhaustPipe, isPipeLeftEnd: true, areaProvider: () => ComputeExhaustPortArea(crankAngle)) { DischargeCoefficient = 0.8, UseInertance = false }; solver.AddOrificeLink(exhaustPort); // Pipe open end pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(pipeOpenEnd); // Crankshaft (3000 rpm) crankshaft = new Crankshaft(initialRPM: 10000.0); crankAngle = crankshaft.CrankAngle; engineSpeed = crankshaft.AngularVelocity; combustionPending = false; // first combustion will occur at next TDC stepCount = 0; Console.WriteLine("2‑Stroke engine test"); Console.WriteLine($"Engine: {Bore*1000:F0} mm x {Stroke*1000:F0} mm, {sweptVolume*1e6:F0} cc"); Console.WriteLine($"Compression ratio: {CompressionRatio:F1}, clearance volume: {clearanceVolume*1e6:F2} cc"); Console.WriteLine($"Exhaust port opens at {ExhaustPortOpens}° ATDC, closes at {ExhaustPortCloses}° ATDC"); } // ---- Port area vs crank angle (linear ramp, symmetric) ---- private double ComputeExhaustPortArea(double thetaRad) { double thetaDeg = thetaRad * 180.0 / Math.PI; // Wrap to [0,360) for easier logic thetaDeg %= 360.0; // Exhaust open period if (thetaDeg >= ExhaustPortOpens && thetaDeg <= ExhaustPortCloses) { // Ramp up from 0 to Max, then back down double halfPeriod = (ExhaustPortCloses - ExhaustPortOpens) / 2.0; double midPoint = ExhaustPortOpens + halfPeriod; double distFromMid = Math.Abs(thetaDeg - midPoint) / halfPeriod; double fraction = 1.0 - distFromMid; fraction = Math.Clamp(fraction, 0.0, 1.0); return MaxPortArea * fraction; } return 0.0; } // ---- Cylinder volume vs crank angle (slider‑crank) ---- private double ComputeCylinderVolume(double thetaRad) { // thetaRad = crank angle from TDC (0 at TDC) double r = CrankRadius; double l = ConRodLength; double cosTh = Math.Cos(thetaRad); double sinTh = Math.Sin(thetaRad); double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh); double x = r * (1.0 - cosTh) + l * (1.0 - term); double area = Math.PI * 0.25 * Bore * Bore; double deltaV = area * x; return clearanceVolume + deltaV; } // ---- Combustion: set cylinder pressure AND temperature ---- private void Combustion() { double peakPressure = 20.0 * Units.atm; // 30 bar double peakTemperature = 2000.0; // K cylinder.SetPressure(peakPressure, peakTemperature); } public override float Process() { // Previous crank angle for detecting TDC crossing double prevAngle = crankshaft.CrankAngle; // Advance crankshaft crankshaft.Step(dt); crankAngle = crankshaft.CrankAngle; engineSpeed = crankshaft.AngularVelocity; // Update cylinder volume to match current crank angle double newVolume = ComputeCylinderVolume(crankAngle); cylinder.Dvdt = (newVolume - cylinder.Volume) / dt; cylinder.Volume = newVolume; // ----- Ignition (once per revolution at TDC) ----- const double TwoPi = 2.0 * Math.PI; double prevMod = prevAngle % TwoPi; double currMod = crankAngle % TwoPi; // Detect crossing of 0 mod 2π (TDC) – going from near 2π to near 0 if (prevMod > Math.PI * 1.8 && currMod < Math.PI * 0.2) { if (!combustionPending) { Combustion(); combustionPending = true; // prevent multiple firings during the crossing } } else if (currMod > Math.PI * 0.2 && currMod < Math.PI * 1.8) { combustionPending = false; // reset flag once clear of TDC } // Run solver solver.Step(); stepCount++; // Log every 500 steps if (stepCount % 50000 == 0) { int midCell = exhaustPipe.CellCount / 2; double cylP_bar = cylinder.Pressure / 1e5; double cylT_K = cylinder.Temperature; double cylVol_cc = cylinder.Volume * 1e6; double pipeL_bar = exhaustPipe.GetCellPressure(0) / 1e5; double pipeM_bar = exhaustPipe.GetCellPressure(midCell) / 1e5; double pipeR_bar = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5; double mdotExh = exhaustPort.LastMassFlowRate; // kg/s, positive into cylinder double mdotOpen = pipeOpenEnd.LastMassFlowRate; // kg/s, positive out Console.WriteLine( $"Step {stepCount}: Angle={crankAngle*180.0/Math.PI % 360.0:F1}°, " + $"CylP={cylP_bar:F2} bar, CylT={cylT_K:F0} K, Vol={cylVol_cc:F1} cc, " + $"PipeL={pipeL_bar:F2} bar, PipeM={pipeM_bar:F2} bar, PipeR={pipeR_bar:F2} bar, " + $"mdot_exh={mdotExh:E4} kg/s, mdot_open={mdotOpen:E4} kg/s" ); } if (double.IsNaN(exhaustPipe.GetCellPressure(0))) { Console.WriteLine("NaN detected – stopping."); return 0f; } // Audio from open end return soundProcessor.Process(pipeOpenEnd); } public override void Draw(RenderWindow target) { float winWidth = target.GetView().Size.X; float winHeight = target.GetView().Size.Y; float pipeCenterY = winHeight / 2f; float margin = 60f; float pipeStartX = margin; float pipeEndX = winWidth - margin; DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX); } } }