diff --git a/Components/Cylinder.cs b/Components/Cylinder.cs index 3f14d2f..db64dbd 100644 --- a/Components/Cylinder.cs +++ b/Components/Cylinder.cs @@ -100,24 +100,39 @@ namespace FluidSim.Components { float deg = thetaDeg % 720f; if (deg < 0f) deg += 720f; - float duration = closes - opens; + + float duration; + float effectiveOpen = opens; + float effectiveClose = closes; + + if (closes < opens) + { + // Wrap‑around case (e.g., exhaust: opens near 480°, closes near 30°) + effectiveClose += 720f; + } + duration = effectiveClose - effectiveOpen; if (duration <= 0f) return 0f; + // Map the angle into the [opens, opens+duration] window + float mapped = deg; + if (mapped < opens) mapped += 720f; + if (mapped < opens || mapped > effectiveClose) return 0f; + float rampDur = duration * 0.25f; float holdDur = duration - 2f * rampDur; - if (deg >= opens && deg < opens + rampDur) + if (mapped >= opens && mapped < opens + rampDur) { - float t = (deg - opens) / rampDur; + float t = (mapped - opens) / rampDur; return peakLift * t * t * (3f - 2f * t); } - else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur) + else if (mapped >= opens + rampDur && mapped < opens + rampDur + holdDur) { return peakLift; } - else if (deg >= opens + rampDur + holdDur && deg <= closes) + else if (mapped >= opens + rampDur + holdDur && mapped <= effectiveClose) { - float t = (deg - (opens + rampDur + holdDur)) / rampDur; + float t = (mapped - (opens + rampDur + holdDur)) / rampDur; return peakLift * (1f - t) * (1f - t) * (1f + 2f * t); } return 0f; @@ -194,6 +209,7 @@ namespace FluidSim.Components float totalMass = _airMass + _exhaustMass; _airMass = 0f; _exhaustMass = totalMass; } + fuelInjected = false; float dFraction = newFraction - burnFraction; if (dFraction > 0f) diff --git a/Core/BoundarySystem.cs b/Core/BoundarySystem.cs index 5264250..7daf2b0 100644 --- a/Core/BoundarySystem.cs +++ b/Core/BoundarySystem.cs @@ -25,7 +25,8 @@ namespace FluidSim.Core public float EffectiveLength; public float CurrentMdot; // kg/s, positive = volume → pipe - // --- Loss coefficient (linear resistance) --- + // --- Loss coefficient (linear resistance) – inertance only --- + // If 0 when UseInertance is true, a stable default is auto‑computed at runtime. public float LossCoefficient; // N·s/m⁵ or kg/(m⁴·s) } @@ -57,9 +58,10 @@ namespace FluidSim.Core public int OpenEndCount { get; private set; } // ---------- Add orifice (no inertance) ---------- + // Simple isentropic nozzle – no built‑in loss. For dissipation use pipe damping + // or the inertance model if you need a damped resonator. public void AddOrifice(Port volumePort, int pipeIndex, bool isLeftEnd, - int areaIndex, float dischargeCoeff = 1f, - float lossCoefficient = 0f) + int areaIndex, float dischargeCoeff = 1f) { _orifices[OrificeCount] = new OrificeDesc { @@ -71,22 +73,24 @@ namespace FluidSim.Core UseInertance = false, EffectiveLength = 0f, CurrentMdot = 0f, - LossCoefficient = lossCoefficient + LossCoefficient = 0f }; OrificeCount++; } // ---------- Add orifice with inertance ---------- + // effectiveLength – length of the inertial slug (m), typically the physical neck length. + // lossCoefficient – linear resistance (N·s/m⁵). If 0 (or omitted) an automatic stable + // value will be computed from the pipe's characteristic impedance. public void AddOrificeWithInertance(Port volumePort, int pipeIndex, bool isLeftEnd, int areaIndex, float dischargeCoeff, float effectiveLength, float lossCoefficient = 0f) { - // Reuse the base AddOrifice and then override fields - AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff, lossCoefficient); + AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff); ref var d = ref _orifices[OrificeCount - 1]; d.UseInertance = true; d.EffectiveLength = effectiveLength; - d.LossCoefficient = lossCoefficient; // store the linear resistance + d.LossCoefficient = lossCoefficient; } public void AddOpenEnd(int pipeIndex, bool isLeftEnd, @@ -146,7 +150,7 @@ namespace FluidSim.Core ? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex) : _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex); - // ---- Handle closed orifice (area ≈ 0) as a wall ---- + // ---- Handle closed orifice as a wall ---- if (area < 1e-12f || d.VolumePort == null) { var (rInt, uInt, pInt) = d.IsLeftEnd @@ -165,7 +169,7 @@ namespace FluidSim.Core continue; } - // ---- Preliminary isentropic solution (used for face pressure if inertance is on) ---- + // ---- Preliminary isentropic solution (for reference) ---- float mdotEst, rhoFaceEst, uFaceEst, pFaceEst; if (volP >= pipeP) { @@ -184,16 +188,26 @@ namespace FluidSim.Core if (d.UseInertance) { + // ---- Inertance ODE with (possibly automatic) linear loss ---- float rhoUp = d.CurrentMdot >= 0 ? volRho : pipeRho; float inertance = rhoUp * d.EffectiveLength / MathF.Max(area, 1e-12f); float dp = volP - pipeP; - float Rlin = d.LossCoefficient; - // Forward Euler + // If loss coefficient not provided, use a tiny fraction of the pipe's characteristic impedance + float Rlin = d.LossCoefficient; + if (Rlin <= 0f) + { + // Auto‑sized linear drag: 0.5 % of Z_char + float rhoRef = d.CurrentMdot >= 0 ? volRho : pipeRho; + float cRef = d.CurrentMdot >= 0 ? MathF.Sqrt(Gamma * Rgas * volT) : MathF.Sqrt(Gamma * Rgas * pipeT); + float Z_char = rhoRef * cRef / MathF.Max(area, 1e-12f); + Rlin = 0.005f * Z_char; + } + float dmdot_dt = (dp - Rlin * d.CurrentMdot) / MathF.Max(inertance, 1e-12f); float mdotNew = d.CurrentMdot + dmdot_dt * dt; - // ---------- Symmetric flow limiters (existing) ---------- + // Symmetric flow limiters if (d.VolumePort.Owner is Volume0D vol0) { float maxOut = vol0.Mass / dt; @@ -209,7 +223,7 @@ namespace FluidSim.Core float maxFromPipe = pipeCellMass / dt; if (mdotNew < -maxFromPipe) mdotNew = -maxFromPipe; - // ---------- Velocity clamp to Mach 0.9 ---------- + // Velocity clamp Mach 0.9 float rhoFacePrelim = mdotNew >= 0 ? volRho : pipeRho; float uFacePrelim = MathF.Abs(mdotNew) / MathF.Max(rhoFacePrelim * area, 1e-12f); float cUp = mdotNew >= 0 ? MathF.Sqrt(Gamma * Rgas * volT) : MathF.Sqrt(Gamma * Rgas * pipeT); @@ -220,20 +234,18 @@ namespace FluidSim.Core mdotNew = rhoFacePrelim * uFacePrelim * area * (mdotNew >= 0 ? 1f : -1f); } - // NaN safety if (float.IsNaN(mdotNew)) mdotNew = 0f; d.CurrentMdot = mdotNew; mdotFinal = mdotNew; - // Ghost state rhoFace = mdotFinal >= 0 ? volRho : pipeRho; pFace = pFaceEst; uFace = MathF.Abs(mdotFinal) / MathF.Max(rhoFace * area, 1e-12f); } else { - // ---- Standard quasi‑steady orifice ---- + // ---- Standard quasi‑steady orifice (purely isentropic) ---- mdotFinal = mdotEst; rhoFace = rhoFaceEst; uFace = uFaceEst; @@ -245,6 +257,17 @@ namespace FluidSim.Core float maxOut = vol0.Mass / dt; if (mdotFinal > maxOut) mdotFinal = maxOut; } + + // Safety velocity clamp (Mach 0.9) + float cLocal = mdotFinal >= 0 ? MathF.Sqrt(Gamma * Rgas * volT) : MathF.Sqrt(Gamma * Rgas * pipeT); + float maxULocal = 0.9f * cLocal; + float uCheck = MathF.Abs(mdotFinal) / MathF.Max(rhoFace * area, 1e-12f); + if (uCheck > maxULocal) + { + uFace = maxULocal; + mdotFinal = rhoFace * uFace * area * (mdotFinal >= 0 ? 1f : -1f); + } + d.CurrentMdot = mdotFinal; } @@ -271,18 +294,17 @@ namespace FluidSim.Core else _pipeSystem.SetGhostRight(d.PipeIndex, rhoFace, uFace, pFace, airFracGhost); - // ---- Update volume port (mass flow: positive into volume) ---- + // ---- Update volume port ---- if (d.VolumePort != null) { d.VolumePort.MassFlowRate = -mdotFinal; - // Set enthalpy of the stream entering the volume - if (-mdotFinal >= 0) // mass flowing into the volume (out of pipe) + if (-mdotFinal >= 0) // mass flowing into the volume { float pipeH = GammaOverGm1 * pipeP / MathF.Max(pipeRho, 1e-12f); d.VolumePort.SpecificEnthalpy = pipeH; } - else // mass flowing out of the volume (into pipe) + else // mass flowing out of the volume { d.VolumePort.SpecificEnthalpy = volH; } diff --git a/Program.cs b/Program.cs index 784dd3b..d9469ae 100644 --- a/Program.cs +++ b/Program.cs @@ -50,7 +50,7 @@ public class Program { var window = CreateWindow(); LoadFont(); - _scenario = new HelmholtzScenario(); + _scenario = new SingleCylScenario(); _scenario.Initialize(SampleRate); _lastThrottleUpdateTime = 0.0f; diff --git a/Scenarios/HelmholtzScenario.cs b/Scenarios/HelmholtzScenario.cs index 1c0231c..095212d 100644 --- a/Scenarios/HelmholtzScenario.cs +++ b/Scenarios/HelmholtzScenario.cs @@ -38,7 +38,7 @@ namespace FluidSim.Tests int neckCells = 20; // --- Volume (cavity) --- - float initialPressure = 1.2f * 101325f; // slight overpressure + float initialPressure = 1.1f * 101325f; // slight overpressure float initialTemperature = 300f; cavity = new Volume0D(cavityVolume, initialPressure, initialTemperature); cavityPort = cavity.CreatePort(); @@ -56,24 +56,16 @@ namespace FluidSim.Tests float rho0 = 101325f / (287f * 300f); pipeSystem = new PipeSystem(neckCells, pipeStart, pipeEnd, areas, dxs, rho0, 0f, 101325f); - pipeSystem.DampingMultiplier = 500f; // --- Boundary system --- boundaries = new BoundarySystem(pipeSystem, maxOrifices: 1, maxOpenEnds: 1); - float ComputeResistance(float decayTimeSeconds, float rho, float L_eff, float A) - { - // R = 2 * rho * L_eff / (A * decayTimeSeconds) - return 2f * rho * L_eff / (A * MathF.Max(decayTimeSeconds, 1e-6f)); - } - - // Use steady orifice – the pipe already provides the inertia + // Standard orifice with built‑in minor loss (K = 0.5) – no inertance needed boundaries.AddOrificeWithInertance( cavityPort, pipeIndex: 0, isLeftEnd: true, areaIndex: cavityOrificeIdx, dischargeCoeff: 0.9f, - effectiveLength: neckLength, // physical length (or L_eff) - lossCoefficient: 7000 // start with this, adjust for decay time + effectiveLength: neckLength // physical neck length ); // Open end at right side of pipe @@ -83,8 +75,7 @@ namespace FluidSim.Tests boundaries.SetOrificeAreas(orificeAreas); // --- Solver --- - // Slightly higher sub‑step count to ensure stability of the resonant oscillation - solver = new Solver { SubStepCount = 6, EnableProfiling = false }; + solver = new Solver { SubStepCount = 8, EnableProfiling = false }; solver.SetTimeStep(dt); solver.SetPipeSystem(pipeSystem); solver.SetBoundarySystem(boundaries); @@ -99,43 +90,12 @@ namespace FluidSim.Tests public override float Process() { - stepCount++; - if (stepCount <= 8192) return 0f; // let buffer pre‑fill - solver.Step(); + stepCount++; float flow = boundaries.GetOpenEndMassFlow(openEndIdx); float sample = soundProcessor.Process(flow); - if (stepCount % 10000 == 0) - { - float cavityP = cavity.Pressure; - float cavityT = cavity.Temperature; - float cavityRho = cavity.Density; - float cCavity = MathF.Sqrt(1.4f * cavityP / MathF.Max(cavityRho, 1e-12f)); - - // Temperature in the middle of the neck - int midCell = 10; - float pMid = pipeSystem.GetCellPressure(midCell); - float rhoMid = pipeSystem.GetCellDensity(midCell); - float tMid = pMid / MathF.Max(rhoMid * 287f, 1e-12f); - - // Neck effective length (physical + end correction) - float neckLen = 0.05f; // physical - float neckDia = 0.02f; - float neckArea = MathF.PI * 0.25f * neckDia * neckDia; - float endCorr = 0.85f * neckDia; // unflanged end - float L_eff = neckLen + endCorr; - - // Theoretical Helmholtz frequency from current cavity sound speed - float fHelmholtz = cCavity / (2f * MathF.PI) * - MathF.Sqrt(neckArea / (cavity.Volume * L_eff)); - - Console.WriteLine( - $"Step {stepCount}: cav P={cavityP / 1e5f:F4} bar, T={cavityT:F1} K, " + - $"pipeMid T={tMid:F1} K, est f={fHelmholtz:F1} Hz"); - } - return sample; } diff --git a/Scenarios/SingleCylScenario.cs b/Scenarios/SingleCylScenario.cs index f149fac..29b769c 100644 --- a/Scenarios/SingleCylScenario.cs +++ b/Scenarios/SingleCylScenario.cs @@ -9,165 +9,208 @@ 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; - private Volume0D exhaustCollector; - private Port colIn, colOut; - private int throttleAreaIdx, plenumRunnerAreaIdx, intakeValveIdx, exhaustValveIdx; - private float[] orificeAreas; + // 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; - public float MaxThrottleArea = 1e-4f; // 1 cm² - // pipe area for open end calculations - private float pipeArea; + // ---------- 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); + // ---------- Crankshaft ---------- + crankshaft = new Crankshaft(600); // start at ~600 RPM crankshaft.Inertia = 0.2f; - crankshaft.FrictionConstant = 2f; + crankshaft.FrictionConstant = 2.0f; crankshaft.FrictionViscous = 0.04f; - // ---- Cylinder ---- - float bore = 0.056f, stroke = 0.057f, conRod = 0.110f, compRatio = 9.2f; - float ivo = 350f, ivc = 580f, evo = 120f, evc = 370f; - cylinder = new Cylinder(bore, stroke, conRod, compRatio, - ivo, ivc, evo, evc, crankshaft) + // ---------- Cylinder ---------- + cylinder = new Cylinder(Bore, Stroke, ConRod, CompressionRatio, + IVO, IVC, EVO, EVC, crankshaft) { - IntakeValveDiameter = 0.03f, - IntakeValveLift = 0.005f, - ExhaustValveDiameter = 0.028f, - ExhaustValveLift = 0.005f + 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 = 10 + 10 + 50; - int[] pipeStart = { 0, 10, 20 }; - int[] pipeEnd = { 10, 20, 70 }; - float[] area = new float[totalCells]; - float[] dx = new float[totalCells]; - float pipeDiameter = 0.02f; // 2 cm - pipeArea = MathF.PI * 0.25f * pipeDiameter * pipeDiameter; - float areaVal = pipeArea; - float intakeLenBefore = 0.2f, intakeLenRunner = 0.2f, exhaustLen = 0.5f; + // ---------- 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++) { - area[i] = areaVal; - if (i < 10) dx[i] = intakeLenBefore / 10f; - else if (i < 20) dx[i] = intakeLenRunner / 10f; - else dx[i] = exhaustLen / 50f; + areas[i] = PipeArea; + if (i < CellsBefore) + dxs[i] = dxBefore; + else if (i < CellsBefore + CellsRunner) + dxs[i] = dxRunner; + else + dxs[i] = dxExh; } - pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx, - 1.225f, 0f, 101325f); + float rho0 = 101325f / (287f * 300f); + pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, areas, dxs, + rho0, 0f, 101325f); pipeSystem.DampingMultiplier = 0.5f; - pipeSystem.EnergyRelaxationRate = 0f; + pipeSystem.EnergyRelaxationRate = 0f; // adiabatic pipes pipeSystem.AmbientPressure = 101325f; - // ---- Volumes ---- - intakePlenum = new Volume0D(5e-6f, 101325f, 300f); // 5 mL - plenumInlet = intakePlenum.CreatePort(); + // ---------- Volumes ---------- + intakePlenum = new Volume0D(PlenumVolume, 101325f, 300f); + plenumInlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort(); - exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // 10 mL (unused but present) - colIn = exhaustCollector.CreatePort(); - colOut = exhaustCollector.CreatePort(); - // ---- Boundary system ---- + // ---------- Boundary system ---------- boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2); - throttleAreaIdx = 0; - plenumRunnerAreaIdx = 1; - intakeValveIdx = 2; - exhaustValveIdx = 3; + throttleAreaIdx = 0; + plenumRunnerIdx = 1; + intakeValveIdx = 2; + exhaustValveIdx = 3; - // Intake open end (pipe0 left) - boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, pipeArea); + // Open ends + boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, PipeArea); intakeOpenIdx = 0; - - // Throttle orifice (plenum inlet to pipe0 right) - boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false, throttleAreaIdx, 0.2f); - - // Plenum to runner (plenum outlet to pipe1 left) - boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true, plenumRunnerAreaIdx, 1f); - - // Intake valve (cylinder intake to pipe1 right) - boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false, intakeValveIdx, 1f); - - // Exhaust valve (cylinder exhaust to pipe2 left) - boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true, exhaustValveIdx, 1f); - - // Exhaust open end (pipe2 right) - boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, pipeArea); + boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, PipeArea); exhaustOpenIdx = 1; - orificeAreas = new float[4]; - orificeAreas[plenumRunnerAreaIdx] = areaVal; // fixed plenum->runner area + // 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); - // ---- Solver ---- - solver = new Solver { SubStepCount = 4, EnableProfiling = false }; + 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); - solver.AddComponent(exhaustCollector); - // ---- Sound ---- - exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 1f }; - intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 1f }; + // ---------- 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("TestScenario ready."); + 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 orifice areas - float throttledArea = MaxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1f); + // ---- Update variable areas ---- + float throttledArea = MaxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1.0f); orificeAreas[throttleAreaIdx] = throttledArea; - orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; + orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; boundaries.SetOrificeAreas(orificeAreas); + // ---- Fluids step ---- solver.Step(); stepCount++; - // Retrieve open‑end mass flows for sound synthesis + // ---- Sound ---- float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx); - float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx); + float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx); float exhaustDry = exhaustSound.Process(exhaustFlow); - float intakeDry = intakeSound.Process(intakeFlow); + float intakeDry = intakeSound.Process(intakeFlow); - if (stepCount % 1000 == 0) + 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"); - Console.WriteLine($"intake flow: {intakeFlow:F12}, exhaust flow: {exhaustFlow:F16}"); + Console.WriteLine($"Step {stepCount}, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar, " + + $"Throttle={Throttle * 100:F0}%"); } - return reverb.Process(intakeDry); + return reverb.Process(exhaustDry + intakeDry); } public override void Draw(RenderWindow target) @@ -177,44 +220,53 @@ namespace FluidSim.Tests float intakeY = winH / 2f - 40f; float exhaustY = winH / 2f + 80f; - float openEndX = 40f; + float leftX = 40f; - // Intake pipe before throttle (pipe 0) - float pipe1StartX = openEndX; - float pipe1EndX = pipe1StartX + 120f; - DrawPipe(target, pipeSystem, 0, intakeY, pipe1StartX, pipe1EndX); + // 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 throttleX = pipe1EndX + 5f; - var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) + float thrX = p0EndX + 5f; + var thr = new RectangleShape(new Vector2f(8f, 30f)) { FillColor = Color.Yellow, - Position = new Vector2f(throttleX, intakeY - 15f) + Position = new Vector2f(thrX, intakeY - 15f) }; - target.Draw(throttleRect); + target.Draw(thr); - // Plenum - float plenW = 60f, plenH = 80f; - float plenLeftX = throttleX + 10f; + // 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); - // Runner pipe (pipe 1) - float runnerStartX = plenLeftX + plenW + 5f; - float runnerEndX = runnerStartX + 100f; - DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerEndX); + // Pipe 1 – runner + float rStartX = plenLeftX + plenW + 10f; + float rEndX = rStartX + 100f; + DrawPipe(target, pipeSystem, 1, intakeY, rStartX, rEndX); // Cylinder - float cylCX = runnerEndX + 50f; + float cylCX = rEndX + 50f; float cylTopY = intakeY - 120f; float cylW = 80f, cylMaxH = 240f; DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); - // Exhaust pipe (pipe 2) + // 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); } } } \ No newline at end of file