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 PipeSystem pipeSystem; private BoundarySystem boundaries; private Solver solver; private Volume0D intakePlenum; private Port plenumInlet, plenumOutlet; private Volume0D exhaustMuffler; private Port mufflerIn, mufflerOut; private Vehicle vehicle; private int throttleAreaIdx, plenumRunnerIdx, intakeValveIdx, 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, exhaustHeaderArea; public override void ShiftUp() => vehicle.ShiftUp(); public override void ShiftDown() => vehicle.ShiftDown(); public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // ── Vehicle ────────────────────────────────────────────────────────── vehicle = new Vehicle(); // ── Throttle body: 42 mm – wider to reduce high-RPM intake restriction ── _maxThrottleArea = (float)Units.AreaFromDiameter(42 * Units.mm); // ── Crankshaft ─────────────────────────────────────────────────────── // Lighter flywheel for quicker revving; friction tuned to ~0.5 kW loss at idle crankshaft = new Crankshaft(2000); crankshaft.CycleLength = 2f * MathF.PI; // two-stroke: fire every rev crankshaft.Inertia = 0.06f; // lighter flywheel crankshaft.FrictionConstant = 0.4f; // ~0.4 Nm constant drag crankshaft.FrictionViscous = 0.0004f; // ~2.5 Nm at 10 000 RPM // ── Cylinder: 125 cc, motocross-style two-stroke ───────────────────── // Bore × stroke = 54 × 54.5 mm → 124.9 cc float bore = 0.054f; float stroke = 0.0545f; float conRod = 0.110f; // ~2× stroke float compRatio = 7.2f; // geometric CR; effective CR after port closure is ~12:1 // Port timings: exhaust 195°, transfer 155° – competitive MX 125 float transferDuration = 155f; float exhaustDuration = 195f; cylinder = new TwoStrokeCylinder(bore, stroke, conRod, compRatio, transferDuration, exhaustDuration, crankshaft) { IntakeValveDiameter = 0.042f, // matched to intake pipe IntakeValveLift = 0.015f, ExhaustValveDiameter = 0.040f, ExhaustValveLift = 0.013f }; // ── Pipe geometry ──────────────────────────────────────────────────── // // Layout (all lengths in mm): // Intake path: airbox stub 100 mm | runner 180 mm // Exhaust path: expansion chamber tuned to ~9 000 RPM power peak // header 170 mm Ø 40 mm // diffuser 280 mm Ø 40 → 72 mm // belly 200 mm Ø 72 mm // convergent 130 mm Ø 72 → 28 mm // stinger 70 mm Ø 28 mm // total 850 mm // // Cell sizing: ~14 mm/cell. // CFL: c_sound ≈ 550 m/s, dx=0.014 m → dt_max ≈ 25 µs // at 44100 Hz dt = 22.7 µs → SubStepCount=4 keeps CFL safely ≤ 1 // --- Cell counts --- int intakeCells = 7; // 100 mm stub → ~14 mm/cell int runnerCells = 13; // 180 mm runner → ~14 mm/cell int exhaustCells = 60; // 850 mm total → ~14 mm/cell int totalCells = intakeCells + runnerCells + exhaustCells; int[] pipeStart = { 0, intakeCells, intakeCells + runnerCells }; int[] pipeEnd = { intakeCells, intakeCells + runnerCells, totalCells }; float[] area = new float[totalCells]; float[] dx = new float[totalCells]; // --- Intake --- float intakeDia = 0.042f; // matches throttle body float intakeStubLen = 0.100f; float intakeRunnerLen= 0.160f; // shorter runner → less pumping loss intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia; for (int i = 0; i < intakeCells; i++) { area[i] = intakePipeArea; dx[i] = intakeStubLen / intakeCells; } for (int i = intakeCells; i < intakeCells + runnerCells; i++) { area[i] = intakePipeArea; dx[i] = intakeRunnerLen / runnerCells; } // Expansion chamber tuned for ~8 500 RPM power peak. // Return-pulse travel distance = 0.5 × c_avg × (60 / RPM_target) // c_avg ≈ 480 m/s → distance = 0.5 × 480 × (60/8500) ≈ 1.69 m round-trip // → one-way pipe length ≈ 0.84 m (matches total below) float headerDia = 0.040f; float headerLen = 0.130f; // shorter header → earlier pulse float diffEndDia = 0.070f; float diffuserLen = 0.250f; // slightly narrower belly float bellyDia = 0.070f; float bellyLen = 0.220f; float convEndDia = 0.028f; float convergentLen= 0.160f; // longer convergent → stronger return pulse float stingerDia = 0.028f; float stingerLen = 0.080f; // total = 0.13+0.25+0.22+0.16+0.08 = 0.84 m exhaustHeaderArea = MathF.PI * 0.25f * headerDia * headerDia; float bellyArea = MathF.PI * 0.25f * bellyDia * bellyDia; float stingerArea = MathF.PI * 0.25f * stingerDia * stingerDia; // Distribute cells proportionally by section length int headerCells = Math.Max(1, (int)MathF.Round(exhaustCells * headerLen / 0.84f)); int diffuserCells = Math.Max(1, (int)MathF.Round(exhaustCells * diffuserLen / 0.84f)); int bellyCells = Math.Max(1, (int)MathF.Round(exhaustCells * bellyLen / 0.84f)); int convergentCells = Math.Max(1, (int)MathF.Round(exhaustCells * convergentLen/ 0.84f)); int stingerCells = exhaustCells - headerCells - diffuserCells - bellyCells - convergentCells; if (stingerCells < 1) stingerCells = 1; int exhBase = intakeCells + runnerCells; int idx = 0; for (int i = exhBase; i < totalCells; i++, idx++) { if (idx < headerCells) { area[i] = exhaustHeaderArea; dx[i] = headerLen / headerCells; } else if (idx < headerCells + diffuserCells) { float t = (idx - headerCells) / (float)(diffuserCells - 1); // Smooth cosine taper instead of linear for better wave reflection float ct = 0.5f * (1f - MathF.Cos(MathF.PI * t)); float dia = headerDia + (diffEndDia - headerDia) * ct; area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = diffuserLen / diffuserCells; } else if (idx < headerCells + diffuserCells + bellyCells) { area[i] = bellyArea; dx[i] = bellyLen / bellyCells; } else if (idx < headerCells + diffuserCells + bellyCells + convergentCells) { float t = (idx - headerCells - diffuserCells - bellyCells) / (float)(convergentCells - 1); // Steeper cosine convergent for a sharper return pulse float ct = 0.5f * (1f - MathF.Cos(MathF.PI * t)); float dia = bellyDia + (convEndDia - bellyDia) * ct; area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = convergentLen / convergentCells; } else { area[i] = stingerArea; dx[i] = stingerLen / stingerCells; } } pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx, 1.225f, 0f, 101325f); pipeSystem.DampingMultiplier = 0.8f; // slightly less damping → stronger pulses pipeSystem.EnergyRelaxationRate = 0.4f; pipeSystem.AmbientPressure = 101325f; // ── 0-D Volumes ────────────────────────────────────────────────────── // Intake plenum: acts as a small airbox resonator (8 cc) intakePlenum = new Volume0D(8e-3f, 101325f, 300f); plenumInlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort(); // Exhaust silencer volume: 600 cc is realistic for a small-bore muffler exhaustMuffler = new Volume0D(600e-6f, 101325f, 650f); mufflerIn = exhaustMuffler.CreatePort(); mufflerOut = exhaustMuffler.CreatePort(); // ── Boundary system ─────────────────────────────────────────────────── boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2); throttleAreaIdx = 0; plenumRunnerIdx = 1; intakeValveIdx = 2; exhaustValveIdx = 3; // Open ends: atmosphere at both extremes boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, intakePipeArea); intakeOpenIdx = 0; boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, stingerArea); exhaustOpenIdx = 1; // Orifices: throttle → plenum → runner → cylinder → exhaust pipe boundaries.AddOrifice(plenumInlet, 0, false, throttleAreaIdx, 0.72f); boundaries.AddOrifice(plenumOutlet, 1, true, plenumRunnerIdx, 1.00f); boundaries.AddOrifice(cylinder.IntakePort, 1, false, intakeValveIdx, 0.68f); boundaries.AddOrifice(cylinder.ExhaustPort, 2, true, exhaustValveIdx, 0.70f); orificeAreas = new float[4]; orificeAreas[plenumRunnerIdx] = intakePipeArea; // runner always fully open // ── Solver ──────────────────────────────────────────────────────────── // SubStepCount = 4 keeps CFL ≤ 1 for 5 mm cells at 44 100 Hz solver = new Solver { SubStepCount = 4, EnableProfiling = false }; solver.SetTimeStep(dt); solver.SetPipeSystem(pipeSystem); solver.SetBoundarySystem(boundaries); solver.AddComponent(cylinder); solver.AddComponent(intakePlenum); solver.AddComponent(exhaustMuffler); // ── 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("125cc Two-Stroke – expansion chamber tuned for ~8 500 RPM power peak"); Console.WriteLine($" Exhaust cells: {exhaustCells} | header {headerCells} diffuser {diffuserCells}" + $" belly {bellyCells} convergent {convergentCells} stinger {stingerCells}"); } public override float Process() { float engineRpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); vehicle.ClutchInput = Clutch; var (clutchTorque, effectiveInertia) = vehicle.Update(engineRpm, crankshaft.Inertia, (float)dt); crankshaft.SetEffectiveInertia(effectiveInertia); crankshaft.SetLoadTorque(clutchTorque); crankshaft.Step((float)dt); cylinder.PreStep((float)dt); float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.001f, 1f); orificeAreas[throttleAreaIdx] = throttledArea; orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; boundaries.SetOrificeAreas(orificeAreas); 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); float powerKw = crankshaft.AveragePower * 1e-3f; float torqueNm = crankshaft.AverageTorque; Console.WriteLine($"Step {stepCount,7} | RPM={rpm,6:F0} | Power={powerKw,5:F2} kW" + $" | Torque={torqueNm,5:F1} Nm | Gear={vehicle.CurrentGear}" + $" | Speed={vehicle.SpeedKmh,4:F0} km/h"); } return reverb.Process((intakeDry + exhaustDry) * 0.5f); } // ── Drawing ─────────────────────────────────────────────────────────────── public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float intakeY = winH / 2f - 40f; float exhaustY = winH / 2f + 80f; float openEndX = 40f; // Intake stub float x = openEndX; float w = 120f; DrawPipe(target, pipeSystem, 0, intakeY, x, x + w); // Throttle body float throttleX = x + w + 5f; var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(throttleX, intakeY - 15f) }; target.Draw(throttleRect); // Plenum float plenW = 40f, plenH = 60f; float plenX = throttleX + 10f; DrawVolume(target, intakePlenum, plenX + plenW / 2f, intakeY - plenH / 2f, plenW, plenH); // Runner float runnerStartX = plenX + plenW + 5f; DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerStartX + 100f); // Cylinder float cylCX = runnerStartX + 150f; float cylTopY = intakeY - 120f; DrawCylinder(target, cylinder, cylCX, cylTopY, 80f, 240f); // Exhaust pipe (expansion chamber) float exhStartX = cylCX + 40f + 20f; DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, winW - 60f, areaScale: 800f); // HUD labels float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); float powerKw = crankshaft.AveragePower * 1e-3f; float torqueNm = crankshaft.AverageTorque; 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); DrawLabel(target, $"Torque: {torqueNm:F1} Nm",new Vector2f(20, 140), Color.White, 20); string gearText = vehicle.CurrentGear == 0 ? "N" : vehicle.CurrentGear.ToString(); DrawLabel(target, $"Gear: {gearText}", new Vector2f(20, 162), Color.Cyan, 20); DrawLabel(target, $"Speed: {vehicle.SpeedKmh:F0} km/h", new Vector2f(20, 184), Color.Cyan, 20); DrawLabel(target, vehicle.Engagement > 0.99f ? "Clutch: Locked" : "Clutch: Slipping", new Vector2f(20, 204), Color.Cyan, 14); // Dyno curve UpdateDynoCurve(rpm, powerKw, torqueNm); DrawDynoCurve(target, winW - 410f, winH - 260f, 400f, 250f, rpm, powerKw); } } }