using FluidSim.Components; using FluidSim.Core; using FluidSim.Interfaces; using FluidSim.Utils; using SFML.Graphics; using SFML.System; using System; namespace FluidSim.Tests { public class TwoStrokeScenario : Scenario { private Crankshaft crankshaft; private TwoStrokeCylinder cylinder; private Crankcase crankcase; private PipeSystem pipeSystem; private BoundarySystem boundaries; private Solver solver; private Volume0D intakePlenum; private Port plenumInlet, plenumOutlet; private int throttleAreaIdx, reedInletIdx, reedOutletIdx, transferInletIdx, transferOutletIdx, exhaustValveIdx; private float[] orificeAreas; private int intakeOpenIdx, exhaustOpenIdx; private SoundProcessor exhaustSound, intakeSound; private OutdoorExhaustReverb reverb; private double dt; private int stepCount; private float maxThrottleArea; private float intakePipeArea, reedPipeArea, transferPipeArea, exhaustHeaderArea; private bool reedOpen; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; maxThrottleArea = (float)Units.AreaFromDiameter(42 * Units.mm); // ---- Crankshaft ---- crankshaft = new Crankshaft(3000); crankshaft.CycleLength = 2f * MathF.PI; crankshaft.Inertia = 0.01f; crankshaft.FrictionConstant = 1.0f; crankshaft.FrictionViscous = 0.002f; // ---- Cylinder (125cc) ---- float bore = 0.054f, stroke = 0.0545f, conRod = 0.110f, compRatio = 7.2f; float transferDur = 140f, exhaustDur = 195f; cylinder = new TwoStrokeCylinder(bore, stroke, conRod, compRatio, transferDur, exhaustDur, crankshaft) { // FIX: realistic transfer port diameter (was 40mm) IntakeValveDiameter = 0.030f, // 30 mm IntakeValveLift = 0.010f, ExhaustValveDiameter = 0.040f, ExhaustValveLift = 0.010f }; // ---- Crankcase ---- float crankRadius = stroke * 0.5f; float ccClearance = 150e-6f; crankcase = new Crankcase(crankshaft, crankRadius, conRod, bore, ccClearance, 101325f, 300f); cylinder.SetCrankcase(crankcase); // ---- Pipe system ---- int intakeCells = 8; int reedCells = 4; int transferCells = 8; int exhaustCells = 60; int totalCells = intakeCells + reedCells + transferCells + exhaustCells; int[] pipeStart = { 0, intakeCells, intakeCells + reedCells, intakeCells + reedCells + transferCells }; int[] pipeEnd = { intakeCells, intakeCells + reedCells, intakeCells + reedCells + transferCells, totalCells }; float[] area = new float[totalCells]; float[] dx = new float[totalCells]; float intakeDia = 0.042f, reedDia = 0.040f, transferDia = 0.040f; intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia; reedPipeArea = MathF.PI * 0.25f * reedDia * reedDia; transferPipeArea = MathF.PI * 0.25f * transferDia * transferDia; for (int i = 0; i < intakeCells; i++) { area[i] = intakePipeArea; dx[i] = 0.100f / intakeCells; } for (int i = intakeCells; i < intakeCells + reedCells; i++) { area[i] = reedPipeArea; dx[i] = 0.030f / reedCells; } for (int i = intakeCells + reedCells; i < intakeCells + reedCells + transferCells; i++) { area[i] = transferPipeArea; dx[i] = 0.200f / transferCells; } float hdrD = 0.040f, hdrL = 0.130f; float difEndD = 0.070f, difL = 0.250f; float belL = 0.220f; float convEndD = 0.028f, convL = 0.160f; float stiL = 0.080f; float totL = hdrL + difL + belL + convL + stiL; exhaustHeaderArea = MathF.PI * 0.25f * hdrD * hdrD; float bellyArea = MathF.PI * 0.25f * difEndD * difEndD; float stingerArea = MathF.PI * 0.25f * convEndD * convEndD; int exhStart = intakeCells + reedCells + transferCells; int hdrC = (int)(exhaustCells * hdrL / totL); int difC = (int)(exhaustCells * difL / totL); int belC = (int)(exhaustCells * belL / totL); int conC = (int)(exhaustCells * convL / totL); int stiC = exhaustCells - hdrC - difC - belC - conC; int idx = 0; for (int i = exhStart; i < totalCells; i++) { if (idx < hdrC) { area[i] = exhaustHeaderArea; dx[i] = hdrL / hdrC; } else if (idx < hdrC + difC) { float t = (idx - hdrC) / (float)(difC - 1); float dia = hdrD + (difEndD - hdrD) * t; area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = difL / difC; } else if (idx < hdrC + difC + belC) { area[i] = bellyArea; dx[i] = belL / belC; } else if (idx < hdrC + difC + belC + conC) { float t = (idx - hdrC - difC - belC) / (float)(conC - 1); float dia = difEndD + (convEndD - difEndD) * t; area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = convL / conC; } else { area[i] = stingerArea; dx[i] = stiL / stiC; } idx++; } pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx, 1.225f, 0f, 101325f); pipeSystem.DampingMultiplier = 0.8f; pipeSystem.EnergyRelaxationRate = 0.4f; // ---- Volumes ---- intakePlenum = new Volume0D(0.5e-3f, 101325f, 300f); plenumInlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort(); // ---- Boundary system ---- boundaries = new BoundarySystem(pipeSystem, maxOrifices: 6, maxOpenEnds: 2); throttleAreaIdx = 0; reedInletIdx = 1; reedOutletIdx = 2; transferInletIdx = 3; transferOutletIdx = 4; exhaustValveIdx = 5; boundaries.AddOpenEnd(0, true, 101325f, intakePipeArea); intakeOpenIdx = 0; boundaries.AddOpenEnd(3, false, 101325f, stingerArea); exhaustOpenIdx = 1; boundaries.AddOrifice(plenumInlet, 0, false, throttleAreaIdx, 0.72f); boundaries.AddOrifice(plenumOutlet, 1, true, reedInletIdx, 1.0f); boundaries.AddOrifice(crankcase.IntakePort, 1, false, reedOutletIdx, 0.9f); boundaries.AddOrifice(crankcase.TransferPort,2, true, transferInletIdx,1.0f); boundaries.AddOrifice(cylinder.IntakePort, 2, false, transferOutletIdx,1.0f); boundaries.AddOrifice(cylinder.ExhaustPort, 3, true, exhaustValveIdx, 0.7f); orificeAreas = new float[6]; orificeAreas[reedInletIdx] = reedPipeArea; orificeAreas[reedOutletIdx] = 0f; orificeAreas[transferInletIdx] = transferPipeArea; orificeAreas[transferOutletIdx] = 0f; // ---- Solver ---- solver = new Solver { SubStepCount = 4 }; solver.SetTimeStep(dt); solver.SetPipeSystem(pipeSystem); solver.SetBoundarySystem(boundaries); solver.AddComponent(cylinder); solver.AddComponent(crankcase); solver.AddComponent(intakePlenum); // ---- Sound ---- exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 4.5f }; intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 4.5f }; reverb = new OutdoorExhaustReverb(sampleRate); stepCount = 0; Console.WriteLine("Two‑Stroke engine ready."); } public override float Process() { const float reedMargin = 200f; if (crankcase.Pressure < intakePlenum.Pressure - reedMargin) reedOpen = true; else if (crankcase.Pressure > intakePlenum.Pressure + reedMargin) reedOpen = false; float throttledFraction = Throttle; if (throttledFraction < 0.001f) throttledFraction = 0f; throttledFraction = Math.Clamp(throttledFraction, 0f, 1f); float throttledArea = maxThrottleArea * throttledFraction; orificeAreas[throttleAreaIdx] = throttledArea; orificeAreas[reedOutletIdx] = reedOpen ? reedPipeArea : 0f; orificeAreas[transferOutletIdx] = cylinder.IntakeValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; boundaries.SetOrificeAreas(orificeAreas); if (stepCount < 20000) crankshaft.AddTorque(5.0f); // FIX: update crankshaft BEFORE volumes, so crankcase and cylinder see the same new angle crankshaft.Step((float)dt); cylinder.PreStep((float)dt); crankcase.PreStep((float)dt); solver.Step(); stepCount++; float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx); float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx); float exhaustDry = exhaustSound.Process(exhaustFlow); float intakeDry = intakeSound.Process(intakeFlow); if (stepCount % 2000 == 0) { float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); Console.WriteLine($"Step {stepCount} | RPM={rpm:F0} | CylP={cylinder.Pressure/1e5f:F2} bar | CCP={crankcase.Pressure/1e5f:F3} bar | Plenum={intakePlenum.Pressure/1e5f:F3} bar | Reed={reedOpen}"); } return reverb.Process((intakeDry + exhaustDry) * 0.5f); } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float startX = 40f; float endX = winW - 80f; DrawPipe(target, pipeSystem, 0, winH * 0.25f, startX, startX + 120f); var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(startX + 125f, winH * 0.25f - 15f) }; target.Draw(throttleRect); float plenX = startX + 140f; DrawVolume(target, intakePlenum, plenX + 30f, winH * 0.25f - 25f, 60f, 50f); float reedStartX = plenX + 70f; DrawPipe(target, pipeSystem, 1, winH * 0.25f, reedStartX, reedStartX + 30f); float transStartX = reedStartX + 40f; DrawPipe(target, pipeSystem, 2, winH * 0.45f, transStartX, transStartX + 120f); float cylCX = transStartX + 180f; float cylTopY = winH * 0.45f - 90f; DrawCylinder(target, cylinder, cylCX, cylTopY, 80f, 240f); float exhStartX = cylCX + 60f; DrawPipe(target, pipeSystem, 3, winH * 0.65f, exhStartX, endX, areaScale: 800f); float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); float powerKw = crankshaft.AveragePower * 1e-3f; DrawLabel(target, $"RPM: {rpm:F0}", new Vector2f(20, 90), Color.White, 24); DrawLabel(target, $"Power: {powerKw:F2} kW", new Vector2f(20, 115), Color.White, 24); float torqueNm = crankshaft.AverageTorque; UpdateDynoCurve(rpm, powerKw, torqueNm); DrawDynoCurve(target, winW - 410f, winH - 260f, 400f, 250f, rpm, powerKw); } } }