using System; using SFML.Graphics; using SFML.System; using FluidSim.Components; using FluidSim.Core; using FluidSim.Utils; namespace FluidSim.Tests { public class Inline4Scenario : Scenario { // Crankshaft private Crankshaft crankshaft; // Cylinders private Cylinder cyl1, cyl2, cyl3, cyl4; // Intake private Pipe1D intakePipeBeforeThrottle; private Volume0D intakePlenum; // Runners private Pipe1D runner1, runner2, runner3, runner4; // Exhaust pipes private Pipe1D exh1, exh2, exh3, exh4; // Links – intake private OpenEndLink intakeOpenEnd; private OrificeLink throttleOrifice; // Plenum‑to‑runner orifices private OrificeLink plenumToRunner1, plenumToRunner2, plenumToRunner3, plenumToRunner4; // Intake valves private OrificeLink intakeValve1, intakeValve2, intakeValve3, intakeValve4; // Exhaust valves private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4; // Exhaust open ends private OpenEndLink exhaustOpenEnd1, exhaustOpenEnd2, exhaustOpenEnd3, exhaustOpenEnd4; private Solver solver; private SoundProcessor exhaustSoundProcessor; private SoundProcessor intakeSoundProcessor; private OutdoorExhaustReverb reverb; private double dt; private int stepCount; public double MaxThrottleArea { get; set; } = 3 * Units.cm2; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; solver = new Solver(); solver.SetTimeStep(dt); solver.CflTarget = 0.9; // ---- Shared crankshaft ---- crankshaft = new Crankshaft(800); crankshaft.Inertia = 1; crankshaft.FrictionConstant = 16; crankshaft.FrictionViscous = 0.5; // ---- Cylinder geometry ---- double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0; // Firing order 1-3-4-2 → phase offsets in radians double phase0 = 0.0 * Math.PI / 180.0; double phase1 = 180.0 * Math.PI / 180.0; double phase2 = 540.0 * Math.PI / 180.0; double phase3 = 360.0 * Math.PI / 180.0; cyl1 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { IntakeValveDiameter = 30 * Units.mm, IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, PhaseOffset = phase0, EnergyVariationFraction = 0.03, MisfireProbability = 0.01 }; cyl2 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { IntakeValveDiameter = 30 * Units.mm, IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, PhaseOffset = phase1, EnergyVariationFraction = 0.03, MisfireProbability = 0.01 }; cyl3 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { IntakeValveDiameter = 30 * Units.mm, IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, PhaseOffset = phase2, EnergyVariationFraction = 0.03, MisfireProbability = 0.01 }; cyl4 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { IntakeValveDiameter = 30 * Units.mm, IntakeValveLift = 5 * Units.mm, ExhaustValveDiameter = 28 * Units.mm, ExhaustValveLift = 5 * Units.mm, PhaseOffset = phase3, EnergyVariationFraction = 0.03, MisfireProbability = 0.01 }; solver.AddComponent(cyl1); solver.AddComponent(cyl2); solver.AddComponent(cyl3); solver.AddComponent(cyl4); double pipeDiameter = 2 * Units.cm; double pipeArea = Units.AreaFromDiameter(pipeDiameter); exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; reverb = new OutdoorExhaustReverb(sampleRate); // ---- Intake pipe before throttle ---- intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10); solver.AddComponent(intakePipeBeforeThrottle); // ---- Plenum ---- intakePlenum = new Volume0D(50 * Units.mL, 101325.0, 300.0); var plenumInlet = intakePlenum.CreatePort(); // port 0 var plenumOut1 = intakePlenum.CreatePort(); // port 1 var plenumOut2 = intakePlenum.CreatePort(); // port 2 var plenumOut3 = intakePlenum.CreatePort(); // port 3 var plenumOut4 = intakePlenum.CreatePort(); // port 4 solver.AddComponent(intakePlenum); // ---- Runners ---- runner1 = new Pipe1D(0.2, pipeArea, 5); runner2 = new Pipe1D(0.2, pipeArea, 5); runner3 = new Pipe1D(0.2, pipeArea, 5); runner4 = new Pipe1D(0.2, pipeArea, 5); solver.AddComponent(runner1); solver.AddComponent(runner2); solver.AddComponent(runner3); solver.AddComponent(runner4); // ---- Exhaust pipes ---- exh1 = new Pipe1D(0.2, pipeArea, 10); exh2 = new Pipe1D(0.2, pipeArea, 10); exh3 = new Pipe1D(0.2, pipeArea, 10); exh4 = new Pipe1D(0.2, pipeArea, 10); solver.AddComponent(exh1); solver.AddComponent(exh2); solver.AddComponent(exh3); solver.AddComponent(exh4); // ---- Plenum → runner orifices ---- plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(plenumToRunner1); solver.AddOrificeLink(plenumToRunner2); solver.AddOrificeLink(plenumToRunner3); solver.AddOrificeLink(plenumToRunner4); // ---- Intake valves ---- intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(intakeValve1); solver.AddOrificeLink(intakeValve2); solver.AddOrificeLink(intakeValve3); solver.AddOrificeLink(intakeValve4); // ---- Exhaust valves ---- exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exh1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exh2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exh3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exh4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(exhaustValve1); solver.AddOrificeLink(exhaustValve2); solver.AddOrificeLink(exhaustValve3); solver.AddOrificeLink(exhaustValve4); // ---- Exhaust open ends ---- exhaustOpenEnd1 = new OpenEndLink(exh1, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; exhaustOpenEnd2 = new OpenEndLink(exh2, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; exhaustOpenEnd3 = new OpenEndLink(exh3, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; exhaustOpenEnd4 = new OpenEndLink(exh4, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(exhaustOpenEnd1); solver.AddOpenEndLink(exhaustOpenEnd2); solver.AddOpenEndLink(exhaustOpenEnd3); solver.AddOpenEndLink(exhaustOpenEnd4); // ---- Intake open end ---- intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(intakeOpenEnd); // ---- Throttle ---- throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false, areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1.0)) { DischargeCoefficient = 0.2, UseInertance = false }; solver.AddOrificeLink(throttleOrifice); stepCount = 0; Console.WriteLine("Inline-4 engine test"); Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}"); Console.WriteLine("Firing order 1-3-4-2, 180° intervals"); } public override float Process() { crankshaft.Step(dt); cyl1.PreStep(dt); cyl2.PreStep(dt); cyl3.PreStep(dt); cyl4.PreStep(dt); solver.Step(); stepCount++; if (stepCount % 10000 == 0) { double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI); Console.WriteLine($"Step {stepCount}, RPM = {rpm:F0}, " + $"cyl1 P = {cyl1.Pressure / 1e5:F2} bar, " + $"plenum P = {intakePlenum.Pressure / 1e5:F2} bar"); } // Mix all exhaust sounds float exhaustMix = exhaustSoundProcessor.Process(exhaustOpenEnd1) + exhaustSoundProcessor.Process(exhaustOpenEnd2) + exhaustSoundProcessor.Process(exhaustOpenEnd3) + exhaustSoundProcessor.Process(exhaustOpenEnd4); float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd); return reverb.Process(exhaustMix * 0.25f + intakeDry); } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float startX = 60f; float spacing = 80f; float intakeY = winH / 2f - 80f; float exhaustY = winH / 2f + 80f; // Plenum float plenW = 50f, plenH = 120f; float plenX = startX; float plenTopY = intakeY - plenH / 2f; DrawVolume(target, intakePlenum, plenX, plenTopY, plenW, plenH); // Helper arrays just for drawing (no closures) var cyls = new[] { cyl1, cyl2, cyl3, cyl4 }; var runners = new[] { runner1, runner2, runner3, runner4 }; var exhausts = new[] { exh1, exh2, exh3, exh4 }; for (int i = 0; i < 4; i++) { float cylX = plenX + plenW + 30f + i * spacing; float runnerStartX = plenX + plenW + 5f; float runnerEndX = cylX - 20f; DrawPipe(target, runners[i], intakeY, runnerStartX, runnerEndX); float cylTopY = intakeY - 120f; DrawCylinder(target, cyls[i], cylX, cylTopY, 70f, 200f); float exhStartX = cylX + 35f; float exhEndX = exhStartX + 100f; DrawPipe(target, exhausts[i], exhaustY, exhStartX, exhEndX); var mark = new CircleShape(4f) { FillColor = Color.Magenta }; mark.Position = new Vector2f(exhEndX - 4f, exhaustY - 4f); target.Draw(mark); } // Throttle symbol var throttleRect = new RectangleShape(new Vector2f(6f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(plenX - 16f, intakeY - 15f) }; target.Draw(throttleRect); } } }