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 (shorter, fewer cells) private Pipe1D runner1, runner2, runner3, runner4; // Exhaust collector + tailpipe private Volume0D exhaustCollector; private Pipe1D tailPipe; // Exhaust stubs (short pipes between cylinders and collector) private Pipe1D exhStub1, exhStub2, exhStub3, exhStub4; // 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 (cylinder → stub) private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4; // Stub‑to‑collector orifices private OrificeLink stubToCollector1, stubToCollector2, stubToCollector3, stubToCollector4; // Collector‑to‑tailpipe orifice private OrificeLink collectorToTailpipe; // Exhaust open end (tailpipe exit) private OpenEndLink exhaustOpenEnd; private Solver solver; private SoundProcessor exhaustSoundProcessor; private SoundProcessor intakeSoundProcessor; private OutdoorExhaustReverb reverb; private double dt; private int stepCount; public double MaxThrottleArea { get; set; } = 10 * Units.cm2; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; solver = new Solver(); solver.SetTimeStep(dt); solver.CflTarget = 1; // ---- Shared crankshaft ---- crankshaft = new Crankshaft(800); crankshaft.Inertia = 1; crankshaft.FrictionConstant = 3; crankshaft.FrictionViscous = 0.2; // ---- Cylinder geometry ---- double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 10; double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0; // Firing order 1-3-4-2 with 180° intervals (0°, 180°, 360°, 540°) double phaseCyl1 = 0.0; double phaseCyl3 = Math.PI; // 180° double phaseCyl4 = 2.0 * Math.PI; // 360° double phaseCyl2 = 3.0 * Math.PI; // 540° 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 = phaseCyl1, EnergyVariationFraction = 0.03, MisfireProbability = 0.0 }; 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 = phaseCyl2, EnergyVariationFraction = 0.03, MisfireProbability = 0.0 }; 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 = phaseCyl3, EnergyVariationFraction = 0.03, MisfireProbability = 0.0 }; 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 = phaseCyl4, EnergyVariationFraction = 0.03, MisfireProbability = 0.0 }; solver.AddComponent(cyl1); solver.AddComponent(cyl2); solver.AddComponent(cyl3); solver.AddComponent(cyl4); double pipeDiameter = 4 * Units.cm; double pipeArea = Units.AreaFromDiameter(pipeDiameter); // Sound processors (only one exhaust source now) exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f }; intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f }; reverb = new OutdoorExhaustReverb(sampleRate); // ---- Intake pipe before throttle (shorter, fewer cells) ---- intakePipeBeforeThrottle = new Pipe1D(0.1, pipeArea, 10); intakePipeBeforeThrottle.Name = "Intake pipe"; solver.AddComponent(intakePipeBeforeThrottle); // ---- Plenum ---- intakePlenum = new Volume0D(100 * 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); // ---- Intake runners (shorter, fewer cells) ---- runner1 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 1" }; runner2 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 2" }; runner3 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 3" }; runner4 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 4" }; solver.AddComponent(runner1); solver.AddComponent(runner2); solver.AddComponent(runner3); solver.AddComponent(runner4); // ---- Exhaust collector volume ---- exhaustCollector = new Volume0D(200 * Units.mL, 101325.0, 800.0); var colIn1 = exhaustCollector.CreatePort(); // cylinder 1 stub var colIn2 = exhaustCollector.CreatePort(); // cylinder 2 stub var colIn3 = exhaustCollector.CreatePort(); // cylinder 3 stub var colIn4 = exhaustCollector.CreatePort(); // cylinder 4 stub var colOut = exhaustCollector.CreatePort(); // to tailpipe solver.AddComponent(exhaustCollector); // ---- Exhaust stub pipes (short connection cylinder → collector) ---- exhStub1 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 1" }; exhStub2 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 2" }; exhStub3 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 3" }; exhStub4 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 4" }; solver.AddComponent(exhStub1); solver.AddComponent(exhStub2); solver.AddComponent(exhStub3); solver.AddComponent(exhStub4); foreach (var p in new[] { runner1, runner2, runner3, runner4, exhStub1, exhStub2, exhStub3, exhStub4, intakePipeBeforeThrottle }) { p.DampingMultiplier = 0.5; p.EnergyRelaxationRate = 0.0; } // ---- Tailpipe (single exhaust pipe) ---- tailPipe = new Pipe1D(0.5, pipeArea, 20) { Name = "Tailpipe", DampingMultiplier = 0.5, EnergyRelaxationRate = 0.0 }; solver.AddComponent(tailPipe); // ---- Plenum → runner orifices (volume port to pipe left end) ---- 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 (cylinder port to runner right end) ---- 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 (cylinder port to stub left end) ---- exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exhStub1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exhStub2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exhStub3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exhStub4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(exhaustValve1); solver.AddOrificeLink(exhaustValve2); solver.AddOrificeLink(exhaustValve3); solver.AddOrificeLink(exhaustValve4); // ---- Stub → collector orifices (collector port to stub right end) ---- stubToCollector1 = new OrificeLink(colIn1, exhStub1, isPipeLeftEnd: false, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; stubToCollector2 = new OrificeLink(colIn2, exhStub2, isPipeLeftEnd: false, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; stubToCollector3 = new OrificeLink(colIn3, exhStub3, isPipeLeftEnd: false, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; stubToCollector4 = new OrificeLink(colIn4, exhStub4, isPipeLeftEnd: false, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(stubToCollector1); solver.AddOrificeLink(stubToCollector2); solver.AddOrificeLink(stubToCollector3); solver.AddOrificeLink(stubToCollector4); // ---- Collector → tailpipe (collector port to tailpipe left end) ---- collectorToTailpipe = new OrificeLink(colOut, tailPipe, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(collectorToTailpipe); // ---- Exhaust open end (tailpipe exit) ---- exhaustOpenEnd = new OpenEndLink(tailPipe, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(exhaustOpenEnd); // ---- 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.0005, 1.0)) { DischargeCoefficient = 0.9, 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"); } // Sound: only one exhaust source now float exhaustSound = exhaustSoundProcessor.Process(exhaustOpenEnd); float intakeSound = intakeSoundProcessor.Process(intakeOpenEnd); return reverb.Process(exhaustSound * 0.25f + intakeSound); } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; // --- Layout constants --- float leftMargin = 40f; float plenumW = 50f, plenumH = 120f; float cylinderWidth = 60f, cylinderMaxHeight = 180f; float cylinderSpacing = 90f; float cylinderTopY = winH * 0.25f; // Plenum position float plenumCenterX = leftMargin + plenumW / 2f; float plenumTopY = cylinderTopY - 20f; DrawVolume(target, intakePlenum, plenumCenterX, plenumTopY, plenumW, plenumH); // Throttle symbol (yellow rectangle) left of plenum float throttleWidth = 8f, throttleHeight = 30f; float throttleCenterX = leftMargin - 10f; var throttleRect = new RectangleShape(new Vector2f(throttleWidth, throttleHeight)) { FillColor = Color.Yellow, Position = new Vector2f(throttleCenterX - throttleWidth / 2f, plenumTopY + plenumH / 2f - throttleHeight / 2f) }; target.Draw(throttleRect); // Intake pipe before throttle (left of throttle) float intakePipeEndX = throttleCenterX - throttleWidth / 2f; float intakePipeStartX = intakePipeEndX - 100f; float intakePipeY = plenumTopY + plenumH / 2f; DrawPipe(target, intakePipeBeforeThrottle, intakePipeY, intakePipeStartX, intakePipeEndX); // Intake open end marker var intakeMark = new CircleShape(4f) { FillColor = Color.Magenta }; intakeMark.Position = new Vector2f(intakePipeStartX - 4f, intakePipeY - 4f); target.Draw(intakeMark); // Cylinders and runners float runnerStartX = leftMargin + plenumW; Cylinder[] cyls = { cyl1, cyl2, cyl3, cyl4 }; Pipe1D[] runners = { runner1, runner2, runner3, runner4 }; for (int i = 0; i < 4; i++) { float cylCenterX = runnerStartX + 40f + i * cylinderSpacing; float runnerEndX = cylCenterX; DrawPipe(target, runners[i], plenumTopY + plenumH / 2f, runnerStartX, runnerEndX); DrawCylinder(target, cyls[i], cylCenterX, cylinderTopY, cylinderWidth, cylinderMaxHeight); } // Exhaust collector below cylinders float collectorLeftX = runnerStartX + 40f - cylinderWidth / 2f; float collectorWidth = 3 * cylinderSpacing + cylinderWidth; float collectorTopY = cylinderTopY + cylinderMaxHeight + 40f; float collectorHeight = 50f; float collectorCenterX = collectorLeftX + collectorWidth / 2f; DrawVolume(target, exhaustCollector, collectorCenterX, collectorTopY, collectorWidth, collectorHeight); // Tailpipe from right edge of collector float tailStartX = collectorLeftX + collectorWidth; float tailEndX = tailStartX + 150f; float tailCenterY = collectorTopY + collectorHeight / 2f; DrawPipe(target, tailPipe, tailCenterY, tailStartX, tailEndX); // Exhaust open end marker var exhaustMark = new CircleShape(4f) { FillColor = Color.Magenta }; exhaustMark.Position = new Vector2f(tailEndX - 4f, tailCenterY - 4f); target.Draw(exhaustMark); // Exhaust stubs (vertical connections from cylinder bottom to collector) Pipe1D[] stubs = { exhStub1, exhStub2, exhStub3, exhStub4 }; for (int i = 0; i < 4; i++) { float cylCenterX = runnerStartX + 40f + i * cylinderSpacing; float vertStartY = cylinderTopY + cylinderMaxHeight; float vertEndY = collectorTopY; // Draw stub as a vertical pipe DrawPipeVertical(target, stubs[i], cylCenterX, vertStartY, vertEndY); } } // Helper to draw a pipe vertically (reuse temperature coloring) private void DrawPipeVertical(RenderWindow target, Pipe1D pipe, float centerX, float topY, float bottomY) { int n = pipe.CellCount; if (n < 2) return; float pipeLengthPx = bottomY - topY; float dy = pipeLengthPx / (n - 1); float baseRadius = 25f; float rangeFactor = 2f; float scaleFactor = 2f; static float SmoothStep(float edge0, float edge1, float x) { float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f); return t * t * (3f - 2f * t); } var centersY = new float[n]; var radii = new float[n]; var temperatures = new double[n]; double R_gas = 287.0; for (int i = 0; i < n; i++) { double p = pipe.GetCellPressure(i); double rho = pipe.GetCellDensity(i); double T = p / Math.Max(rho * R_gas, 1e-12); temperatures[i] = T; float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor); radii[i] = baseRadius * (1f + deviation * scaleFactor); if (radii[i] < 2f) radii[i] = 2f; centersY[i] = topY + i * dy; } int segmentsPerCell = 8; int totalPoints = n + (n - 1) * segmentsPerCell; Vertex[] stripVertices = new Vertex[totalPoints * 2]; int idx = 0; for (int i = 0; i < n; i++) { float y = centersY[i]; float r = radii[i]; Color col = TemperatureColor(temperatures[i]); stripVertices[idx++] = new Vertex(new Vector2f(centerX - r, y), col); stripVertices[idx++] = new Vertex(new Vector2f(centerX + r, y), col); if (i < n - 1) { for (int s = 1; s <= segmentsPerCell; s++) { float t = s / (float)segmentsPerCell; float st = SmoothStep(0f, 1f, t); float yi = centersY[i] + (centersY[i + 1] - centersY[i]) * t; float ri = radii[i] + (radii[i + 1] - radii[i]) * st; double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; Color coli = TemperatureColor(Ti); stripVertices[idx++] = new Vertex(new Vector2f(centerX - ri, yi), coli); stripVertices[idx++] = new Vertex(new Vector2f(centerX + ri, yi), coli); } } } var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length); for (int i = 0; i < stripVertices.Length; i++) pipeMesh[(uint)i] = stripVertices[i]; target.Draw(pipeMesh); } } }