using System; using SFML.Graphics; using SFML.System; using FluidSim.Components; using FluidSim.Core; namespace FluidSim.Tests { public class TestScenario : Scenario { // Engine private Cylinder cylinder; // Intake side private Pipe1D intakePipeBeforeThrottle; // pipe from ambient to plenum private Volume0D intakePlenum; // plenum (100 mL) private Pipe1D intakeRunner; // pipe from plenum to cylinder // Exhaust side private Pipe1D exhaustPipe; // Links private OpenEndLink intakeOpenEnd; // ambient → left end of first pipe private OrificeLink throttleOrifice; // first pipe right end → plenum inlet (variable area) private OrificeLink plenumToRunner; // plenum outlet → runner left end (fixed area) private OrificeLink intakeValve; // runner right end → cylinder intake port private OrificeLink exhaustValve; private OpenEndLink exhaustOpenEnd; private Solver solver; private SoundProcessor soundProcessor; private double dt; private int stepCount; public double ThrottleArea { get; set; } = 0.0; // controlled externally public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f }; solver = new Solver(); solver.SetTimeStep(dt); solver.CflTarget = 0.9; // ---- Cylinder (no valve overlap to avoid backflow) ---- double bore = 0.056, stroke = 0.050, conRod = 0.110, compRatio = 10.0; double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0; cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, 1000) { MaxIntakeArea = 0.00037, MaxExhaustArea = 0.00037, }; solver.AddComponent(cylinder); double pipeArea = 0.00037; // 3.7 cm² // ---- Pipes ---- intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); // short pipe before throttle intakeRunner = new Pipe1D(0.10, pipeArea, 5); // runner after plenum exhaustPipe = new Pipe1D(1.00, pipeArea, 80); solver.AddComponent(intakePipeBeforeThrottle); solver.AddComponent(intakeRunner); solver.AddComponent(exhaustPipe); // ---- Plenum (100 mL) ---- intakePlenum = new Volume0D(0.0001, 101325.0, 300.0); // 0.0001 m³ var plenumInlet = intakePlenum.CreatePort(); // from throttle var plenumOutlet = intakePlenum.CreatePort(); // to runner solver.AddComponent(intakePlenum); // ---- Intake open end (ambient → left end of first pipe) ---- intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true) { AmbientPressure = 101325.0, Gamma = 1.4 }; solver.AddOpenEndLink(intakeOpenEnd); // ---- Throttle orifice (first pipe right end → plenum inlet) ---- throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false, areaProvider: () => ThrottleArea) { DischargeCoefficient = 0.1, // realistic throttle Cd UseInertance = false }; solver.AddOrificeLink(throttleOrifice); // ---- Plenum → runner (fixed area = pipe area) ---- plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false }; solver.AddOrificeLink(plenumToRunner); // ---- Intake valve (runner right end → cylinder intake port) ---- 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() { // 1. Advance crankshaft & pre‑step cylinder.Crankshaft.Step(dt); cylinder.PreStep(dt); // 2. Run solver solver.Step(); stepCount++; // 3. Log every 200 steps if (stepCount % 200 == 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; 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 area = {ThrottleArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar"); } return soundProcessor.Process(exhaustOpenEnd); } public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; // Fixed vertical centres for intake and exhaust float intakeY = winH / 2f - 40f; float exhaustY = winH / 2f + 80f; // ---- 1. Open end (ambient air source) ---- float openEndX = 40f; var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan }; openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f); target.Draw(openEndMark); // ---- 2. First intake pipe (ambient → throttle) ---- float pipe1StartX = openEndX; float pipe1EndX = pipe1StartX + 120f; DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX); // ---- 3. Throttle (symbolic restriction) ---- float throttleX = pipe1EndX + 5f; var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(throttleX, intakeY - 15f) }; target.Draw(throttleRect); // ---- 4. 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); // ---- 5. Runner pipe (plenum → cylinder) ---- float runnerStartX = plenLeftX + plenW + 5f; float runnerEndX = runnerStartX + 100f; DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX); // ---- 6. Cylinder ---- float cylCX = runnerEndX + 50f; // center X float cylTopY = intakeY - 120f; // top of cylinder (so it sits above the pipe) float cylW = 80f, cylMaxH = 240f; DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); // ---- 7. Exhaust pipe (cylinder → open end) ---- 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); } } }