using FluidSim.Components; using FluidSim.Core; using FluidSim.Interfaces; using SFML.Graphics; using SFML.System; using System; namespace FluidSim.Tests { public class SingleCylScenario : Scenario { // ---------- Engine components ---------- private Crankshaft crankshaft; private Cylinder cylinder; // ---------- Fluid network ---------- private PipeSystem pipeSystem; private BoundarySystem boundaries; private Solver solver; // Volumes private Volume0D intakePlenum; // Ports private Port plenumInlet, plenumOutlet; // Orifice / open‑end indices private int throttleAreaIdx, plenumRunnerIdx, intakeValveIdx, exhaustValveIdx; private int intakeOpenIdx, exhaustOpenIdx; private float[] orificeAreas; // Sound private SoundProcessor exhaustSound, intakeSound; private OutdoorExhaustReverb reverb; // ---------- Simulation state ---------- private double dt; private int stepCount; // ---------- Geometry (Lifan YX140) ---------- // Bore 56 mm, Stroke 57 mm, CR 9.5 private const float Bore = 0.056f; private const float Stroke = 0.057f; private const float ConRod = 0.110f; // typical for 57 mm stroke private const float CompressionRatio = 9.5f; // Valve diameters (intake 27 mm, exhaust 23 mm) private const float IntakeValveDiam = 0.027f; private const float ExhaustValveDiam = 0.023f; private const float ValveLift = 0.006f; // 6 mm peak lift // Valve timings (degrees, 720° four‑stroke) // Intake: 15° BTDC → 45° ABDC private const float IVO = 345f; // 15° BTDC private const float IVC = 585f; // 45° ABDC (180°+45°) // Exhaust: 45° BBDC → 15° ATDC private const float EVO = 135f; // 45° BBDC (180°-45°) private const float EVC = 375f; // 15° ATDC (360°+15°) // Spark advance: 30° BTDC private const float SparkAdv = 30f; // Pipe / plenum sizes private const float PipeDiam = 0.025f; // 25 mm intake / exhaust private const float PipeArea = 0.00049087f; // π*D²/4 private const float PlenumVolume = 0.0005f; // 500 mL private const float MaxThrottleArea = 1e-4f; // ~1 cm² (fully open) // Pipe lengths and cell counts private const float IntakeLenBefore = 0.15f; // 15 cm before throttle private const float RunnerLen = 0.25f; // 25 cm runner private const float ExhaustLen = 0.60f; // 60 cm exhaust private const int CellsBefore = 6; private const int CellsRunner = 10; private const int CellsExhaust = 24; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // ---------- Crankshaft ---------- crankshaft = new Crankshaft(600); // start at ~600 RPM crankshaft.Inertia = 0.2f; crankshaft.FrictionConstant = 2.0f; crankshaft.FrictionViscous = 0.04f; // ---------- Cylinder ---------- cylinder = new Cylinder(Bore, Stroke, ConRod, CompressionRatio, IVO, IVC, EVO, EVC, crankshaft) { IntakeValveDiameter = IntakeValveDiam, ExhaustValveDiameter = ExhaustValveDiam, IntakeValveLift = ValveLift, ExhaustValveLift = ValveLift, SparkAdvance = SparkAdv, EnergyVariationFraction = 0.03f, // small cycle‑to‑cycle variation MisfireProbability = 0.0f }; // ---------- Pipe system ---------- int totalCells = CellsBefore + CellsRunner + CellsExhaust; int[] pipeStart = { 0, CellsBefore, CellsBefore + CellsRunner }; int[] pipeEnd = { CellsBefore, CellsBefore + CellsRunner, totalCells }; float[] areas = new float[totalCells]; float[] dxs = new float[totalCells]; float dxBefore = IntakeLenBefore / CellsBefore; float dxRunner = RunnerLen / CellsRunner; float dxExh = ExhaustLen / CellsExhaust; for (int i = 0; i < totalCells; i++) { areas[i] = PipeArea; if (i < CellsBefore) dxs[i] = dxBefore; else if (i < CellsBefore + CellsRunner) dxs[i] = dxRunner; else dxs[i] = dxExh; } float rho0 = 101325f / (287f * 300f); pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, areas, dxs, rho0, 0f, 101325f); pipeSystem.DampingMultiplier = 0.5f; pipeSystem.EnergyRelaxationRate = 0f; // adiabatic pipes pipeSystem.AmbientPressure = 101325f; // ---------- Volumes ---------- intakePlenum = new Volume0D(PlenumVolume, 101325f, 300f); plenumInlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort(); // ---------- Boundary system ---------- boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2); throttleAreaIdx = 0; plenumRunnerIdx = 1; intakeValveIdx = 2; exhaustValveIdx = 3; // Open ends boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, PipeArea); intakeOpenIdx = 0; boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, PipeArea); exhaustOpenIdx = 1; // Orifices // throttle – variable area, low discharge for restriction boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false, throttleAreaIdx, dischargeCoeff: 0.8f); // plenum → runner boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true, plenumRunnerIdx, dischargeCoeff: 1.0f); // intake valve boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false, intakeValveIdx, dischargeCoeff: 1.0f); // exhaust valve boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true, exhaustValveIdx, dischargeCoeff: 1.0f); orificeAreas = new float[4]; orificeAreas[plenumRunnerIdx] = PipeArea; // fixed full‑bore // ---------- Solver ---------- solver = new Solver { SubStepCount = 5, EnableProfiling = false }; solver.SetTimeStep(dt); solver.SetPipeSystem(pipeSystem); solver.SetBoundarySystem(boundaries); solver.AddComponent(cylinder); solver.AddComponent(intakePlenum); // ---------- Sound ---------- exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 0.2f }; intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 0.2f }; reverb = new OutdoorExhaustReverb(sampleRate); stepCount = 0; Console.WriteLine("Single‑cylinder engine (YX140) ready."); } public override float Process() { // ---- Crank and cylinder pre‑step ---- crankshaft.Step((float)dt); cylinder.PreStep((float)dt); // ---- Update variable areas ---- float throttledArea = MaxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1.0f); orificeAreas[throttleAreaIdx] = throttledArea; orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; boundaries.SetOrificeAreas(orificeAreas); // ---- Fluids step ---- solver.Step(); stepCount++; // ---- Sound ---- 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); Console.WriteLine($"Step {stepCount}, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar, " + $"Throttle={Throttle * 100:F0}%"); } return reverb.Process(exhaustDry + intakeDry); } 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 leftX = 40f; // Intake open end marker var om = new CircleShape(5f) { FillColor = Color.Cyan }; om.Position = new Vector2f(leftX - 5f, intakeY - 5f); target.Draw(om); // Pipe 0 – before throttle float p0EndX = leftX + 80f; DrawPipe(target, pipeSystem, 0, intakeY, leftX, p0EndX); // Throttle symbol float thrX = p0EndX + 5f; var thr = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, Position = new Vector2f(thrX, intakeY - 15f) }; target.Draw(thr); // Plenum volume float plenW = 60f, plenH = 50f; float plenLeftX = thrX + 12f; float plenCenterX = plenLeftX + plenW / 2f; float plenTopY = intakeY - plenH / 2f; DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH); // Pipe 1 – runner float rStartX = plenLeftX + plenW + 10f; float rEndX = rStartX + 100f; DrawPipe(target, pipeSystem, 1, intakeY, rStartX, rEndX); // Cylinder float cylCX = rEndX + 50f; float cylTopY = intakeY - 120f; float cylW = 80f, cylMaxH = 240f; DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); // Pipe 2 – exhaust float exhStartX = cylCX + cylW / 2f + 20f; float exhEndX = winW - 60f; DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, exhEndX); // Exhaust open end var em = new CircleShape(5f) { FillColor = Color.Magenta }; em.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f); target.Draw(em); } } }