diff --git a/Components/Crankshaft.cs b/Components/Crankshaft.cs index cd4090e..0af1361 100644 --- a/Components/Crankshaft.cs +++ b/Components/Crankshaft.cs @@ -8,21 +8,46 @@ namespace FluidSim.Components public float CrankAngle; // rad, 0 … 4π public float PreviousAngle; - public float Inertia = 0.2f; + public float Inertia = 0.2f; // kg·m² public float FrictionConstant; // N·m public float FrictionViscous; // N·m per rad/s + public float LastNetTorque { get; private set; } + public float AveragePower { get; private set; } // smoothed, watts + public float AverageTorque { get; private set; } // smoothed, Nm + private float externalTorque; + private float _loadTorque; // external brake torque (Nm) + + // Power averaging buffer + private readonly float[] _powerBuffer; + private int _powerBufIdx; + private int _powerBufCount; + private float _powerBufSum; + + // Torque averaging buffer (same size as power buffer) + private readonly float[] _torqueBuffer; + private int _torqueBufIdx; + private int _torqueBufCount; + private float _torqueBufSum; public Crankshaft(float initialRPM = 400f) { AngularVelocity = initialRPM * 2f * MathF.PI / 60f; CrankAngle = 0f; PreviousAngle = 0f; + + _powerBuffer = new float[16384]; + _torqueBuffer = new float[16384]; } public void AddTorque(float torque) => externalTorque += torque; + public void SetLoadTorque(float torque) + { + _loadTorque = Math.Max(torque, 0f); + } + public void Step(float dt) { if (float.IsNaN(AngularVelocity) || float.IsInfinity(AngularVelocity)) @@ -32,10 +57,17 @@ namespace FluidSim.Components PreviousAngle = CrankAngle; + // Internal friction torque float friction = FrictionConstant * MathF.Sign(AngularVelocity) + FrictionViscous * AngularVelocity; + + // Net torque from gas pressure minus friction (used for power/torque display) float netTorque = externalTorque - friction; - float alpha = netTorque / Inertia; + LastNetTorque = netTorque; + + // Total torque after subtracting external load (brake) + float totalNetTorque = netTorque - _loadTorque; + float alpha = totalNetTorque / Inertia; AngularVelocity += alpha * dt; if (AngularVelocity < 0f) AngularVelocity = 0f; @@ -46,6 +78,35 @@ namespace FluidSim.Components else if (CrankAngle < 0f) CrankAngle += 4f * MathF.PI; + // ---- Power averaging ---- + float instantPower = netTorque * AngularVelocity; + if (_powerBufCount == _powerBuffer.Length) + { + _powerBufSum -= _powerBuffer[_powerBufIdx]; + } + else + { + _powerBufCount++; + } + _powerBuffer[_powerBufIdx] = instantPower; + _powerBufSum += instantPower; + _powerBufIdx = (_powerBufIdx + 1) % _powerBuffer.Length; + AveragePower = _powerBufSum / _powerBufCount; + + // ---- Torque averaging ---- + if (_torqueBufCount == _torqueBuffer.Length) + { + _torqueBufSum -= _torqueBuffer[_torqueBufIdx]; + } + else + { + _torqueBufCount++; + } + _torqueBuffer[_torqueBufIdx] = netTorque; + _torqueBufSum += netTorque; + _torqueBufIdx = (_torqueBufIdx + 1) % _torqueBuffer.Length; + AverageTorque = _torqueBufSum / _torqueBufCount; + externalTorque = 0f; } } diff --git a/Components/Cylinder.cs b/Components/Cylinder.cs index 748e9c4..8b18342 100644 --- a/Components/Cylinder.cs +++ b/Components/Cylinder.cs @@ -107,13 +107,11 @@ namespace FluidSim.Components 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; @@ -153,6 +151,10 @@ namespace FluidSim.Components public void PreStep(float dt) { + // Speed‑dependent spark advance (simple linear) + float rpm = Crankshaft.AngularVelocity * 60f / (2f * MathF.PI); + SparkAdvance = Math.Clamp(10f + rpm * 0.002f, 5f, 40f); // 10° at idle, ~30° at 10k rpm + float prevVolume = cylinderVolume; float crankAngleRad = Crankshaft.CrankAngle + PhaseOffset; cylinderVolume = ComputeVolume(crankAngleRad); @@ -170,7 +172,7 @@ namespace FluidSim.Components float prevDeg = (Crankshaft.PreviousAngle + PhaseOffset) * 180f / MathF.PI % 720f; float currDeg = crankAngleRad * 180f / MathF.PI % 720f; - // Intake closing + // Intake closing – triggers fuel injection if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC) { trappedAirMass = _airMass; diff --git a/Program.cs b/Program.cs index d9469ae..c2668e2 100644 --- a/Program.cs +++ b/Program.cs @@ -33,17 +33,21 @@ public class Program // Audio & simulation private static SimulationRingBuffer _simRingBuffer = null!; private static SoundEngine _soundEngine = null!; - private static Scenario _scenario = null!; // cast to access ThrottleArea + private static Scenario _scenario = null!; private static Font? _overlayFont; private static Text? _overlayText; // Throttle control - private static float _throttleTarget = 1.0f; // 0‑1, set by arrow keys - private static float _throttleCurrent = 0.0f; // actual current fraction (lerped) - private const float ThrottleLerpRate = 10.0f; // times per second (speed of movement) + private static float _throttleTarget = 1.0f; + private static float _throttleCurrent = 0.0f; + private const float ThrottleLerpRate = 10.0f; private static bool _wKeyHeld = false; private static float _lastThrottleUpdateTime; + // Load + private static float _loadTarget = 0.0f; // 0‑1 + private static float _loadCurrent = 0.0f; + private const int TargetMaxFill = (int)(SampleRate * 0.2); public static void Main() @@ -51,6 +55,7 @@ public class Program var window = CreateWindow(); LoadFont(); _scenario = new SingleCylScenario(); + _scenario.Font = _overlayFont; _scenario.Initialize(SampleRate); _lastThrottleUpdateTime = 0.0f; @@ -76,14 +81,12 @@ public class Program (1.0 - Math.Exp(-8.0 * (now - lastDrawTime))); _soundEngine.Speed = _currentDisplaySpeed; - // ---- Throttle update ---- + // ---- Throttle & Load update (shared dt) ---- float dtThrottle = (float)now - _lastThrottleUpdateTime; _lastThrottleUpdateTime = (float)now; float throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0f; - - // Snap to zero instantly when target is zero (key released) - if (throttleDesiredFraction == 0.0) + if (throttleDesiredFraction == 0.0f) { _throttleCurrent = 0.0f; } @@ -93,8 +96,13 @@ public class Program _throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing; } + float loadSmoothing = 1.0f - MathF.Exp(-ThrottleLerpRate * dtThrottle); + _loadCurrent += (_loadTarget - _loadCurrent) * loadSmoothing; + _scenario.Load = _loadCurrent; + _scenario.Throttle = _throttleCurrent; + // ---- Drawing ---- if (now - lastDrawTime >= 1.0 / DrawFrequency) { @@ -103,7 +111,7 @@ public class Program string toggleHint = _isRealTime ? "[Space] slow mo" : "[Space] real time"; _overlayText.DisplayedString = $"{toggleHint} Speed: {_currentDisplaySpeed:F3}x RT: {(_currentDisplaySpeed * 100.0):F1}% Sim load: {_loadTracker.LoadPercent:F0}%\n" + - $"Throttle: {_throttleCurrent * 100:F0}% Target: {_throttleTarget * 100:F0}% [W] {(_wKeyHeld ? "BLIP" : "---")}"; + $"Load: {_loadCurrent*100:F0}% [←][→] Throttle: {_throttleCurrent * 100:F0}% Target: {_throttleTarget * 100:F0}% [W] {(_wKeyHeld ? "BLIP" : "---")}"; } window.Clear(Color.Black); @@ -205,6 +213,14 @@ public class Program case Keyboard.Key.Down: _throttleTarget = MathF.Max(0.0f, _throttleTarget - 0.05f); break; + + case Keyboard.Key.Left: + _loadTarget = MathF.Max(0.0f, _loadTarget - 0.05f); + break; + + case Keyboard.Key.Right: + _loadTarget = MathF.Min(1.0f, _loadTarget + 0.05f); + break; } } diff --git a/Scenarios/Scenario.cs b/Scenarios/Scenario.cs index c8a2cde..7c170de 100644 --- a/Scenarios/Scenario.cs +++ b/Scenarios/Scenario.cs @@ -2,6 +2,8 @@ using SFML.System; using FluidSim.Core; using FluidSim.Components; +using System; +using System.Collections.Generic; namespace FluidSim.Tests { @@ -10,11 +12,200 @@ namespace FluidSim.Tests protected const float AmbientPressure = 101325f; protected const float AmbientTemperature = 300f; public float Throttle { get; set; } + public float Load { get; set; } + public Font? Font { get; set; } public abstract void Initialize(int sampleRate); public abstract float Process(); public abstract void Draw(RenderWindow target); + // ---- Dyno curve graph ---- + private const float RpmBinSize = 50f; + private readonly List<(float powerKw, float torqueNm)> _dynoBins = new(); + private int _lastDynoBin = -1; + + public void ResetDynoCurve() + { + _dynoBins.Clear(); + _lastDynoBin = -1; + } + + protected void UpdateDynoCurve(float rpm, float powerKw, float torqueNm) + { + if (rpm <= 0) return; + int bin = (int)(rpm / RpmBinSize); + + while (_dynoBins.Count <= bin) + _dynoBins.Add((0f, 0f)); + + if (_lastDynoBin >= 0 && bin > _lastDynoBin + 1) + { + float lastPower = _dynoBins[_lastDynoBin].powerKw > 0 ? _dynoBins[_lastDynoBin].powerKw : 0f; + float lastTorque = _dynoBins[_lastDynoBin].torqueNm > 0 ? _dynoBins[_lastDynoBin].torqueNm : 0f; + for (int b = _lastDynoBin + 1; b < bin; b++) + { + float t = (b - _lastDynoBin) / (float)(bin - _lastDynoBin); + float interpPower = lastPower + (powerKw - lastPower) * t; + float interpTorque = lastTorque + (torqueNm - lastTorque) * t; + if (interpPower > _dynoBins[b].powerKw || _dynoBins[b].powerKw <= 0) + _dynoBins[b] = (interpPower, _dynoBins[b].torqueNm); + if (interpTorque > _dynoBins[b].torqueNm || _dynoBins[b].torqueNm <= 0) + _dynoBins[b] = (_dynoBins[b].powerKw, interpTorque); + } + } + + var current = _dynoBins[bin]; + if (powerKw > current.powerKw || current.powerKw <= 0) + current.powerKw = powerKw; + if (torqueNm > current.torqueNm || current.torqueNm <= 0) + current.torqueNm = torqueNm; + _dynoBins[bin] = current; + + _lastDynoBin = bin; + } + + protected void DrawDynoCurve(RenderWindow target, + float graphX, float graphY, float graphWidth, float graphHeight, + float currentRpm, float currentPowerKw) + { + if (_dynoBins.Count == 0) return; + + float maxPowerKw = 0.01f, maxTorqueNm = 0.01f, maxRpm = 1000f; + for (int b = 0; b < _dynoBins.Count; b++) + { + var bin = _dynoBins[b]; + if (bin.powerKw > 0 || bin.torqueNm > 0) + { + float rpmBin = b * RpmBinSize + RpmBinSize / 2f; + if (bin.powerKw > maxPowerKw) maxPowerKw = bin.powerKw; + if (bin.torqueNm > maxTorqueNm) maxTorqueNm = bin.torqueNm; + if (rpmBin > maxRpm) maxRpm = rpmBin; + } + } + maxPowerKw *= 1.1f; + maxTorqueNm *= 1.1f; + maxRpm = MathF.Max(maxRpm * 1.05f, 1000f); + + var bg = new RectangleShape(new Vector2f(graphWidth, graphHeight)) + { + FillColor = new Color(20, 20, 20, 200), + Position = new Vector2f(graphX, graphY) + }; + target.Draw(bg); + + const float leftMargin = 50f, rightMargin = 50f, topMargin = 20f, bottomMargin = 35f; + float plotX = graphX + leftMargin; + float plotY = graphY + topMargin; + float plotW = graphWidth - leftMargin - rightMargin; + float plotH = graphHeight - topMargin - bottomMargin; + + float xMin = 0f, xMax = maxRpm; + float yLeftMin = 0f, yLeftMax = maxPowerKw; + float yRightMin = 0f, yRightMax = maxTorqueNm; + + var powerColor = new Color(0xFF, 0x1B, 0x1B); + var torqueColor = new Color(0x09, 0x09, 0xFF); + var gridColor = new Color(50, 50, 50); + + for (int i = 0; i <= 9; i++) + { + float t = i / 9f; + float x = plotX + t * plotW; + var vLine = new VertexArray(PrimitiveType.Lines, 2); + vLine[0] = new Vertex(new Vector2f(x, plotY), gridColor); + vLine[1] = new Vertex(new Vector2f(x, plotY + plotH), gridColor); + target.Draw(vLine); + } + for (int i = 0; i <= 5; i++) + { + float t = i / 5f; + float y = plotY + (1 - t) * plotH; + var hLine = new VertexArray(PrimitiveType.Lines, 2); + hLine[0] = new Vertex(new Vector2f(plotX, y), gridColor); + hLine[1] = new Vertex(new Vector2f(plotX + plotW, y), gridColor); + target.Draw(hLine); + } + + DrawLabel(target, "RPM", new Vector2f(graphX + graphWidth / 2 - 12, graphY + graphHeight - 15), Color.White, 12); + DrawLabel(target, "kW", new Vector2f(graphX + 5, graphY + 2), Color.White, 11); + DrawLabel(target, "Nm", new Vector2f(graphX + graphWidth - 25, graphY + 2), Color.White, 11); + + for (int i = 0; i <= 5; i++) + { + float leftValue = yLeftMin + (yLeftMax - yLeftMin) * i / 5f; + float rightValue = yRightMin + (yRightMax - yRightMin) * i / 5f; + float y = plotY + (1 - i / 5f) * plotH; + DrawLabel(target, $"{leftValue:F1}", new Vector2f(graphX + 2, y - 6), Color.White, 9); + DrawLabel(target, $"{rightValue:F1}", new Vector2f(graphX + graphWidth - 40, y - 6), Color.White, 9); + } + + for (int i = 0; i <= 9; i++) + { + float value = xMin + (xMax - xMin) * i / 9f; + float x = plotX + i / 9f * plotW; + DrawLabel(target, $"{value / 1000f:F1}k", new Vector2f(x - 15, graphY + graphHeight - bottomMargin + 5), Color.White, 9); + } + + var powerLine = new VertexArray(PrimitiveType.LineStrip); + bool firstPower = true; + for (int b = 0; b < _dynoBins.Count; b++) + { + float rpmBin = b * RpmBinSize + RpmBinSize / 2f; + if (rpmBin > xMax) break; + var bin = _dynoBins[b]; + if (bin.powerKw > 0) + { + float sx = plotX + (rpmBin - xMin) / (xMax - xMin) * plotW; + float sy = plotY + (1 - (bin.powerKw - yLeftMin) / (yLeftMax - yLeftMin)) * plotH; + if (firstPower) { powerLine.Clear(); firstPower = false; } + powerLine.Append(new Vertex(new Vector2f(sx, sy), powerColor)); + } + else if (!firstPower) + { + target.Draw(powerLine); + powerLine.Clear(); + firstPower = true; + } + } + if (!firstPower) target.Draw(powerLine); + + var torqueLine = new VertexArray(PrimitiveType.LineStrip); + bool firstTorque = true; + for (int b = 0; b < _dynoBins.Count; b++) + { + float rpmBin = b * RpmBinSize + RpmBinSize / 2f; + if (rpmBin > xMax) break; + var bin = _dynoBins[b]; + if (bin.torqueNm > 0) + { + float sx = plotX + (rpmBin - xMin) / (xMax - xMin) * plotW; + float sy = plotY + (1 - (bin.torqueNm - yRightMin) / (yRightMax - yRightMin)) * plotH; + if (firstTorque) { torqueLine.Clear(); firstTorque = false; } + torqueLine.Append(new Vertex(new Vector2f(sx, sy), torqueColor)); + } + else if (!firstTorque) + { + target.Draw(torqueLine); + torqueLine.Clear(); + firstTorque = true; + } + } + if (!firstTorque) target.Draw(torqueLine); + + if (currentRpm > 0 && currentRpm <= xMax && currentPowerKw > 0) + { + float sx = plotX + (currentRpm - xMin) / (xMax - xMin) * plotW; + float sy = plotY + (1 - (currentPowerKw - yLeftMin) / (yLeftMax - yLeftMin)) * plotH; + var dot = new CircleShape(2.5f) + { + FillColor = Color.White, + Position = new Vector2f(sx - 2.5f, sy - 2.5f) + }; + target.Draw(dot); + } + } + + // ---- Drawing helpers ---- protected Color PressureColor(float pressurePa) { float bar = pressurePa / 1e5f; @@ -157,5 +348,18 @@ namespace FluidSim.Tests } target.Draw(va); } + + protected void DrawLabel(RenderWindow target, string text, Vector2f position, Color fillColor, uint characterSize = 14) + { + if (Font == null) return; + var txt = new Text(Font) + { + DisplayedString = text, + Position = position, + FillColor = fillColor, + CharacterSize = characterSize + }; + target.Draw(txt); + } } } \ No newline at end of file diff --git a/Scenarios/SingleCylScenario.cs b/Scenarios/SingleCylScenario.cs index 7046639..91d4eac 100644 --- a/Scenarios/SingleCylScenario.cs +++ b/Scenarios/SingleCylScenario.cs @@ -32,53 +32,72 @@ namespace FluidSim.Tests private double dt; private int stepCount; - // Use a private field for the maximum throttle area, avoiding any base‑class conflicts private float _maxThrottleArea; - - // pipe area for open end calculations - private float pipeArea; + private float intakePipeArea, exhaustPipeArea; + private const float MaxBrakeTorque = 30.0f; // Nm at full load public override void Initialize(int sampleRate) { dt = 1.0 / sampleRate; - // Maximum throttle area – independent of base class - _maxThrottleArea = (float)Units.AreaFromDiameter(3 * Units.cm); // 1 cm² + // Throttle body diameter 44mm (typical for 250cc MX) + _maxThrottleArea = (float)Units.AreaFromDiameter(44 * Units.mm); // ---- Crankshaft ---- crankshaft = new Crankshaft(2000); - crankshaft.Inertia = 0.01f; - crankshaft.FrictionConstant = 2f; - crankshaft.FrictionViscous = 0.0f; + crankshaft.Inertia = 0.02f; // kg·m² (crank + flywheel) + crankshaft.FrictionConstant = 3.0f; // Nm – bearings, rings, seals + crankshaft.FrictionViscous = 0.002f; // Nm/(rad/s) – oil windage + + // ---- Cylinder (CRF250R) ---- + float bore = 0.078f; // 78 mm + float stroke = 0.0522f; // 52.2 mm → 249.4 cc + float conRod = 0.1044f; // 2× stroke + float compRatio = 13.5f; // typical + + // Valve events (high‑performance MX cam) + float ivo = 340f, ivc = 600f; // intake opens 20° BTDC (overlap), closes 60° ABDC + float evo = 120f, evc = 380f; // exhaust opens 60° BBDC, closes 20° ATDC - // ---- Cylinder ---- - float bore = 0.056f, stroke = 0.057f, conRod = 0.110f, compRatio = 11f; - float ivo = 350f, ivc = 580f, evo = 120f, evc = 370f; cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { - IntakeValveDiameter = 0.03f, - IntakeValveLift = 0.005f, - ExhaustValveDiameter = 0.028f, - ExhaustValveLift = 0.005f + IntakeValveDiameter = 0.036f, // 36 mm + IntakeValveLift = 0.0095f, // 9.5 mm + ExhaustValveDiameter = 0.030f, // 30 mm + ExhaustValveLift = 0.0085f // 8.5 mm }; // ---- Pipe system ---- int[] pipeStart = { 0, 10, 20 }; int[] pipeEnd = { 10, 20, 70 }; - int totalCells = pipeEnd[^1]; // automatically 70, stays in sync + int totalCells = pipeEnd[^1]; 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.4f; + float[] dx = new float[totalCells]; + + float intakeDia = 0.040f; // 40 mm intake runner + float exhaustDia = 0.038f; // 38 mm exhaust primary + intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia; + exhaustPipeArea = MathF.PI * 0.25f * exhaustDia * exhaustDia; + + float intakeLenBefore = 0.15f; // throttle body to plenum + float intakeLenRunner = 0.25f; // plenum to valve + float exhaustLen = 0.50f; // exhaust length + 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; + if (i < 10) + { + area[i] = intakePipeArea; dx[i] = intakeLenBefore / 10f; + } + else if (i < 20) + { + area[i] = intakePipeArea; dx[i] = intakeLenRunner / 10f; + } + else + { + area[i] = exhaustPipeArea; dx[i] = exhaustLen / 50f; + } } pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx, @@ -88,10 +107,10 @@ namespace FluidSim.Tests pipeSystem.AmbientPressure = 101325f; // ---- Volumes ---- - intakePlenum = new Volume0D(100e-6f, 101325f, 300f); // 100 mL + intakePlenum = new Volume0D(1.0e-3f, 101325f, 300f); // 1 litre airbox plenumInlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort(); - exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // 10 mL (unused but present) + exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // unused colIn = exhaustCollector.CreatePort(); colOut = exhaustCollector.CreatePort(); @@ -103,28 +122,20 @@ namespace FluidSim.Tests intakeValveIdx = 2; exhaustValveIdx = 3; - // Intake open end (pipe0 left) - boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, pipeArea); + // Open ends (pipe area = pipe cross‑section) + boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, intakePipeArea); 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, exhaustPipeArea); exhaustOpenIdx = 1; + // Orifices + boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false, throttleAreaIdx, 0.7f); // throttle + boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true, plenumRunnerAreaIdx, 1.0f); // plenum→runner + boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false, intakeValveIdx, 1.0f); // intake valve + boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true, exhaustValveIdx, 1.0f); // exhaust valve + orificeAreas = new float[4]; - orificeAreas[plenumRunnerAreaIdx] = areaVal; // fixed plenum->runner area + orificeAreas[plenumRunnerAreaIdx] = intakePipeArea; // runner cross‑section (fixed) // ---- Solver ---- solver = new Solver { SubStepCount = 4, EnableProfiling = false }; @@ -136,22 +147,26 @@ namespace FluidSim.Tests solver.AddComponent(exhaustCollector); // ---- Sound ---- - exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 20f }; - intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 20f }; + exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 10f }; + intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 10f }; reverb = new OutdoorExhaustReverb(sampleRate); stepCount = 0; - Console.WriteLine("TestScenario ready."); + Console.WriteLine("CRF250R engine ready."); } public override float Process() - { + { + // Manual brake torque (0..30 Nm) + float loadTorque = Load * MaxBrakeTorque; + crankshaft.SetLoadTorque(loadTorque); + crankshaft.Step((float)dt); cylinder.PreStep((float)dt); - // Update variable orifice areas – use the private _maxThrottleArea - float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1f); + float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.001f, 1f); orificeAreas[throttleAreaIdx] = throttledArea; + orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; boundaries.SetOrificeAreas(orificeAreas); @@ -159,41 +174,36 @@ namespace FluidSim.Tests solver.Step(); stepCount++; - // Retrieve open‑end mass flows for sound synthesis 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) { float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); - float crankDeg = crankshaft.CrankAngle; // degrees (0–720) - Console.WriteLine($"Step {stepCount}, CA={crankDeg:F1} deg, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar"); + float crankDeg = (crankshaft.CrankAngle + cylinder.PhaseOffset) * 180f / MathF.PI % 720f; + Console.WriteLine($"Step {stepCount}, CA={crankDeg:F1}°, RPM={rpm:F0}, CylP={cylinder.Pressure/1e5f:F2} bar"); Console.WriteLine($" intake flow: {intakeFlow:F6}, exhaust flow: {exhaustFlow:F6}"); - // Pipe 0 (intake before throttle) var (r0L, u0L, p0L) = pipeSystem.GetInteriorStateLeft(0); var (r0R, u0R, p0R) = pipeSystem.GetInteriorStateRight(0); Console.WriteLine($" Pipe0 L: rho={r0L:F4} u={u0L:F3} p={p0L/1e5:F3}bar | R: rho={r0R:F4} u={u0R:F3} p={p0R/1e5:F3}bar"); - // Pipe 1 (runner) var (r1L, u1L, p1L) = pipeSystem.GetInteriorStateLeft(1); var (r1R, u1R, p1R) = pipeSystem.GetInteriorStateRight(1); Console.WriteLine($" Pipe1 L: rho={r1L:F4} u={u1L:F3} p={p1L/1e5:F3}bar | R: rho={r1R:F4} u={u1R:F3} p={p1R/1e5:F3}bar"); - // Pipe 2 (exhaust) var (r2L, u2L, p2L) = pipeSystem.GetInteriorStateLeft(2); var (r2R, u2R, p2R) = pipeSystem.GetInteriorStateRight(2); Console.WriteLine($" Pipe2 L: rho={r2L:F4} u={u2L:F3} p={p2L/1e5:F3}bar | R: rho={r2R:F4} u={u2R:F3} p={p2R/1e5:F3}bar"); - // Plenum and cylinder mass Console.WriteLine($" Plenum P={intakePlenum.Pressure/1e5:F3}bar, mass={intakePlenum.Mass:E4} kg"); Console.WriteLine($" Cyl mass={cylinder.Mass:E4} kg"); } - return reverb.Process(intakeDry + exhaustDry); + return reverb.Process((intakeDry + exhaustDry) * 0.5f); } public override void Draw(RenderWindow target) @@ -205,12 +215,10 @@ namespace FluidSim.Tests float exhaustY = winH / 2f + 80f; float openEndX = 40f; - // Intake pipe before throttle (pipe 0) float pipe1StartX = openEndX; float pipe1EndX = pipe1StartX + 120f; DrawPipe(target, pipeSystem, 0, intakeY, pipe1StartX, pipe1EndX); - // Throttle symbol float throttleX = pipe1EndX + 5f; var throttleRect = new RectangleShape(new Vector2f(8f, 30f)) { @@ -219,28 +227,40 @@ namespace FluidSim.Tests }; target.Draw(throttleRect); - // 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); - // Runner pipe (pipe 1) float runnerStartX = plenLeftX + plenW + 5f; float runnerEndX = runnerStartX + 100f; DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerEndX); - // Cylinder float cylCX = runnerEndX + 50f; float cylTopY = intakeY - 120f; float cylW = 80f, cylMaxH = 240f; DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); - // Exhaust pipe (pipe 2) float exhStartX = cylCX + cylW / 2f + 20f; float exhEndX = winW - 60f; DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, exhEndX); + + // --- RPM & Power labels --- + float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); + float powerKw = crankshaft.AveragePower * 1e-3f; + DrawLabel(target, $"RPM: {rpm:F0}", new Vector2f(20, 90), Color.White, 24); + DrawLabel(target, $"Power: {powerKw:F2} kW", new Vector2f(20, 115), Color.White, 24); + + // --- Dyno curve --- + float torqueNm = crankshaft.AverageTorque; + UpdateDynoCurve(rpm, powerKw, torqueNm); + + float graphX = winW - 410f; + float graphY = winH - 260f; + float graphW = 400f; + float graphH = 250f; + DrawDynoCurve(target, graphX, graphY, graphW, graphH, rpm, powerKw); } } } \ No newline at end of file