Files
FluidSim/Scenarios/SingleCylScenario.cs
2026-06-09 20:20:56 +02:00

266 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Interfaces;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
using System;
namespace FluidSim.Tests
{
public class SingleCylScenario : Scenario
{
private Crankshaft crankshaft;
private Cylinder cylinder;
private PipeSystem pipeSystem;
private BoundarySystem boundaries;
private Solver solver;
private Volume0D intakePlenum;
private Port plenumInlet, plenumOutlet;
private Volume0D exhaustCollector;
private Port colIn, colOut;
private int throttleAreaIdx, plenumRunnerAreaIdx, intakeValveIdx, exhaustValveIdx;
private float[] orificeAreas;
private int intakeOpenIdx, exhaustOpenIdx;
private SoundProcessor exhaustSound, intakeSound;
private OutdoorExhaustReverb reverb;
private double dt;
private int stepCount;
private float _maxThrottleArea;
private float intakePipeArea, exhaustPipeArea;
private const float MaxBrakeTorque = 30.0f; // Nm at full load
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// Throttle body diameter 44mm (typical for 250cc MX)
_maxThrottleArea = (float)Units.AreaFromDiameter(44 * Units.mm);
// ---- Crankshaft ----
crankshaft = new Crankshaft(2000);
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 (highperformance 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 = new Cylinder(bore, stroke, conRod, compRatio,
ivo, ivc, evo, evc, crankshaft)
{
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];
float[] area = new float[totalCells];
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++)
{
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,
1.225f, 0f, 101325f);
pipeSystem.DampingMultiplier = 1.0f;
pipeSystem.EnergyRelaxationRate = 0.5f;
pipeSystem.AmbientPressure = 101325f;
// ---- Volumes ----
intakePlenum = new Volume0D(1.0e-3f, 101325f, 300f); // 1 litre airbox
plenumInlet = intakePlenum.CreatePort();
plenumOutlet = intakePlenum.CreatePort();
exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // unused
colIn = exhaustCollector.CreatePort();
colOut = exhaustCollector.CreatePort();
// ---- Boundary system ----
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2);
throttleAreaIdx = 0;
plenumRunnerAreaIdx = 1;
intakeValveIdx = 2;
exhaustValveIdx = 3;
// Open ends (pipe area = pipe crosssection)
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, intakePipeArea);
intakeOpenIdx = 0;
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] = intakePipeArea; // runner crosssection (fixed)
// ---- Solver ----
solver = new Solver { SubStepCount = 4, 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 = 10f };
intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 10f };
reverb = new OutdoorExhaustReverb(sampleRate);
stepCount = 0;
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);
float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.001f, 1f);
orificeAreas[throttleAreaIdx] = throttledArea;
orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea;
orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea;
boundaries.SetOrificeAreas(orificeAreas);
solver.Step();
stepCount++;
float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx);
float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx);
float exhaustDry = exhaustSound.Process(exhaustFlow);
float intakeDry = intakeSound.Process(intakeFlow);
if (stepCount % 1000 == 0)
{
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
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}");
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");
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");
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");
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) * 0.5f);
}
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 openEndX = 40f;
float pipe1StartX = openEndX;
float pipe1EndX = pipe1StartX + 120f;
DrawPipe(target, pipeSystem, 0, intakeY, pipe1StartX, pipe1EndX);
float throttleX = pipe1EndX + 5f;
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleX, intakeY - 15f)
};
target.Draw(throttleRect);
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);
float runnerStartX = plenLeftX + plenW + 5f;
float runnerEndX = runnerStartX + 100f;
DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerEndX);
float cylCX = runnerEndX + 50f;
float cylTopY = intakeY - 120f;
float cylW = 80f, cylMaxH = 240f;
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
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);
}
}
}