added two stroke scenario with vehicle
This commit is contained in:
@@ -13,12 +13,16 @@ namespace FluidSim.Tests
|
||||
protected const float AmbientTemperature = 300f;
|
||||
public float Throttle { get; set; }
|
||||
public float Load { get; set; }
|
||||
public float Clutch { get; set; } // 0 = engaged, 1 = fully disengaged (manual lever)
|
||||
public Font? Font { get; set; }
|
||||
|
||||
public abstract void Initialize(int sampleRate);
|
||||
public abstract float Process();
|
||||
public abstract void Draw(RenderWindow target);
|
||||
|
||||
public virtual void ShiftUp() { }
|
||||
public virtual void ShiftDown() { }
|
||||
|
||||
// ---- Dyno curve graph ----
|
||||
private const float RpmBinSize = 50f;
|
||||
private readonly List<(float powerKw, float torqueNm)> _dynoBins = new();
|
||||
@@ -259,7 +263,7 @@ namespace FluidSim.Tests
|
||||
target.Draw(border);
|
||||
}
|
||||
|
||||
protected void DrawCylinder(RenderWindow target, Cylinder cylinder,
|
||||
protected void DrawCylinder(RenderWindow target, EngineCylinder cylinder,
|
||||
float centerX, float topY, float width, float maxHeight)
|
||||
{
|
||||
float fraction = cylinder.PistonFraction;
|
||||
@@ -298,7 +302,8 @@ namespace FluidSim.Tests
|
||||
}
|
||||
|
||||
protected void DrawPipe(RenderWindow target, PipeSystem pipeSystem, int pipeIndex,
|
||||
float pipeCenterY, float pipeStartX, float pipeEndX)
|
||||
float pipeCenterY, float pipeStartX, float pipeEndX,
|
||||
float areaScale = 0f)
|
||||
{
|
||||
int start = pipeSystem.GetPipeStart(pipeIndex);
|
||||
int end = pipeSystem.GetPipeEnd(pipeIndex);
|
||||
@@ -307,20 +312,34 @@ namespace FluidSim.Tests
|
||||
|
||||
float pipeLen = pipeEndX - pipeStartX;
|
||||
float dx = pipeLen / (n - 1);
|
||||
float baseRadius = 25f;
|
||||
|
||||
var centers = new float[n];
|
||||
var radii = new float[n];
|
||||
var temps = new float[n];
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int cell = start + i;
|
||||
float p = pipeSystem.GetCellPressure(cell);
|
||||
float rho = pipeSystem.GetCellDensity(cell);
|
||||
temps[i] = p / MathF.Max(rho * 287f, 1e-12f);
|
||||
float dev = MathF.Tanh((p - AmbientPressure) / AmbientPressure * 0.5f);
|
||||
radii[i] = baseRadius * (1f + dev * 2f);
|
||||
if (radii[i] < 2f) radii[i] = 2f;
|
||||
|
||||
if (areaScale > 0f)
|
||||
{
|
||||
// Use actual cell area to determine visual radius
|
||||
float area = pipeSystem.GetCellArea(cell);
|
||||
radii[i] = MathF.Sqrt(area / MathF.PI) * areaScale;
|
||||
if (radii[i] < 1f) radii[i] = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Original pressure‑based radius
|
||||
float dev = MathF.Tanh((p - AmbientPressure) / AmbientPressure * 0.5f);
|
||||
float baseRadius = 25f; // default visual radius for constant‑area pipes
|
||||
radii[i] = baseRadius * (1f + dev * 2f);
|
||||
if (radii[i] < 2f) radii[i] = 2f;
|
||||
}
|
||||
|
||||
centers[i] = pipeStartX + i * dx;
|
||||
}
|
||||
|
||||
|
||||
293
Scenarios/TwoStrokeScenario.cs
Normal file
293
Scenarios/TwoStrokeScenario.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
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 TwoStrokeScenario : Scenario
|
||||
{
|
||||
private Crankshaft crankshaft;
|
||||
private TwoStrokeCylinder cylinder;
|
||||
|
||||
private PipeSystem pipeSystem;
|
||||
private BoundarySystem boundaries;
|
||||
private Solver solver;
|
||||
|
||||
private Volume0D intakePlenum;
|
||||
private Port plenumInlet, plenumOutlet;
|
||||
private Volume0D exhaustMuffler;
|
||||
private Port mufflerIn, mufflerOut;
|
||||
|
||||
private Vehicle vehicle;
|
||||
|
||||
private int throttleAreaIdx, plenumRunnerIdx, 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, exhaustHeaderArea;
|
||||
|
||||
// -- Override shift from Scenario base class --
|
||||
public override void ShiftUp() => vehicle.ShiftUp();
|
||||
public override void ShiftDown() => vehicle.ShiftDown();
|
||||
|
||||
public override void Initialize(int sampleRate)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
|
||||
// ---- Vehicle ----
|
||||
vehicle = new Vehicle();
|
||||
|
||||
// ---- Throttle (38 mm) ----
|
||||
_maxThrottleArea = (float)Units.AreaFromDiameter(38 * Units.mm);
|
||||
|
||||
// ---- Crankshaft ----
|
||||
crankshaft = new Crankshaft(2000);
|
||||
crankshaft.CycleLength = 2f * MathF.PI; // two‑stroke
|
||||
crankshaft.Inertia = 0.05f; // engine's own inertia (light)
|
||||
crankshaft.FrictionConstant = 2.5f;
|
||||
crankshaft.FrictionViscous = 0.0015f;
|
||||
|
||||
// ---- Cylinder (125cc) ----
|
||||
float bore = 0.054f, stroke = 0.0545f, conRod = 0.109f, compRatio = 12.5f;
|
||||
|
||||
// Symmetric durations (around BDC)
|
||||
float transferDuration = 130f; // 130°
|
||||
float exhaustDuration = 190f; // 190°
|
||||
|
||||
cylinder = new TwoStrokeCylinder(bore, stroke, conRod, compRatio,
|
||||
transferDuration, exhaustDuration,
|
||||
crankshaft)
|
||||
{
|
||||
IntakeValveDiameter = 0.038f,
|
||||
IntakeValveLift = 0.010f,
|
||||
ExhaustValveDiameter = 0.040f,
|
||||
ExhaustValveLift = 0.010f
|
||||
};
|
||||
|
||||
// ---- Pipe system (60 exhaust cells, simple diffuser) ----
|
||||
int intakeCells = 8;
|
||||
int runnerCells = 8;
|
||||
int exhaustCells = 60;
|
||||
int totalCells = intakeCells + runnerCells + exhaustCells;
|
||||
int[] pipeStart = { 0, intakeCells, intakeCells + runnerCells };
|
||||
int[] pipeEnd = { intakeCells, intakeCells + runnerCells, totalCells };
|
||||
|
||||
float[] area = new float[totalCells];
|
||||
float[] dx = new float[totalCells];
|
||||
|
||||
float intakeDia = 0.038f;
|
||||
float intakeLenBefore = 0.15f;
|
||||
float intakeLenRunner = 0.20f;
|
||||
intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia;
|
||||
|
||||
// Single‑stage diffuser – 840 mm total, easy to tune
|
||||
float headerDia = 0.042f, headerLen = 0.160f;
|
||||
float diffuserLen = 0.250f, diffuserEndDia = 0.070f; // belly
|
||||
float bellyLen = 0.240f;
|
||||
float convergentLen = 0.120f;
|
||||
float stingerDia = 0.026f, stingerLen = 0.070f;
|
||||
// total = 0.16 + 0.25 + 0.24 + 0.12 + 0.07 = 0.84 m
|
||||
|
||||
exhaustHeaderArea = MathF.PI * 0.25f * headerDia * headerDia;
|
||||
float bellyArea = MathF.PI * 0.25f * diffuserEndDia * diffuserEndDia;
|
||||
float stingerArea = MathF.PI * 0.25f * stingerDia * stingerDia;
|
||||
|
||||
float totalExhaustLen = headerLen + diffuserLen + bellyLen + convergentLen + stingerLen; // 840 mm
|
||||
int headerCells = (int)(exhaustCells * (headerLen / totalExhaustLen));
|
||||
int diffuserCells = (int)(exhaustCells * (diffuserLen / totalExhaustLen));
|
||||
int bellyCells = (int)(exhaustCells * (bellyLen / totalExhaustLen));
|
||||
int convergentCells = (int)(exhaustCells * (convergentLen / totalExhaustLen));
|
||||
int stingerCells = exhaustCells - headerCells - diffuserCells - bellyCells - convergentCells;
|
||||
|
||||
// Fill cells
|
||||
for (int i = 0; i < intakeCells; i++)
|
||||
{ area[i] = intakePipeArea; dx[i] = intakeLenBefore / intakeCells; }
|
||||
for (int i = intakeCells; i < intakeCells + runnerCells; i++)
|
||||
{ area[i] = intakePipeArea; dx[i] = intakeLenRunner / runnerCells; }
|
||||
|
||||
int exhStart = intakeCells + runnerCells;
|
||||
int idx = 0;
|
||||
for (int i = exhStart; i < totalCells; i++)
|
||||
{
|
||||
if (idx < headerCells)
|
||||
{ area[i] = exhaustHeaderArea; dx[i] = headerLen / headerCells; }
|
||||
else if (idx < headerCells + diffuserCells)
|
||||
{
|
||||
float t = (idx - headerCells) / (float)(diffuserCells - 1);
|
||||
float dia = headerDia + (diffuserEndDia - headerDia) * t;
|
||||
area[i] = MathF.PI * 0.25f * dia * dia;
|
||||
dx[i] = diffuserLen / diffuserCells;
|
||||
}
|
||||
else if (idx < headerCells + diffuserCells + bellyCells)
|
||||
{ area[i] = bellyArea; dx[i] = bellyLen / bellyCells; }
|
||||
else if (idx < headerCells + diffuserCells + bellyCells + convergentCells)
|
||||
{
|
||||
float t = (idx - headerCells - diffuserCells - bellyCells) / (float)(convergentCells - 1);
|
||||
float dia = diffuserEndDia + (stingerDia - diffuserEndDia) * t;
|
||||
area[i] = MathF.PI * 0.25f * dia * dia;
|
||||
dx[i] = convergentLen / convergentCells;
|
||||
}
|
||||
else
|
||||
{ area[i] = stingerArea; dx[i] = stingerLen / stingerCells; }
|
||||
idx++;
|
||||
}
|
||||
|
||||
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(0.5e-3f, 101325f, 300f);
|
||||
plenumInlet = intakePlenum.CreatePort();
|
||||
plenumOutlet = intakePlenum.CreatePort();
|
||||
|
||||
exhaustMuffler = new Volume0D(5e-4f, 101325f, 600f);
|
||||
mufflerIn = exhaustMuffler.CreatePort();
|
||||
mufflerOut = exhaustMuffler.CreatePort();
|
||||
|
||||
// ---- Boundary system ----
|
||||
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2);
|
||||
throttleAreaIdx = 0; plenumRunnerIdx = 1; intakeValveIdx = 2; exhaustValveIdx = 3;
|
||||
|
||||
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, intakePipeArea);
|
||||
intakeOpenIdx = 0;
|
||||
boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, stingerArea);
|
||||
exhaustOpenIdx = 1;
|
||||
|
||||
boundaries.AddOrifice(plenumInlet, 0, false, throttleAreaIdx, 0.7f);
|
||||
boundaries.AddOrifice(plenumOutlet, 1, true, plenumRunnerIdx, 1.0f);
|
||||
boundaries.AddOrifice(cylinder.IntakePort, 1, false, intakeValveIdx, 0.65f);
|
||||
boundaries.AddOrifice(cylinder.ExhaustPort,2, true, exhaustValveIdx, 0.68f);
|
||||
|
||||
orificeAreas = new float[4];
|
||||
orificeAreas[plenumRunnerIdx] = intakePipeArea;
|
||||
|
||||
// ---- Solver ----
|
||||
solver = new Solver { SubStepCount = 4, EnableProfiling = false }; // 4 sub‑steps for 60 cells
|
||||
solver.SetTimeStep(dt);
|
||||
solver.SetPipeSystem(pipeSystem);
|
||||
solver.SetBoundarySystem(boundaries);
|
||||
solver.AddComponent(cylinder);
|
||||
solver.AddComponent(intakePlenum);
|
||||
solver.AddComponent(exhaustMuffler);
|
||||
|
||||
// ---- Sound ----
|
||||
exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 10f };
|
||||
intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 10f };
|
||||
reverb = new OutdoorExhaustReverb(sampleRate);
|
||||
|
||||
stepCount = 0;
|
||||
Console.WriteLine("125cc Two‑Stroke with vehicle coupling ready.");
|
||||
}
|
||||
|
||||
public override float Process()
|
||||
{
|
||||
float engineRpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
|
||||
|
||||
vehicle.ClutchInput = Clutch;
|
||||
|
||||
var (clutchTorque, effectiveInertia) = vehicle.Update(engineRpm, crankshaft.Inertia, (float)dt);
|
||||
crankshaft.SetEffectiveInertia(effectiveInertia);
|
||||
crankshaft.SetLoadTorque(clutchTorque); // clutch torque now includes drag when locked
|
||||
|
||||
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 % 2000 == 0)
|
||||
{
|
||||
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
|
||||
Console.WriteLine($"Step {stepCount}, RPM={rpm:F0}, Gear={vehicle.CurrentGear}, Speed={vehicle.SpeedKmh:F0} km/h");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Intake pipe
|
||||
float x = openEndX;
|
||||
float w = 120f;
|
||||
DrawPipe(target, pipeSystem, 0, intakeY, x, x + w);
|
||||
|
||||
// Throttle
|
||||
float throttleX = x + w + 5f;
|
||||
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
|
||||
{
|
||||
FillColor = Color.Yellow,
|
||||
Position = new Vector2f(throttleX, intakeY - 15f)
|
||||
};
|
||||
target.Draw(throttleRect);
|
||||
|
||||
// Plenum
|
||||
float plenW = 40f, plenH = 60f;
|
||||
float plenX = throttleX + 10f;
|
||||
DrawVolume(target, intakePlenum, plenX + plenW / 2f, intakeY - plenH / 2f, plenW, plenH);
|
||||
|
||||
// Runner
|
||||
float runnerStartX = plenX + plenW + 5f;
|
||||
DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerStartX + 100f);
|
||||
|
||||
// Cylinder
|
||||
float cylCX = runnerStartX + 150f;
|
||||
float cylTopY = intakeY - 120f;
|
||||
DrawCylinder(target, cylinder, cylCX, cylTopY, 80f, 240f);
|
||||
|
||||
// Exhaust pipe
|
||||
float exhStartX = cylCX + 40f + 20f;
|
||||
DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, winW - 60f, areaScale: 1000f);
|
||||
|
||||
// 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);
|
||||
DrawLabel(target, $"Gear: {vehicle.CurrentGear}", new Vector2f(20, 140), Color.Cyan, 20);
|
||||
DrawLabel(target, $"Speed: {vehicle.SpeedKmh:F0} km/h", new Vector2f(20, 160), Color.Cyan, 20);
|
||||
|
||||
// Dyno curve
|
||||
float torqueNm = crankshaft.AverageTorque;
|
||||
UpdateDynoCurve(rpm, powerKw, torqueNm);
|
||||
DrawDynoCurve(target, winW - 410f, winH - 260f, 400f, 250f, rpm, powerKw);
|
||||
|
||||
string gearText = vehicle.CurrentGear == 0 ? "N" : vehicle.CurrentGear.ToString();
|
||||
DrawLabel(target, $"Gear: {gearText}", new Vector2f(20, 140), Color.Cyan, 20);
|
||||
DrawLabel(target, $"Speed: {vehicle.SpeedKmh:F0} km/h", new Vector2f(20, 160), Color.Cyan, 20);
|
||||
DrawLabel(target, vehicle.Engagement > 0.99f ? "Clutch Locked" : "Clutch Slipping", new Vector2f(20, 180), Color.Cyan, 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user