diff --git a/Components/Cylinder.cs b/Components/Cylinder.cs index ddf460f..4dabab5 100644 --- a/Components/Cylinder.cs +++ b/Components/Cylinder.cs @@ -46,9 +46,7 @@ namespace FluidSim.Components public double FuelLowerHeatingValue { get; set; } = 44e6; // Cycle‑to‑cycle randomness - /// Fractional variation in fuel energy (±). 0.05 = ±5%. public double EnergyVariationFraction { get; set; } = 0.05; - /// Probability of a misfire (0‑1). public double MisfireProbability { get; set; } = 0.01; // Heat loss @@ -56,7 +54,14 @@ namespace FluidSim.Components public double HeatTransferCoefficient { get; set; } = 100.0; public double AmbientTemperature { get; set; } = 300.0; - // State + // ---- Multi‑cylinder support ---- + /// + /// Phase offset (radians) added to the crankshaft angle for this cylinder. + /// Used for multi‑cylinder engines; set to 0 for single‑cylinder. + /// + public double PhaseOffset { get; set; } = 0.0; + + // State (public for drawing) public double Volume => cylinderVolume; public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12); public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12); @@ -75,8 +80,7 @@ namespace FluidSim.Components private bool combustionActive; private bool fuelInjected; - // per‑cycle randomness - private double _energyFactor = 1.0; // applied to FuelLowerHeatingValue this cycle + private double _energyFactor = 1.0; private readonly Random _random = new Random(); private const double Gamma = 1.4; @@ -115,7 +119,10 @@ namespace FluidSim.Components private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0); private double CrankRadius => Stroke / 2.0; private double Obliquity => CrankRadius / ConRodLength; - private double CrankDeg => (Crankshaft.CrankAngle % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0; + + // Offset-aware crank angle in degrees + private double CrankDeg => + ((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0; public double ComputeVolume(double thetaRad) { @@ -174,7 +181,9 @@ namespace FluidSim.Components public void PreStep(double dt) { double prevVolume = cylinderVolume; - double crankAngleRad = Crankshaft.CrankAngle; + + // ----- Use phase‑offset crank angle for this cylinder ----- + double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset; cylinderVolume = ComputeVolume(crankAngleRad); double dV = cylinderVolume - prevVolume; @@ -191,7 +200,9 @@ namespace FluidSim.Components cylinderEnergy -= Pressure * dV; - double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0; + // Also use offset angle for event detection + double crankshaftPrevAngle = Crankshaft.PreviousAngle; + double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0; double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0; // ----- Intake closing: capture trapped air mass and compute fuel ----- @@ -202,7 +213,7 @@ namespace FluidSim.Components fuelInjected = true; } - // ----- Spark ignition (once per cycle, with misfire chance) ----- + // ----- Spark ignition ----- double sparkAngle = 0.0 - SparkAdvance; if (sparkAngle < 0) sparkAngle += 720.0; @@ -210,19 +221,15 @@ namespace FluidSim.Components (prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle); if (crossedSpark && !combustionActive && fuelInjected) { - // Decide misfire bool misfire = _random.NextDouble() < MisfireProbability; if (misfire) { - combustionActive = false; // no combustion this cycle - // fuel is not burned – will remain in cylinder and eventually exit as unburned mixture + combustionActive = false; } else { combustionActive = true; burnFraction = 0.0; - - // Energy variation factor for this cycle double range = EnergyVariationFraction; _energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0); } @@ -239,7 +246,6 @@ namespace FluidSim.Components { newFraction = 1.0; combustionActive = false; - // All gas becomes exhaust double totalMass = _airMass + _exhaustMass; _airMass = 0.0; _exhaustMass = totalMass; @@ -255,7 +261,7 @@ namespace FluidSim.Components } } - // ----- Heat loss to cylinder walls ----- + // ----- Heat loss ----- double dQ_loss = HeatTransferCoefficient * CylinderWallArea * (Temperature - AmbientTemperature) * dt; cylinderEnergy -= dQ_loss; diff --git a/Components/Pipe1D.cs b/Components/Pipe1D.cs index d7770dc..a95301d 100644 --- a/Components/Pipe1D.cs +++ b/Components/Pipe1D.cs @@ -18,6 +18,7 @@ namespace FluidSim.Components public double Area { get; } public double DampingMultiplier { get; set; } = 10.0; public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s + public string Name = "Pipe"; private double _ambientPressure = 101325.0; public double AmbientPressure diff --git a/Core/Solver.cs b/Core/Solver.cs index ab2e8f4..f87b0ee 100644 --- a/Core/Solver.cs +++ b/Core/Solver.cs @@ -20,17 +20,11 @@ namespace FluidSim.Core // ---------- Timing accumulators (reset every LogInterval steps) ---------- private long _stepCount; - private double _timeTotal; - private double _timeCFL; - private double _timeOrifice; - private double _timeOpenEnd; - private double _timeJunction; - private double _timePipe; - private double _timeClearGhosts; - private double _timeUpdateState; + private double _timeTotal, _timeCFL, _timeOrifice, _timeOpenEnd, + _timePipe, _timeClearGhosts, _timeUpdateState; - private const int LogInterval = 5000; // print once per second (at 44.1 kHz) - private const bool EnableLogging = false; + private const int LogInterval = 5000; + private const bool EnableLogging = false; // temporarily ON for debugging public void SetTimeStep(double dt) => _dt = dt; @@ -45,18 +39,44 @@ namespace FluidSim.Core var sw = Stopwatch.StartNew(); - // CFL count + // CFL count – track which pipe demands the most sub‑steps int nSub = 1; + Pipe1D worstPipe = pipes[0]; foreach (var p in pipes) - nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt, CflTarget)); + { + int n = p.GetRequiredSubSteps(_dt, CflTarget); + if (n > nSub) + { + nSub = n; + worstPipe = p; + } + } double dtSub = _dt / nSub; + // ----- Diagnostic: warn if nSub is high ----- + if (nSub > 50) + { + double maxW = 0; + for (int i = 0; i < worstPipe.CellCount; i++) + { + double rho = worstPipe.GetCellDensity(i); + double u = Math.Abs(worstPipe.GetCellVelocity(i)); + double p = worstPipe.GetCellPressure(i); + double c = Math.Sqrt(1.4 * p / Math.Max(rho, 1e-12)); + if (u + c > maxW) maxW = u + c; + } + Console.WriteLine($"nSub = {nSub} (worst pipe: {worstPipe.Name}, maxW = {maxW:F0} m/s)"); + } + _timeCFL += sw.Elapsed.TotalSeconds; + // ----- Safety cap – prevent the solver from hanging ----- const int maxSubSteps = 10000; - if (nSub > maxSubSteps) + const int hardLimit = 500; // temporary low cap for debugging + + if (nSub > hardLimit) { - Console.WriteLine($"Warning: required sub‑steps {nSub} exceeds limit. Simulation stopped."); + Console.WriteLine($"nSub ({nSub}) exceeds hard limit {hardLimit}. Simulation step skipped."); return; } @@ -90,49 +110,33 @@ namespace FluidSim.Core comp.UpdateState(_dt); _timeUpdateState += sw.Elapsed.TotalSeconds - tUS; - // accumulate total step time (includes CFL, sub‑steps, clear ghosts, update state) _timeTotal += sw.Elapsed.TotalSeconds; - // ---------- Periodic report ---------- _stepCount++; if (_stepCount % LogInterval == 0 && EnableLogging) { if (_timeTotal > 0) { - double totalMs = _timeTotal * 1000.0; - double avgUs = (_timeTotal / LogInterval) * 1e6; // µs per step - double stepsPerSec = LogInterval / _timeTotal; // steps per second + double stepsPerSec = LogInterval / _timeTotal; + double avgUs = (_timeTotal / LogInterval) * 1e6; Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---"); Console.WriteLine($" Steps per second: {stepsPerSec:F1}"); Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})"); - Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} % ({_timeCFL * 1e6 / LogInterval:F1} µs/step)"); + Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} %"); Console.WriteLine($" Sub‑step loop:"); - Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} % ({_timeOrifice * 1e6 / LogInterval:F1} µs/step)"); - Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} % ({_timeOpenEnd * 1e6 / LogInterval:F1} µs/step)"); - Console.WriteLine($" Junctions: {_timeJunction / _timeTotal * 100:F1} % ({_timeJunction * 1e6 / LogInterval:F1} µs/step)"); - Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} % ({_timePipe * 1e6 / LogInterval:F1} µs/step)"); - Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} % ({_timeClearGhosts * 1e6 / LogInterval:F1} µs/step)"); - Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} % ({_timeUpdateState * 1e6 / LogInterval:F1} µs/step)"); + Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} %"); + Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} %"); + Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} %"); + Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} %"); + Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} %"); Console.WriteLine(); - - // ---------- Optional detailed pipe profiling ---------- - if (Pipe1D.EnableDetailedProfiling) - { - foreach (var pipe in pipes) - { - Console.WriteLine(pipe.GetDetailProfileReport()); - pipe.ResetDetailCounters(); - } - } } - // Reset accumulators for next interval _timeTotal = 0; _timeCFL = 0; _timeOrifice = 0; _timeOpenEnd = 0; - _timeJunction = 0; _timePipe = 0; _timeClearGhosts = 0; _timeUpdateState = 0; diff --git a/Program.cs b/Program.cs index c18f6ee..02c910e 100644 --- a/Program.cs +++ b/Program.cs @@ -33,7 +33,7 @@ public class Program // Audio & simulation private static SimulationRingBuffer _simRingBuffer = null!; private static SoundEngine _soundEngine = null!; - private static TestScenario _scenario = null!; // cast to access ThrottleArea + private static Scenario _scenario = null!; // cast to access ThrottleArea private static Font? _overlayFont; private static Text? _overlayText; @@ -50,7 +50,8 @@ public class Program { var window = CreateWindow(); LoadFont(); - _scenario = (TestScenario)InitializeScenario(); + _scenario = new TestScenario(); + _scenario.Initialize(SampleRate); _lastThrottleUpdateTime = 0.0; _simRingBuffer = new SimulationRingBuffer(131072); @@ -170,13 +171,6 @@ public class Program }; } - private static Scenario InitializeScenario() - { - var sc = new TestScenario(); - sc.Initialize(SampleRate); - return sc; - } - private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e) { if (_timeWarpActive) return; diff --git a/Scenarios/Inline4Scenario.cs b/Scenarios/Inline4Scenario.cs new file mode 100644 index 0000000..c812729 --- /dev/null +++ b/Scenarios/Inline4Scenario.cs @@ -0,0 +1,303 @@ +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 + private Pipe1D runner1, runner2, runner3, runner4; + + // Exhaust pipes + private Pipe1D exh1, exh2, exh3, exh4; + + // 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 + private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4; + + // Exhaust open ends + private OpenEndLink exhaustOpenEnd1, exhaustOpenEnd2, exhaustOpenEnd3, exhaustOpenEnd4; + + private Solver solver; + private SoundProcessor exhaustSoundProcessor; + private SoundProcessor intakeSoundProcessor; + private OutdoorExhaustReverb reverb; + private double dt; + private int stepCount; + + public double MaxThrottleArea { get; set; } = 3 * Units.cm2; + + public override void Initialize(int sampleRate) + { + dt = 1.0 / sampleRate; + + solver = new Solver(); + solver.SetTimeStep(dt); + solver.CflTarget = 0.9; + + // ---- Shared crankshaft ---- + crankshaft = new Crankshaft(800); + crankshaft.Inertia = 1; + crankshaft.FrictionConstant = 16; + crankshaft.FrictionViscous = 0.5; + + // ---- Cylinder geometry ---- + double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; + double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0; + + // Firing order 1-3-4-2 → phase offsets in radians + double phase0 = 0.0 * Math.PI / 180.0; + double phase1 = 180.0 * Math.PI / 180.0; + double phase2 = 540.0 * Math.PI / 180.0; + double phase3 = 360.0 * Math.PI / 180.0; + + 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 = phase0, + EnergyVariationFraction = 0.03, + MisfireProbability = 0.01 + }; + 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 = phase1, + EnergyVariationFraction = 0.03, + MisfireProbability = 0.01 + }; + 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 = phase2, + EnergyVariationFraction = 0.03, + MisfireProbability = 0.01 + }; + 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 = phase3, + EnergyVariationFraction = 0.03, + MisfireProbability = 0.01 + }; + solver.AddComponent(cyl1); + solver.AddComponent(cyl2); + solver.AddComponent(cyl3); + solver.AddComponent(cyl4); + + double pipeDiameter = 2 * Units.cm; + double pipeArea = Units.AreaFromDiameter(pipeDiameter); + + exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; + intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; + reverb = new OutdoorExhaustReverb(sampleRate); + + // ---- Intake pipe before throttle ---- + intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10); + solver.AddComponent(intakePipeBeforeThrottle); + + // ---- Plenum ---- + intakePlenum = new Volume0D(50 * 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); + + // ---- Runners ---- + runner1 = new Pipe1D(0.2, pipeArea, 5); + runner2 = new Pipe1D(0.2, pipeArea, 5); + runner3 = new Pipe1D(0.2, pipeArea, 5); + runner4 = new Pipe1D(0.2, pipeArea, 5); + solver.AddComponent(runner1); + solver.AddComponent(runner2); + solver.AddComponent(runner3); + solver.AddComponent(runner4); + + // ---- Exhaust pipes ---- + exh1 = new Pipe1D(0.2, pipeArea, 10); + exh2 = new Pipe1D(0.2, pipeArea, 10); + exh3 = new Pipe1D(0.2, pipeArea, 10); + exh4 = new Pipe1D(0.2, pipeArea, 10); + solver.AddComponent(exh1); + solver.AddComponent(exh2); + solver.AddComponent(exh3); + solver.AddComponent(exh4); + + // ---- Plenum → runner orifices ---- + 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 ---- + 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 ---- + exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exh1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exh2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exh3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exh4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false }; + solver.AddOrificeLink(exhaustValve1); + solver.AddOrificeLink(exhaustValve2); + solver.AddOrificeLink(exhaustValve3); + solver.AddOrificeLink(exhaustValve4); + + // ---- Exhaust open ends ---- + exhaustOpenEnd1 = new OpenEndLink(exh1, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; + exhaustOpenEnd2 = new OpenEndLink(exh2, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; + exhaustOpenEnd3 = new OpenEndLink(exh3, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; + exhaustOpenEnd4 = new OpenEndLink(exh4, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 }; + solver.AddOpenEndLink(exhaustOpenEnd1); + solver.AddOpenEndLink(exhaustOpenEnd2); + solver.AddOpenEndLink(exhaustOpenEnd3); + solver.AddOpenEndLink(exhaustOpenEnd4); + + // ---- 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.001, 1.0)) + { + DischargeCoefficient = 0.2, + 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"); + } + + // Mix all exhaust sounds + float exhaustMix = exhaustSoundProcessor.Process(exhaustOpenEnd1) + + exhaustSoundProcessor.Process(exhaustOpenEnd2) + + exhaustSoundProcessor.Process(exhaustOpenEnd3) + + exhaustSoundProcessor.Process(exhaustOpenEnd4); + float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd); + return reverb.Process(exhaustMix * 0.25f + intakeDry); + } + + public override void Draw(RenderWindow target) + { + float winW = target.GetView().Size.X; + float winH = target.GetView().Size.Y; + + float startX = 60f; + float spacing = 80f; + float intakeY = winH / 2f - 80f; + float exhaustY = winH / 2f + 80f; + + // Plenum + float plenW = 50f, plenH = 120f; + float plenX = startX; + float plenTopY = intakeY - plenH / 2f; + DrawVolume(target, intakePlenum, plenX, plenTopY, plenW, plenH); + + // Helper arrays just for drawing (no closures) + var cyls = new[] { cyl1, cyl2, cyl3, cyl4 }; + var runners = new[] { runner1, runner2, runner3, runner4 }; + var exhausts = new[] { exh1, exh2, exh3, exh4 }; + + for (int i = 0; i < 4; i++) + { + float cylX = plenX + plenW + 30f + i * spacing; + float runnerStartX = plenX + plenW + 5f; + float runnerEndX = cylX - 20f; + DrawPipe(target, runners[i], intakeY, runnerStartX, runnerEndX); + + float cylTopY = intakeY - 120f; + DrawCylinder(target, cyls[i], cylX, cylTopY, 70f, 200f); + + float exhStartX = cylX + 35f; + float exhEndX = exhStartX + 100f; + DrawPipe(target, exhausts[i], exhaustY, exhStartX, exhEndX); + + var mark = new CircleShape(4f) { FillColor = Color.Magenta }; + mark.Position = new Vector2f(exhEndX - 4f, exhaustY - 4f); + target.Draw(mark); + } + + // Throttle symbol + var throttleRect = new RectangleShape(new Vector2f(6f, 30f)) + { + FillColor = Color.Yellow, + Position = new Vector2f(plenX - 16f, intakeY - 15f) + }; + target.Draw(throttleRect); + } + } +} \ No newline at end of file diff --git a/Scenarios/Scenario.cs b/Scenarios/Scenario.cs index f38e239..d519112 100644 --- a/Scenarios/Scenario.cs +++ b/Scenarios/Scenario.cs @@ -13,6 +13,7 @@ namespace FluidSim.Tests protected const double AmbientPressure = 101325.0; protected const double AmbientTemperature = 300.0; + public double Throttle { get; set; } = 0.0; // ---------- Color from pressure (volumes) ---------- protected Color PressureColor(double pressurePa) diff --git a/Scenarios/TestScenario.cs b/Scenarios/TestScenario.cs index 2226fa0..6996863 100644 --- a/Scenarios/TestScenario.cs +++ b/Scenarios/TestScenario.cs @@ -37,8 +37,7 @@ namespace FluidSim.Tests private int stepCount; // ---------- Throttle control ---------- - public double Throttle { get; set; } = 0.0; - public double MaxThrottleArea { get; set; } = 3 * Units.cm2; // 2 cm² + public double MaxThrottleArea { get; set; } = 1 * Units.cm2; // 2 cm² public override void Initialize(int sampleRate) { @@ -50,7 +49,7 @@ namespace FluidSim.Tests // ---- Crankshaft (external, passed to cylinder) ---- crankshaft = new Crankshaft(600); - crankshaft.Inertia = 0.1; + crankshaft.Inertia = 0.2; crankshaft.FrictionConstant = 2; crankshaft.FrictionViscous = 0.04;