288 lines
12 KiB
C#
288 lines
12 KiB
C#
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 Crankcase crankcase;
|
||
|
||
private PipeSystem pipeSystem;
|
||
private BoundarySystem boundaries;
|
||
private Solver solver;
|
||
|
||
private Volume0D intakePlenum;
|
||
private Port plenumInlet, plenumOutlet;
|
||
|
||
private int throttleAreaIdx, reedInletIdx, reedOutletIdx,
|
||
transferInletIdx, transferOutletIdx, 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, reedPipeArea, transferPipeArea, exhaustHeaderArea;
|
||
private bool reedOpen;
|
||
|
||
public override void Initialize(int sampleRate)
|
||
{
|
||
dt = 1.0 / sampleRate;
|
||
|
||
maxThrottleArea = (float)Units.AreaFromDiameter(42 * Units.mm);
|
||
|
||
// ---- Crankshaft ----
|
||
crankshaft = new Crankshaft(3000);
|
||
crankshaft.CycleLength = 2f * MathF.PI;
|
||
crankshaft.Inertia = 0.01f;
|
||
crankshaft.FrictionConstant = 1.0f;
|
||
crankshaft.FrictionViscous = 0.002f;
|
||
|
||
// ---- Cylinder (125cc) ----
|
||
float bore = 0.054f, stroke = 0.0545f, conRod = 0.110f, compRatio = 7.2f;
|
||
float transferDur = 140f, exhaustDur = 195f;
|
||
cylinder = new TwoStrokeCylinder(bore, stroke, conRod, compRatio,
|
||
transferDur, exhaustDur, crankshaft)
|
||
{
|
||
// FIX: realistic transfer port diameter (was 40mm)
|
||
IntakeValveDiameter = 0.030f, // 30 mm
|
||
IntakeValveLift = 0.010f,
|
||
ExhaustValveDiameter = 0.040f,
|
||
ExhaustValveLift = 0.010f
|
||
};
|
||
|
||
// ---- Crankcase ----
|
||
float crankRadius = stroke * 0.5f;
|
||
float ccClearance = 150e-6f;
|
||
crankcase = new Crankcase(crankshaft, crankRadius, conRod, bore,
|
||
ccClearance, 101325f, 300f);
|
||
cylinder.SetCrankcase(crankcase);
|
||
|
||
// ---- Pipe system ----
|
||
int intakeCells = 8;
|
||
int reedCells = 4;
|
||
int transferCells = 8;
|
||
int exhaustCells = 60;
|
||
int totalCells = intakeCells + reedCells + transferCells + exhaustCells;
|
||
|
||
int[] pipeStart = {
|
||
0,
|
||
intakeCells,
|
||
intakeCells + reedCells,
|
||
intakeCells + reedCells + transferCells
|
||
};
|
||
int[] pipeEnd = {
|
||
intakeCells,
|
||
intakeCells + reedCells,
|
||
intakeCells + reedCells + transferCells,
|
||
totalCells
|
||
};
|
||
|
||
float[] area = new float[totalCells];
|
||
float[] dx = new float[totalCells];
|
||
|
||
float intakeDia = 0.042f, reedDia = 0.040f, transferDia = 0.040f;
|
||
intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia;
|
||
reedPipeArea = MathF.PI * 0.25f * reedDia * reedDia;
|
||
transferPipeArea = MathF.PI * 0.25f * transferDia * transferDia;
|
||
|
||
for (int i = 0; i < intakeCells; i++)
|
||
{ area[i] = intakePipeArea; dx[i] = 0.100f / intakeCells; }
|
||
|
||
for (int i = intakeCells; i < intakeCells + reedCells; i++)
|
||
{ area[i] = reedPipeArea; dx[i] = 0.030f / reedCells; }
|
||
|
||
for (int i = intakeCells + reedCells; i < intakeCells + reedCells + transferCells; i++)
|
||
{ area[i] = transferPipeArea; dx[i] = 0.200f / transferCells; }
|
||
|
||
float hdrD = 0.040f, hdrL = 0.130f;
|
||
float difEndD = 0.070f, difL = 0.250f;
|
||
float belL = 0.220f;
|
||
float convEndD = 0.028f, convL = 0.160f;
|
||
float stiL = 0.080f;
|
||
float totL = hdrL + difL + belL + convL + stiL;
|
||
exhaustHeaderArea = MathF.PI * 0.25f * hdrD * hdrD;
|
||
float bellyArea = MathF.PI * 0.25f * difEndD * difEndD;
|
||
float stingerArea = MathF.PI * 0.25f * convEndD * convEndD;
|
||
|
||
int exhStart = intakeCells + reedCells + transferCells;
|
||
int hdrC = (int)(exhaustCells * hdrL / totL);
|
||
int difC = (int)(exhaustCells * difL / totL);
|
||
int belC = (int)(exhaustCells * belL / totL);
|
||
int conC = (int)(exhaustCells * convL / totL);
|
||
int stiC = exhaustCells - hdrC - difC - belC - conC;
|
||
|
||
int idx = 0;
|
||
for (int i = exhStart; i < totalCells; i++)
|
||
{
|
||
if (idx < hdrC) { area[i] = exhaustHeaderArea; dx[i] = hdrL / hdrC; }
|
||
else if (idx < hdrC + difC)
|
||
{
|
||
float t = (idx - hdrC) / (float)(difC - 1);
|
||
float dia = hdrD + (difEndD - hdrD) * t;
|
||
area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = difL / difC;
|
||
}
|
||
else if (idx < hdrC + difC + belC) { area[i] = bellyArea; dx[i] = belL / belC; }
|
||
else if (idx < hdrC + difC + belC + conC)
|
||
{
|
||
float t = (idx - hdrC - difC - belC) / (float)(conC - 1);
|
||
float dia = difEndD + (convEndD - difEndD) * t;
|
||
area[i] = MathF.PI * 0.25f * dia * dia; dx[i] = convL / conC;
|
||
}
|
||
else { area[i] = stingerArea; dx[i] = stiL / stiC; }
|
||
idx++;
|
||
}
|
||
|
||
pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx,
|
||
1.225f, 0f, 101325f);
|
||
pipeSystem.DampingMultiplier = 0.8f;
|
||
pipeSystem.EnergyRelaxationRate = 0.4f;
|
||
|
||
// ---- Volumes ----
|
||
intakePlenum = new Volume0D(0.5e-3f, 101325f, 300f);
|
||
plenumInlet = intakePlenum.CreatePort();
|
||
plenumOutlet = intakePlenum.CreatePort();
|
||
|
||
// ---- Boundary system ----
|
||
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 6, maxOpenEnds: 2);
|
||
throttleAreaIdx = 0;
|
||
reedInletIdx = 1;
|
||
reedOutletIdx = 2;
|
||
transferInletIdx = 3;
|
||
transferOutletIdx = 4;
|
||
exhaustValveIdx = 5;
|
||
|
||
boundaries.AddOpenEnd(0, true, 101325f, intakePipeArea);
|
||
intakeOpenIdx = 0;
|
||
boundaries.AddOpenEnd(3, false, 101325f, stingerArea);
|
||
exhaustOpenIdx = 1;
|
||
|
||
boundaries.AddOrifice(plenumInlet, 0, false, throttleAreaIdx, 0.72f);
|
||
boundaries.AddOrifice(plenumOutlet, 1, true, reedInletIdx, 1.0f);
|
||
boundaries.AddOrifice(crankcase.IntakePort, 1, false, reedOutletIdx, 0.9f);
|
||
boundaries.AddOrifice(crankcase.TransferPort,2, true, transferInletIdx,1.0f);
|
||
boundaries.AddOrifice(cylinder.IntakePort, 2, false, transferOutletIdx,1.0f);
|
||
boundaries.AddOrifice(cylinder.ExhaustPort, 3, true, exhaustValveIdx, 0.7f);
|
||
|
||
orificeAreas = new float[6];
|
||
orificeAreas[reedInletIdx] = reedPipeArea;
|
||
orificeAreas[reedOutletIdx] = 0f;
|
||
orificeAreas[transferInletIdx] = transferPipeArea;
|
||
orificeAreas[transferOutletIdx] = 0f;
|
||
|
||
// ---- Solver ----
|
||
solver = new Solver { SubStepCount = 4 };
|
||
solver.SetTimeStep(dt);
|
||
solver.SetPipeSystem(pipeSystem);
|
||
solver.SetBoundarySystem(boundaries);
|
||
solver.AddComponent(cylinder);
|
||
solver.AddComponent(crankcase);
|
||
solver.AddComponent(intakePlenum);
|
||
|
||
// ---- Sound ----
|
||
exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 4.5f };
|
||
intakeSound = new SoundProcessor(sampleRate, 1f) { Gain = 4.5f };
|
||
reverb = new OutdoorExhaustReverb(sampleRate);
|
||
|
||
stepCount = 0;
|
||
Console.WriteLine("Two‑Stroke engine ready.");
|
||
}
|
||
|
||
public override float Process()
|
||
{
|
||
const float reedMargin = 200f;
|
||
if (crankcase.Pressure < intakePlenum.Pressure - reedMargin)
|
||
reedOpen = true;
|
||
else if (crankcase.Pressure > intakePlenum.Pressure + reedMargin)
|
||
reedOpen = false;
|
||
|
||
float throttledFraction = Throttle;
|
||
if (throttledFraction < 0.001f) throttledFraction = 0f;
|
||
throttledFraction = Math.Clamp(throttledFraction, 0f, 1f);
|
||
float throttledArea = maxThrottleArea * throttledFraction;
|
||
|
||
orificeAreas[throttleAreaIdx] = throttledArea;
|
||
orificeAreas[reedOutletIdx] = reedOpen ? reedPipeArea : 0f;
|
||
orificeAreas[transferOutletIdx] = cylinder.IntakeValveArea;
|
||
orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea;
|
||
boundaries.SetOrificeAreas(orificeAreas);
|
||
|
||
if (stepCount < 20000)
|
||
crankshaft.AddTorque(5.0f);
|
||
|
||
// FIX: update crankshaft BEFORE volumes, so crankcase and cylinder see the same new angle
|
||
crankshaft.Step((float)dt);
|
||
cylinder.PreStep((float)dt);
|
||
crankcase.PreStep((float)dt);
|
||
|
||
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} | CylP={cylinder.Pressure/1e5f:F2} bar | CCP={crankcase.Pressure/1e5f:F3} bar | Plenum={intakePlenum.Pressure/1e5f:F3} bar | Reed={reedOpen}");
|
||
}
|
||
|
||
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 startX = 40f;
|
||
float endX = winW - 80f;
|
||
|
||
DrawPipe(target, pipeSystem, 0, winH * 0.25f, startX, startX + 120f);
|
||
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
|
||
{
|
||
FillColor = Color.Yellow,
|
||
Position = new Vector2f(startX + 125f, winH * 0.25f - 15f)
|
||
};
|
||
target.Draw(throttleRect);
|
||
float plenX = startX + 140f;
|
||
DrawVolume(target, intakePlenum, plenX + 30f, winH * 0.25f - 25f, 60f, 50f);
|
||
|
||
float reedStartX = plenX + 70f;
|
||
DrawPipe(target, pipeSystem, 1, winH * 0.25f, reedStartX, reedStartX + 30f);
|
||
|
||
float transStartX = reedStartX + 40f;
|
||
DrawPipe(target, pipeSystem, 2, winH * 0.45f, transStartX, transStartX + 120f);
|
||
|
||
float cylCX = transStartX + 180f;
|
||
float cylTopY = winH * 0.45f - 90f;
|
||
DrawCylinder(target, cylinder, cylCX, cylTopY, 80f, 240f);
|
||
|
||
float exhStartX = cylCX + 60f;
|
||
DrawPipe(target, pipeSystem, 3, winH * 0.65f, exhStartX, endX, areaScale: 800f);
|
||
|
||
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);
|
||
|
||
float torqueNm = crankshaft.AverageTorque;
|
||
UpdateDynoCurve(rpm, powerKw, torqueNm);
|
||
DrawDynoCurve(target, winW - 410f, winH - 260f, 400f, 250f, rpm, powerKw);
|
||
}
|
||
}
|
||
} |