using System; using FluidSim.Components; using FluidSim.Utils; using FluidSim.Interfaces; using SFML.Graphics; using SFML.System; namespace FluidSim.Core { public class EngineScenario : Scenario { private Solver solver; private Crankshaft crankshaft; private EngineCylinder engineCyl; private Pipe1D exhaustPipe; private Pipe1D intakePipe; private PipeVolumeConnection couplingExhaust; private PipeVolumeConnection couplingIntake; private SoundProcessor exhaustSoundProcessor; private SoundProcessor intakeSoundProcessor; private OutdoorExhaustReverb reverb; private Port exhaustPort = new Port(); private Port intakePort = new Port(); private double dt; private double exhPipeArea, intPipeArea; private const double AmbientPressure = 101325.0; private double time; private int stepCount = 0; private const int LogInterval = 1000; public double Throttle { get; set; } = 0.15; private const double FullLoadPeakPressure = 60.0 * Units.bar; public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; // Crankshaft crankshaft = new Crankshaft(initialRPM: 2000.0) { Inertia = 0.05, FrictionConstant = 0.5, FrictionViscous = 0.01 }; // Exhaust pipe (longer, larger) double exhLength = 1; double exhRadius = 0.02; exhPipeArea = Math.PI * exhRadius * exhRadius; exhaustPipe = new Pipe1D(exhLength, exhPipeArea, sampleRate, forcedCellCount: 100); exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure); exhaustPipe.DampingMultiplier = 0.0; exhaustPipe.EnergyRelaxationRate = 100.0f; // Intake pipe (shorter, narrower) double intLength = 1; double intRadius = 0.01; intPipeArea = Math.PI * intRadius * intRadius; intakePipe = new Pipe1D(intLength, intPipeArea, sampleRate, forcedCellCount: 50); intakePipe.SetUniformState(1.225, 0.0, AmbientPressure); // Cylinder (starts at BDC, fresh charge) engineCyl = new EngineCylinder(crankshaft, bore: 0.065, stroke: 0.0565, compressionRatio: 8.0, exhPipeArea: exhPipeArea, intPipeArea: intPipeArea, sampleRate: sampleRate); engineCyl.ignition = true; // Set crank to BDC (180°) and sync crankshaft.CrankAngle = Math.PI; crankshaft.PreviousAngle = Math.PI; // make sure this property is settable (public setter) // Couplings couplingExhaust = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0); couplingIntake = new PipeVolumeConnection(engineCyl.Cylinder, intakePipe, false, orificeArea: 0.0); // Solver solver = new Solver(); solver.SetTimeStep(dt); solver.AddVolume(engineCyl.Cylinder); solver.AddPipe(exhaustPipe); solver.AddPipe(intakePipe); solver.AddConnection(couplingExhaust); solver.AddConnection(couplingIntake); solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure); solver.SetPipeBoundary(intakePipe, true, BoundaryType.GhostCell); // cylinder side – left solver.SetPipeBoundary(intakePipe, false, BoundaryType.OpenEnd, AmbientPressure); // ambient side – right // Sound exhaustSoundProcessor = new SoundProcessor(sampleRate, exhRadius * 2); exhaustSoundProcessor.Gain = 0.001f; intakeSoundProcessor = new SoundProcessor(sampleRate, intRadius * 2); intakeSoundProcessor.Gain = 0.001f; // Reverb reverb = new OutdoorExhaustReverb(sampleRate); reverb.DryMix = 1.0f; reverb.EarlyMix = 0.5f; reverb.TailMix = 0.9f; reverb.Feedback = 0.9f; reverb.DampingFreq = 6000f; Console.WriteLine("=== Engine with intake & cycle‑aware valves ==="); } public override float Process() { double throttle = Math.Clamp(Throttle, 0.2, 1.0); double targetPressure = throttle * FullLoadPeakPressure; engineCyl.TargetPeakPressure = targetPressure; engineCyl.Step(dt); crankshaft.Step(dt); couplingExhaust.OrificeArea = engineCyl.ExhaustOrificeArea; couplingIntake.OrificeArea = engineCyl.IntakeOrificeArea; solver.Step(); UpdateExhaustPort(); UpdateIntakePort(); float dryExhaust = exhaustSoundProcessor.Process(exhaustPort); float dryIntake = intakeSoundProcessor.Process(intakePort); float dry = dryExhaust + dryIntake; float wet = reverb.Process(dry); if (++stepCount % LogInterval == 0) Log(); return wet; } private void Log() { double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI); double cycleDeg = (engineCyl.CycleAngle * 180.0 / Math.PI) % 720.0; string stroke = cycleDeg < 180.0 ? "Power" : cycleDeg < 360.0 ? "Exhaust" : cycleDeg < 540.0 ? "Intake" : "Compression"; // Cylinder double pCyl = engineCyl.Cylinder.Pressure; double TCyl = engineCyl.Cylinder.Temperature; double VCyl = engineCyl.Cylinder.Volume; double mCyl = engineCyl.Cylinder.Mass; double exhArea = engineCyl.ExhaustOrificeArea * 1e6; // mm² double intArea = engineCyl.IntakeOrificeArea * 1e6; // mm² // Exhaust pipe int exhLast = exhaustPipe.GetCellCount() - 1; double pExhEnd = exhaustPipe.GetCellPressure(exhLast); double mdotExhOut = exhaustPipe.GetOpenEndMassFlow(); // positive out // Intake pipe double mdotIntIn = couplingIntake.LastMassFlowIntoVolume; double pIntAmbEnd = intakePort.Pressure; Console.WriteLine( $"{stepCount,8} {stroke,-11} {cycleDeg,6:F1}° " + $"RPM:{rpm,5:F0} " + $"Cyl: p={pCyl/1e5,6:F3}bar T={TCyl,6:F0}K V={VCyl*1e6,6:F0}cm³ m={mCyl*1e3,6:F6}g " + $"Valves: Exh={exhArea,5:F0}mm² Int={intArea,5:F0}mm² " + $"Intake: p_end={pIntAmbEnd/1e5,6:F3}bar mdot_in={mdotIntIn,7:F4}kg/s " + $"Exhaust: p_end={pExhEnd/1e5,6:F3}bar mdot_out={mdotExhOut,7:F4}kg/s"); } private void UpdateExhaustPort() { int last = exhaustPipe.GetCellCount() - 1; double p = exhaustPipe.GetCellPressure(last); double rho = exhaustPipe.GetCellDensity(last); double vel = exhaustPipe.GetCellVelocity(last); // Safety clamps rho = Math.Clamp(rho, 0.01, 50.0); vel = Math.Clamp(vel, -500.0, 500.0); p = Math.Clamp(p, 1e4, 2e6); double outflowMassFlow = rho * vel * exhPipeArea; exhaustPort.Pressure = p; exhaustPort.Density = rho; exhaustPort.Temperature = p / (rho * 287.05); exhaustPort.MassFlowRate = -outflowMassFlow; exhaustPort.SpecificEnthalpy = 0.0; } private void UpdateIntakePort() { // Use the actual valve mass flow (positive = into cylinder) double mdotIntoEngine = couplingIntake.LastMassFlowIntoVolume; // Use cylinder pressure/density for the port state (or intake pipe last cell) double pCyl = engineCyl.Cylinder.Pressure; double rhoCyl = engineCyl.Cylinder.Density; intakePort.Pressure = Math.Max(pCyl, 100); intakePort.Density = Math.Max(rhoCyl, 1e-6); intakePort.Temperature = engineCyl.Cylinder.Temperature; intakePort.MassFlowRate = mdotIntoEngine; intakePort.SpecificEnthalpy = 0.0; } // ==================== Drawing ==================== public override void Draw(RenderWindow target) { float winW = target.GetView().Size.X; float winH = target.GetView().Size.Y; float centerY = winH / 2f; const float T_ambient = 293.15f; const float T_hot = 1500f; const float T_cold = 0f; const float R = 287.05f; float deltaHot = T_hot - T_ambient; float deltaCold = T_ambient - T_cold; float NormaliseTemperature(double T) { double t; if (T >= T_ambient) t = (T - T_ambient) / deltaHot; else t = (T - T_ambient) / deltaCold; return (float)Math.Clamp(t, -1.0, 1.0); } // ---- Cylinder ---- float cylW = 80f, cylH = 150f; var cylRect = new RectangleShape(new Vector2f(cylW, cylH)); cylRect.Position = new Vector2f(200f, centerY - cylH / 2f); double tempCyl = engineCyl.Cylinder.Temperature; float tnCyl = NormaliseTemperature(tempCyl); byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0); byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0); byte gC = (byte)(255 * (1 - Math.Abs(tnCyl))); cylRect.FillColor = new Color(rC, gC, bC); target.Draw(cylRect); // ---- Piston ---- float pistonWidth = cylW - 12f; float pistonHeight = 16f; float pistonFraction = (float)engineCyl.PistonPositionFraction; float pistonTopY = cylRect.Position.Y + pistonFraction * (cylH - pistonHeight); var pistonRect = new RectangleShape(new Vector2f(pistonWidth, pistonHeight)) { Position = new Vector2f(cylRect.Position.X + 6f, pistonTopY), FillColor = new Color(80, 80, 80) }; target.Draw(pistonRect); // ---- Exhaust pipe (rightwards) ---- DrawPipe(target, exhaustPipe, startX: 280f, endX: winW - 60f, centerY, T_ambient, T_hot, T_cold, R, NormaliseTemperature, true); // ---- Intake pipe (leftwards) ---- DrawPipe(target, intakePipe, startX: 200f, endX: 20f, centerY, T_ambient, T_hot, T_cold, R, NormaliseTemperature, false); } private void DrawPipe(RenderWindow target, Pipe1D pipe, float startX, float endX, float centerY, float T_ambient, float T_hot, float T_cold, float R, Func normaliseTemp, bool leftToRight) { int n = pipe.GetCellCount(); float dir = leftToRight ? 1f : -1f; float pipeLen = Math.Abs(endX - startX); float dx = pipeLen / (n - 1) * dir; float baseRadius = leftToRight ? 20f : 14f; // exhaust thicker, intake thinner var vertices = new Vertex[n * 2]; float ambPress = 101325f; for (int i = 0; i < n; i++) { float x = startX + i * dx; double p = pipe.GetCellPressure(i); double rho = pipe.GetCellDensity(i); double T = p / (rho * R); float r = baseRadius * 0.3f * (float)(1.0 + (p - ambPress) / ambPress); if (r < 2f) r = 2f; float tn = normaliseTemp(T); byte rC = (byte)(tn > 0 ? 255 * tn : 0); byte bC = (byte)(tn < 0 ? -255 * tn : 0); byte gC = (byte)(255 * (1 - Math.Abs(tn))); var col = new Color(rC, gC, bC); vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col); vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col); } target.Draw(vertices, PrimitiveType.TriangleStrip); } } }