Files
FluidSim/Scenarios/TwoStrokeScenario.cs

288 lines
12 KiB
C#
Raw 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 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("TwoStroke 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);
}
}
}