crank case based two stroke testing

This commit is contained in:
max
2026-06-10 00:13:14 +02:00
parent 56e9c2867a
commit 16498f8041
4 changed files with 406 additions and 308 deletions

View File

@@ -12,6 +12,7 @@ namespace FluidSim.Tests
{
private Crankshaft crankshaft;
private TwoStrokeCylinder cylinder;
private Crankcase crankcase;
private PipeSystem pipeSystem;
private BoundarySystem boundaries;
@@ -19,12 +20,9 @@ namespace FluidSim.Tests
private Volume0D intakePlenum;
private Port plenumInlet, plenumOutlet;
private Volume0D exhaustMuffler;
private Port mufflerIn, mufflerOut;
private Vehicle vehicle;
private int throttleAreaIdx, plenumRunnerIdx, intakeValveIdx, exhaustValveIdx;
private int throttleAreaIdx, reedInletIdx, reedOutletIdx,
transferInletIdx, transferOutletIdx, exhaustValveIdx;
private float[] orificeAreas;
private int intakeOpenIdx, exhaustOpenIdx;
@@ -34,233 +32,199 @@ namespace FluidSim.Tests
private double dt;
private int stepCount;
private float _maxThrottleArea;
private float intakePipeArea, exhaustHeaderArea;
public override void ShiftUp() => vehicle.ShiftUp();
public override void ShiftDown() => vehicle.ShiftDown();
private float maxThrottleArea;
private float intakePipeArea, reedPipeArea, transferPipeArea, exhaustHeaderArea;
private bool reedOpen;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// ── Vehicle ──────────────────────────────────────────────────────────
vehicle = new Vehicle();
maxThrottleArea = (float)Units.AreaFromDiameter(42 * Units.mm);
// ── Throttle body: 42 mm wider to reduce high-RPM intake restriction ──
_maxThrottleArea = (float)Units.AreaFromDiameter(42 * Units.mm);
// ── Crankshaft ───────────────────────────────────────────────────────
// Lighter flywheel for quicker revving; friction tuned to ~0.5 kW loss at idle
crankshaft = new Crankshaft(2000);
crankshaft.CycleLength = 2f * MathF.PI; // two-stroke: fire every rev
crankshaft.Inertia = 0.06f; // lighter flywheel
crankshaft.FrictionConstant = 0.4f; // ~0.4 Nm constant drag
crankshaft.FrictionViscous = 0.0004f; // ~2.5 Nm at 10 000 RPM
// ── Cylinder: 125 cc, motocross-style two-stroke ─────────────────────
// Bore × stroke = 54 × 54.5 mm → 124.9 cc
float bore = 0.054f;
float stroke = 0.0545f;
float conRod = 0.110f; // ~2× stroke
float compRatio = 7.2f; // geometric CR; effective CR after port closure is ~12:1
// Port timings: exhaust 195°, transfer 155° competitive MX 125
float transferDuration = 155f;
float exhaustDuration = 195f;
// ---- 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,
transferDuration, exhaustDuration,
crankshaft)
transferDur, exhaustDur, crankshaft)
{
IntakeValveDiameter = 0.042f, // matched to intake pipe
IntakeValveLift = 0.015f,
// FIX: realistic transfer port diameter (was 40mm)
IntakeValveDiameter = 0.030f, // 30 mm
IntakeValveLift = 0.010f,
ExhaustValveDiameter = 0.040f,
ExhaustValveLift = 0.013f
ExhaustValveLift = 0.010f
};
// ── Pipe geometry ────────────────────────────────────────────────────
//
// Layout (all lengths in mm):
// Intake path: airbox stub 100 mm | runner 180 mm
// Exhaust path: expansion chamber tuned to ~9 000 RPM power peak
// header 170 mm Ø 40 mm
// diffuser 280 mm Ø 40 → 72 mm
// belly 200 mm Ø 72 mm
// convergent 130 mm Ø 72 → 28 mm
// stinger 70 mm Ø 28 mm
// total 850 mm
//
// Cell sizing: ~14 mm/cell.
// CFL: c_sound ≈ 550 m/s, dx=0.014 m → dt_max ≈ 25 µs
// at 44100 Hz dt = 22.7 µs → SubStepCount=4 keeps CFL safely ≤ 1
// ---- Crankcase ----
float crankRadius = stroke * 0.5f;
float ccClearance = 150e-6f;
crankcase = new Crankcase(crankshaft, crankRadius, conRod, bore,
ccClearance, 101325f, 300f);
cylinder.SetCrankcase(crankcase);
// --- Cell counts ---
int intakeCells = 7; // 100 mm stub → ~14 mm/cell
int runnerCells = 13; // 180 mm runner → ~14 mm/cell
int exhaustCells = 60; // 850 mm total → ~14 mm/cell
// ---- Pipe system ----
int intakeCells = 8;
int reedCells = 4;
int transferCells = 8;
int exhaustCells = 60;
int totalCells = intakeCells + reedCells + transferCells + exhaustCells;
int totalCells = intakeCells + runnerCells + exhaustCells;
int[] pipeStart = { 0, intakeCells, intakeCells + runnerCells };
int[] pipeEnd = { intakeCells, intakeCells + runnerCells, totalCells };
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];
// --- Intake ---
float intakeDia = 0.042f; // matches throttle body
float intakeStubLen = 0.100f;
float intakeRunnerLen= 0.160f; // shorter runner → less pumping loss
intakePipeArea = MathF.PI * 0.25f * intakeDia * intakeDia;
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] = intakeStubLen / intakeCells; }
{ area[i] = intakePipeArea; dx[i] = 0.100f / intakeCells; }
for (int i = intakeCells; i < intakeCells + runnerCells; i++)
{ area[i] = intakePipeArea; dx[i] = intakeRunnerLen / runnerCells; }
for (int i = intakeCells; i < intakeCells + reedCells; i++)
{ area[i] = reedPipeArea; dx[i] = 0.030f / reedCells; }
// Expansion chamber tuned for ~8 500 RPM power peak.
// Return-pulse travel distance = 0.5 × c_avg × (60 / RPM_target)
// c_avg ≈ 480 m/s → distance = 0.5 × 480 × (60/8500) ≈ 1.69 m round-trip
// → one-way pipe length ≈ 0.84 m (matches total below)
float headerDia = 0.040f; float headerLen = 0.130f; // shorter header → earlier pulse
float diffEndDia = 0.070f; float diffuserLen = 0.250f; // slightly narrower belly
float bellyDia = 0.070f; float bellyLen = 0.220f;
float convEndDia = 0.028f; float convergentLen= 0.160f; // longer convergent → stronger return pulse
float stingerDia = 0.028f; float stingerLen = 0.080f;
// total = 0.13+0.25+0.22+0.16+0.08 = 0.84 m
for (int i = intakeCells + reedCells; i < intakeCells + reedCells + transferCells; i++)
{ area[i] = transferPipeArea; dx[i] = 0.200f / transferCells; }
exhaustHeaderArea = MathF.PI * 0.25f * headerDia * headerDia;
float bellyArea = MathF.PI * 0.25f * bellyDia * bellyDia;
float stingerArea = MathF.PI * 0.25f * stingerDia * stingerDia;
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;
// Distribute cells proportionally by section length
int headerCells = Math.Max(1, (int)MathF.Round(exhaustCells * headerLen / 0.84f));
int diffuserCells = Math.Max(1, (int)MathF.Round(exhaustCells * diffuserLen / 0.84f));
int bellyCells = Math.Max(1, (int)MathF.Round(exhaustCells * bellyLen / 0.84f));
int convergentCells = Math.Max(1, (int)MathF.Round(exhaustCells * convergentLen/ 0.84f));
int stingerCells = exhaustCells - headerCells - diffuserCells
- bellyCells - convergentCells;
if (stingerCells < 1) stingerCells = 1;
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 exhBase = intakeCells + runnerCells;
int idx = 0;
for (int i = exhBase; i < totalCells; i++, idx++)
for (int i = exhStart; i < totalCells; i++)
{
if (idx < headerCells)
if (idx < hdrC) { area[i] = exhaustHeaderArea; dx[i] = hdrL / hdrC; }
else if (idx < hdrC + difC)
{
area[i] = exhaustHeaderArea;
dx[i] = headerLen / headerCells;
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 < headerCells + diffuserCells)
else if (idx < hdrC + difC + belC) { area[i] = bellyArea; dx[i] = belL / belC; }
else if (idx < hdrC + difC + belC + conC)
{
float t = (idx - headerCells) / (float)(diffuserCells - 1);
// Smooth cosine taper instead of linear for better wave reflection
float ct = 0.5f * (1f - MathF.Cos(MathF.PI * t));
float dia = headerDia + (diffEndDia - headerDia) * ct;
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);
// Steeper cosine convergent for a sharper return pulse
float ct = 0.5f * (1f - MathF.Cos(MathF.PI * t));
float dia = bellyDia + (convEndDia - bellyDia) * ct;
area[i] = MathF.PI * 0.25f * dia * dia;
dx[i] = convergentLen / convergentCells;
}
else
{
area[i] = stingerArea;
dx[i] = stingerLen / stingerCells;
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; // slightly less damping → stronger pulses
pipeSystem.DampingMultiplier = 0.8f;
pipeSystem.EnergyRelaxationRate = 0.4f;
pipeSystem.AmbientPressure = 101325f;
// ── 0-D Volumes ──────────────────────────────────────────────────────
// Intake plenum: acts as a small airbox resonator (8 cc)
intakePlenum = new Volume0D(8e-3f, 101325f, 300f);
// ---- Volumes ----
intakePlenum = new Volume0D(0.5e-3f, 101325f, 300f);
plenumInlet = intakePlenum.CreatePort();
plenumOutlet = intakePlenum.CreatePort();
// Exhaust silencer volume: 600 cc is realistic for a small-bore muffler
exhaustMuffler = new Volume0D(600e-6f, 101325f, 650f);
mufflerIn = exhaustMuffler.CreatePort();
mufflerOut = exhaustMuffler.CreatePort();
// ---- Boundary system ----
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 6, maxOpenEnds: 2);
throttleAreaIdx = 0;
reedInletIdx = 1;
reedOutletIdx = 2;
transferInletIdx = 3;
transferOutletIdx = 4;
exhaustValveIdx = 5;
// ── Boundary system ───────────────────────────────────────────────────
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2);
throttleAreaIdx = 0;
plenumRunnerIdx = 1;
intakeValveIdx = 2;
exhaustValveIdx = 3;
// Open ends: atmosphere at both extremes
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, intakePipeArea);
intakeOpenIdx = 0;
boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, stingerArea);
boundaries.AddOpenEnd(0, true, 101325f, intakePipeArea);
intakeOpenIdx = 0;
boundaries.AddOpenEnd(3, false, 101325f, stingerArea);
exhaustOpenIdx = 1;
// Orifices: throttle → plenum → runner → cylinder → exhaust pipe
boundaries.AddOrifice(plenumInlet, 0, false, throttleAreaIdx, 0.72f);
boundaries.AddOrifice(plenumOutlet, 1, true, plenumRunnerIdx, 1.00f);
boundaries.AddOrifice(cylinder.IntakePort, 1, false, intakeValveIdx, 0.68f);
boundaries.AddOrifice(cylinder.ExhaustPort, 2, true, exhaustValveIdx, 0.70f);
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[4];
orificeAreas[plenumRunnerIdx] = intakePipeArea; // runner always fully open
orificeAreas = new float[6];
orificeAreas[reedInletIdx] = reedPipeArea;
orificeAreas[reedOutletIdx] = 0f;
orificeAreas[transferInletIdx] = transferPipeArea;
orificeAreas[transferOutletIdx] = 0f;
// ── Solver ────────────────────────────────────────────────────────────
// SubStepCount = 4 keeps CFL ≤ 1 for 5 mm cells at 44 100 Hz
solver = new Solver { SubStepCount = 4, EnableProfiling = false };
// ---- Solver ----
solver = new Solver { SubStepCount = 4 };
solver.SetTimeStep(dt);
solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
solver.AddComponent(cylinder);
solver.AddComponent(crankcase);
solver.AddComponent(intakePlenum);
solver.AddComponent(exhaustMuffler);
// ── Sound ─────────────────────────────────────────────────────────────
// ---- 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("125cc Two-Stroke expansion chamber tuned for ~8 500 RPM power peak");
Console.WriteLine($" Exhaust cells: {exhaustCells} | header {headerCells} diffuser {diffuserCells}" +
$" belly {bellyCells} convergent {convergentCells} stinger {stingerCells}");
Console.WriteLine("TwoStroke engine ready.");
}
public override float Process()
{
float engineRpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
const float reedMargin = 200f;
if (crankcase.Pressure < intakePlenum.Pressure - reedMargin)
reedOpen = true;
else if (crankcase.Pressure > intakePlenum.Pressure + reedMargin)
reedOpen = false;
vehicle.ClutchInput = Clutch;
float throttledFraction = Throttle;
if (throttledFraction < 0.001f) throttledFraction = 0f;
throttledFraction = Math.Clamp(throttledFraction, 0f, 1f);
float throttledArea = maxThrottleArea * throttledFraction;
var (clutchTorque, effectiveInertia) = vehicle.Update(engineRpm, crankshaft.Inertia, (float)dt);
crankshaft.SetEffectiveInertia(effectiveInertia);
crankshaft.SetLoadTorque(clutchTorque);
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);
float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.001f, 1f);
orificeAreas[throttleAreaIdx] = throttledArea;
orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea;
orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea;
boundaries.SetOrificeAreas(orificeAreas);
crankcase.PreStep((float)dt);
solver.Step();
stepCount++;
@@ -273,76 +237,50 @@ namespace FluidSim.Tests
if (stepCount % 2000 == 0)
{
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
float powerKw = crankshaft.AveragePower * 1e-3f;
float torqueNm = crankshaft.AverageTorque;
Console.WriteLine($"Step {stepCount,7} | RPM={rpm,6:F0} | Power={powerKw,5:F2} kW" +
$" | Torque={torqueNm,5:F1} Nm | Gear={vehicle.CurrentGear}" +
$" | Speed={vehicle.SpeedKmh,4:F0} km/h");
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);
}
// ── Drawing ───────────────────────────────────────────────────────────────
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 startX = 40f;
float endX = winW - 80f;
// Intake stub
float x = openEndX;
float w = 120f;
DrawPipe(target, pipeSystem, 0, intakeY, x, x + w);
// Throttle body
float throttleX = x + w + 5f;
DrawPipe(target, pipeSystem, 0, winH * 0.25f, startX, startX + 120f);
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleX, intakeY - 15f)
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);
// Plenum
float plenW = 40f, plenH = 60f;
float plenX = throttleX + 10f;
DrawVolume(target, intakePlenum, plenX + plenW / 2f, intakeY - plenH / 2f, plenW, plenH);
float reedStartX = plenX + 70f;
DrawPipe(target, pipeSystem, 1, winH * 0.25f, reedStartX, reedStartX + 30f);
// Runner
float runnerStartX = plenX + plenW + 5f;
DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerStartX + 100f);
float transStartX = reedStartX + 40f;
DrawPipe(target, pipeSystem, 2, winH * 0.45f, transStartX, transStartX + 120f);
// Cylinder
float cylCX = runnerStartX + 150f;
float cylTopY = intakeY - 120f;
float cylCX = transStartX + 180f;
float cylTopY = winH * 0.45f - 90f;
DrawCylinder(target, cylinder, cylCX, cylTopY, 80f, 240f);
// Exhaust pipe (expansion chamber)
float exhStartX = cylCX + 40f + 20f;
DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, winW - 60f, areaScale: 800f);
float exhStartX = cylCX + 60f;
DrawPipe(target, pipeSystem, 3, winH * 0.65f, exhStartX, endX, areaScale: 800f);
// HUD labels
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
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;
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, $"Torque: {torqueNm:F1} Nm",new Vector2f(20, 140), Color.White, 20);
string gearText = vehicle.CurrentGear == 0 ? "N" : vehicle.CurrentGear.ToString();
DrawLabel(target, $"Gear: {gearText}", new Vector2f(20, 162), Color.Cyan, 20);
DrawLabel(target, $"Speed: {vehicle.SpeedKmh:F0} km/h",
new Vector2f(20, 184), Color.Cyan, 20);
DrawLabel(target, vehicle.Engagement > 0.99f ? "Clutch: Locked" : "Clutch: Slipping",
new Vector2f(20, 204), Color.Cyan, 14);
// Dyno curve
UpdateDynoCurve(rpm, powerKw, torqueNm);
DrawDynoCurve(target, winW - 410f, winH - 260f, 400f, 250f, rpm, powerKw);
}