using System; using SFML.Graphics; using SFML.System; using FluidSim.Components; using FluidSim.Core; using FluidSim.Utils; namespace FluidSim.Tests { public class TestScenario : Scenario { // Engine private Cylinder cylinder; private Crankshaft crankshaft; // Intake side private Pipe1D intakePipeBeforeThrottle; private Volume0D intakePlenum; // 5 mL private Pipe1D intakeRunner; // Exhaust side private Pipe1D exhaustPipe; // Links private OpenEndLink intakeOpenEnd; private OrificeLink throttleOrifice; private OrificeLink plenumToRunner; private OrificeLink intakeValve; private OrificeLink exhaustValve; private OpenEndLink exhaustOpenEnd; private Solver solver; private SoundProcessor exhaustSoundProcessor; private SoundProcessor intakeSoundProcessor; private OutdoorExhaustReverb reverb; private double dt; private int stepCount; // ---------- Throttle control ---------- public double Throttle { get; set; } = 0.0; public double MaxThrottleArea { get; set; } = 6 * Units.cm2; // 2 cm² public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; solver = new Solver(); solver.SetTimeStep(dt); solver.CflTarget = 0.9; // ---- Crankshaft (external, passed to cylinder) ---- crankshaft = new Crankshaft(1000); crankshaft.Inertia = 0.05; crankshaft.FrictionConstant = 2; crankshaft.FrictionViscous = 0.05; // ---- Cylinder ---- double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0; cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { MaxIntakeArea = 3.7 * Units.cm2, MaxExhaustArea = 3.7 * Units.cm2, }; solver.AddComponent(cylinder); double pipeDiameter = 2 * Units.cm; double pipeArea = Units.AreaFromDiameter(pipeDiameter); exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f }; intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f }; reverb = new OutdoorExhaustReverb(sampleRate); // ---- Pipes ---- intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); intakeRunner = new Pipe1D(0.1, pipeArea, 5); exhaustPipe = new Pipe1D(1.00, pipeArea, 80); solver.AddComponent(intakePipeBeforeThrottle); solver.AddComponent(intakeRunner); solver.AddComponent(exhaustPipe); // ---- Plenum (5 mL) ---- intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0); var plenumInlet = intakePlenum.CreatePort(); var plenumOutlet = intakePlenum.CreatePort(); solver.AddComponent(intakePlenum); // ---- Intake open end ---- intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(intakeOpenEnd); // ---- Throttle orifice (variable area) ---- throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false, areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1)) { DischargeCoefficient = 0.1, UseInertance = false }; solver.AddOrificeLink(throttleOrifice); // ---- Plenum to runner (fixed area) ---- plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(plenumToRunner); // ---- Intake valve ---- intakeValve = new OrificeLink(cylinder.IntakePort, intakeRunner, isPipeLeftEnd: false, areaProvider: () => cylinder.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(intakeValve); // ---- Exhaust valve ---- exhaustValve = new OrificeLink(cylinder.ExhaustPort, exhaustPipe, isPipeLeftEnd: true, areaProvider: () => cylinder.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(exhaustValve); // ---- Exhaust open end ---- exhaustOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(exhaustOpenEnd); stepCount = 0; Console.WriteLine("4‑Stroke engine test (plenum + two pipes)"); Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}"); Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)"); } public override float Process() { cylinder.Crankshaft.Step(dt); cylinder.PreStep(dt); solver.Step(); stepCount++; if (stepCount % 10000 == 0) { double crankDeg = cylinder.Crankshaft.CrankAngle * 180.0 / Math.PI % 720.0; double cylP = cylinder.Pressure / 1e5; double cylT = cylinder.Temperature; double cylMass = cylinder.Mass * 1e6; double mdotI = intakeValve.LastMassFlowRate; double mdotE = exhaustValve.LastMassFlowRate; double pipeR = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5; double plenumP = intakePlenum.Pressure / 1e5; double actualArea = MaxThrottleArea * Throttle; Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " + $"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " + $"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar"); Console.WriteLine($"Throttle = {Throttle * 100:F0}% area = {actualArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar"); } float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd); float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd); return reverb.Process(intakeDry + exhaustDry); } 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; // Open end marker float openEndX = 40f; var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan }; openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f); target.Draw(openEndMark); // First intake pipe float pipe1StartX = openEndX; float pipe1EndX = pipe1StartX + 120f; DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX); // Throttle symbol float throttleX = pipe1EndX + 5f; var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(throttleX, intakeY - 15f) }; target.Draw(throttleRect); // Plenum float plenW = 60f, plenH = 80f; float plenLeftX = throttleX + 10f; float plenCenterX = plenLeftX + plenW / 2f; float plenTopY = intakeY - plenH / 2f; DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH); // Runner pipe float runnerStartX = plenLeftX + plenW + 5f; float runnerEndX = runnerStartX + 100f; DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX); // Cylinder float cylCX = runnerEndX + 50f; float cylTopY = intakeY - 120f; float cylW = 80f, cylMaxH = 240f; DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); // Exhaust pipe float exhStartX = cylCX + cylW / 2f + 20f; float exhEndX = winW - 60f; DrawPipe(target, exhaustPipe, exhaustY, exhStartX, exhEndX); // Exhaust open end marker var exhOpenEndMark = new CircleShape(5f) { FillColor = Color.Magenta }; exhOpenEndMark.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f); target.Draw(exhOpenEndMark); } } }