engine almost working, backup before adding gas types.
This commit is contained in:
@@ -3,239 +3,218 @@ using SFML.Graphics;
|
||||
using SFML.System;
|
||||
using FluidSim.Components;
|
||||
using FluidSim.Core;
|
||||
using FluidSim.Utils;
|
||||
|
||||
namespace FluidSim.Tests
|
||||
{
|
||||
public class TestScenario : Scenario
|
||||
{
|
||||
// Simulation core
|
||||
private Solver solver;
|
||||
private double dt;
|
||||
// Engine
|
||||
private Cylinder cylinder;
|
||||
|
||||
// Engine components
|
||||
private Volume0D cylinder;
|
||||
// Intake side
|
||||
private Pipe1D intakePipeBeforeThrottle; // pipe from ambient to plenum
|
||||
private Volume0D intakePlenum; // plenum (100 mL)
|
||||
private Pipe1D intakeRunner; // pipe from plenum to cylinder
|
||||
|
||||
// Exhaust side
|
||||
private Pipe1D exhaustPipe;
|
||||
private OrificeLink exhaustPort;
|
||||
private OpenEndLink pipeOpenEnd;
|
||||
private Crankshaft crankshaft;
|
||||
|
||||
// Audio
|
||||
// Links
|
||||
private OpenEndLink intakeOpenEnd; // ambient → left end of first pipe
|
||||
private OrificeLink throttleOrifice; // first pipe right end → plenum inlet (variable area)
|
||||
private OrificeLink plenumToRunner; // plenum outlet → runner left end (fixed area)
|
||||
private OrificeLink intakeValve; // runner right end → cylinder intake port
|
||||
|
||||
private OrificeLink exhaustValve;
|
||||
private OpenEndLink exhaustOpenEnd;
|
||||
|
||||
private Solver solver;
|
||||
private SoundProcessor soundProcessor;
|
||||
|
||||
// Engine geometry (Suzuki TS125 – Jones Appendix 1)
|
||||
private const double Bore = 0.056; // m
|
||||
private const double Stroke = 0.050; // m
|
||||
private const double ConRodLength = 0.110; // m (typical)
|
||||
private const double CrankRadius = Stroke / 2.0;
|
||||
private const double Obliquity = CrankRadius / ConRodLength;
|
||||
private const double CompressionRatio = 6.7; // from Jones
|
||||
|
||||
// Derived volumes
|
||||
private double sweptVolume;
|
||||
private double clearanceVolume;
|
||||
|
||||
// Port timing (degrees from TDC)
|
||||
private const double ExhaustPortOpens = 98.0; // °ATDC
|
||||
private const double ExhaustPortCloses = 262.0; // °ATDC
|
||||
private const double PortWidth = 0.025; // m (estimated)
|
||||
private const double MaxPortArea = 0.001; // m² (fully open)
|
||||
|
||||
// Engine state
|
||||
private double crankAngle; // rad
|
||||
private double engineSpeed; // rad/s
|
||||
private bool combustionPending; // true when ready to fire at TDC
|
||||
|
||||
// Logging
|
||||
private double dt;
|
||||
private int stepCount;
|
||||
|
||||
public double ThrottleArea { get; set; } = 0.0; // controlled externally
|
||||
|
||||
public override void Initialize(int sampleRate)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
|
||||
// Audio
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||||
|
||||
// Solver
|
||||
solver = new Solver();
|
||||
solver.SetTimeStep(dt);
|
||||
solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses
|
||||
solver.CflTarget = 0.9;
|
||||
|
||||
// Compute engine volumes
|
||||
double boreArea = Math.PI * 0.25 * Bore * Bore;
|
||||
sweptVolume = boreArea * Stroke;
|
||||
clearanceVolume = sweptVolume / (CompressionRatio - 1.0);
|
||||
double initialVolume = clearanceVolume; // at TDC
|
||||
|
||||
// Cylinder
|
||||
cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
|
||||
// ---- Cylinder (no valve overlap to avoid backflow) ----
|
||||
double bore = 0.056, stroke = 0.050, conRod = 0.110, compRatio = 10.0;
|
||||
double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0;
|
||||
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, 1000)
|
||||
{
|
||||
Dvdt = 0.0
|
||||
MaxIntakeArea = 0.00037,
|
||||
MaxExhaustArea = 0.00037,
|
||||
};
|
||||
solver.AddComponent(cylinder);
|
||||
|
||||
// Exhaust pipe (1 m, 1 cm², 100 cells)
|
||||
exhaustPipe = new Pipe1D(0.5, 10e-4, 20);
|
||||
double pipeArea = 0.00037; // 3.7 cm²
|
||||
|
||||
// ---- Pipes ----
|
||||
intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); // short pipe before throttle
|
||||
intakeRunner = new Pipe1D(0.10, pipeArea, 5); // runner after plenum
|
||||
exhaustPipe = new Pipe1D(1.00, pipeArea, 80);
|
||||
solver.AddComponent(intakePipeBeforeThrottle);
|
||||
solver.AddComponent(intakeRunner);
|
||||
solver.AddComponent(exhaustPipe);
|
||||
|
||||
// Exhaust port – orifice with variable area
|
||||
var cylPort = cylinder.CreatePort();
|
||||
exhaustPort = new OrificeLink(cylPort, exhaustPipe, isPipeLeftEnd: true,
|
||||
areaProvider: () => ComputeExhaustPortArea(crankAngle))
|
||||
{
|
||||
DischargeCoefficient = 0.8,
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(exhaustPort);
|
||||
// ---- Plenum (100 mL) ----
|
||||
intakePlenum = new Volume0D(0.0001, 101325.0, 300.0); // 0.0001 m³
|
||||
var plenumInlet = intakePlenum.CreatePort(); // from throttle
|
||||
var plenumOutlet = intakePlenum.CreatePort(); // to runner
|
||||
solver.AddComponent(intakePlenum);
|
||||
|
||||
// Pipe open end
|
||||
pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
|
||||
// ---- Intake open end (ambient → left end of first pipe) ----
|
||||
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
|
||||
{
|
||||
AmbientPressure = 101325.0,
|
||||
Gamma = 1.4
|
||||
};
|
||||
solver.AddOpenEndLink(pipeOpenEnd);
|
||||
solver.AddOpenEndLink(intakeOpenEnd);
|
||||
|
||||
// Crankshaft (3000 rpm)
|
||||
crankshaft = new Crankshaft(initialRPM: 10000.0);
|
||||
crankAngle = crankshaft.CrankAngle;
|
||||
engineSpeed = crankshaft.AngularVelocity;
|
||||
combustionPending = false; // first combustion will occur at next TDC
|
||||
// ---- Throttle orifice (first pipe right end → plenum inlet) ----
|
||||
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||||
areaProvider: () => ThrottleArea)
|
||||
{
|
||||
DischargeCoefficient = 0.1, // realistic throttle Cd
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(throttleOrifice);
|
||||
|
||||
// ---- Plenum → runner (fixed area = pipe area) ----
|
||||
plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true,
|
||||
areaProvider: () => pipeArea)
|
||||
{
|
||||
DischargeCoefficient = 1.0,
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(plenumToRunner);
|
||||
|
||||
// ---- Intake valve (runner right end → cylinder intake port) ----
|
||||
intakeValve = new OrificeLink(cylinder.IntakePort, intakeRunner, isPipeLeftEnd: false,
|
||||
areaProvider: () => cylinder.IntakeValveArea)
|
||||
{
|
||||
DischargeCoefficient = 1.0,
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(intakeValve);
|
||||
|
||||
// ---- Exhaust valve ----
|
||||
exhaustValve = new OrificeLink(cylinder.ExhaustPort, exhaustPipe, isPipeLeftEnd: true,
|
||||
areaProvider: () => cylinder.ExhaustValveArea)
|
||||
{
|
||||
DischargeCoefficient = 1.0,
|
||||
UseInertance = false
|
||||
};
|
||||
solver.AddOrificeLink(exhaustValve);
|
||||
|
||||
// ---- Exhaust open end ----
|
||||
exhaustOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
|
||||
{
|
||||
AmbientPressure = 101325.0,
|
||||
Gamma = 1.4
|
||||
};
|
||||
solver.AddOpenEndLink(exhaustOpenEnd);
|
||||
|
||||
stepCount = 0;
|
||||
|
||||
Console.WriteLine("2‑Stroke engine test");
|
||||
Console.WriteLine($"Engine: {Bore*1000:F0} mm x {Stroke*1000:F0} mm, {sweptVolume*1e6:F0} cc");
|
||||
Console.WriteLine($"Compression ratio: {CompressionRatio:F1}, clearance volume: {clearanceVolume*1e6:F2} cc");
|
||||
Console.WriteLine($"Exhaust port opens at {ExhaustPortOpens}° ATDC, closes at {ExhaustPortCloses}° ATDC");
|
||||
}
|
||||
|
||||
// ---- Port area vs crank angle (linear ramp, symmetric) ----
|
||||
private double ComputeExhaustPortArea(double thetaRad)
|
||||
{
|
||||
double thetaDeg = thetaRad * 180.0 / Math.PI;
|
||||
|
||||
// Wrap to [0,360) for easier logic
|
||||
thetaDeg %= 360.0;
|
||||
|
||||
// Exhaust open period
|
||||
if (thetaDeg >= ExhaustPortOpens && thetaDeg <= ExhaustPortCloses)
|
||||
{
|
||||
// Ramp up from 0 to Max, then back down
|
||||
double halfPeriod = (ExhaustPortCloses - ExhaustPortOpens) / 2.0;
|
||||
double midPoint = ExhaustPortOpens + halfPeriod;
|
||||
double distFromMid = Math.Abs(thetaDeg - midPoint) / halfPeriod;
|
||||
double fraction = 1.0 - distFromMid;
|
||||
fraction = Math.Clamp(fraction, 0.0, 1.0);
|
||||
return MaxPortArea * fraction;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// ---- Cylinder volume vs crank angle (slider‑crank) ----
|
||||
private double ComputeCylinderVolume(double thetaRad)
|
||||
{
|
||||
// thetaRad = crank angle from TDC (0 at TDC)
|
||||
double r = CrankRadius;
|
||||
double l = ConRodLength;
|
||||
double cosTh = Math.Cos(thetaRad);
|
||||
double sinTh = Math.Sin(thetaRad);
|
||||
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
|
||||
double x = r * (1.0 - cosTh) + l * (1.0 - term);
|
||||
double area = Math.PI * 0.25 * Bore * Bore;
|
||||
double deltaV = area * x;
|
||||
return clearanceVolume + deltaV;
|
||||
}
|
||||
|
||||
// ---- Combustion: set cylinder pressure AND temperature ----
|
||||
private void Combustion()
|
||||
{
|
||||
double peakPressure = 20.0 * Units.atm; // 30 bar
|
||||
double peakTemperature = 2000.0; // K
|
||||
cylinder.SetPressure(peakPressure, peakTemperature);
|
||||
Console.WriteLine("4‑Stroke engine test (plenum + two pipes)");
|
||||
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
|
||||
Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)");
|
||||
}
|
||||
|
||||
public override float Process()
|
||||
{
|
||||
// Previous crank angle for detecting TDC crossing
|
||||
double prevAngle = crankshaft.CrankAngle;
|
||||
// 1. Advance crankshaft & pre‑step
|
||||
cylinder.Crankshaft.Step(dt);
|
||||
cylinder.PreStep(dt);
|
||||
|
||||
// Advance crankshaft
|
||||
crankshaft.Step(dt);
|
||||
crankAngle = crankshaft.CrankAngle;
|
||||
engineSpeed = crankshaft.AngularVelocity;
|
||||
|
||||
// Update cylinder volume to match current crank angle
|
||||
double newVolume = ComputeCylinderVolume(crankAngle);
|
||||
cylinder.Dvdt = (newVolume - cylinder.Volume) / dt;
|
||||
cylinder.Volume = newVolume;
|
||||
|
||||
// ----- Ignition (once per revolution at TDC) -----
|
||||
const double TwoPi = 2.0 * Math.PI;
|
||||
double prevMod = prevAngle % TwoPi;
|
||||
double currMod = crankAngle % TwoPi;
|
||||
|
||||
// Detect crossing of 0 mod 2π (TDC) – going from near 2π to near 0
|
||||
if (prevMod > Math.PI * 1.8 && currMod < Math.PI * 0.2)
|
||||
{
|
||||
if (!combustionPending)
|
||||
{
|
||||
Combustion();
|
||||
combustionPending = true; // prevent multiple firings during the crossing
|
||||
}
|
||||
}
|
||||
else if (currMod > Math.PI * 0.2 && currMod < Math.PI * 1.8)
|
||||
{
|
||||
combustionPending = false; // reset flag once clear of TDC
|
||||
}
|
||||
|
||||
// Run solver
|
||||
// 2. Run solver
|
||||
solver.Step();
|
||||
stepCount++;
|
||||
|
||||
// Log every 500 steps
|
||||
if (stepCount % 50000 == 0)
|
||||
// 3. Log every 200 steps
|
||||
if (stepCount % 200 == 0)
|
||||
{
|
||||
int midCell = exhaustPipe.CellCount / 2;
|
||||
double crankDeg = cylinder.Crankshaft.CrankAngle * 180.0 / Math.PI % 720.0;
|
||||
double cylP = cylinder.Pressure / 1e5;
|
||||
double cylT = cylinder.Temperature;
|
||||
double cylMass = cylinder.Mass * 1e6;
|
||||
double mdotI = intakeValve.LastMassFlowRate;
|
||||
double mdotE = exhaustValve.LastMassFlowRate;
|
||||
double pipeR = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
|
||||
double plenumP = intakePlenum.Pressure / 1e5;
|
||||
|
||||
double cylP_bar = cylinder.Pressure / 1e5;
|
||||
double cylT_K = cylinder.Temperature;
|
||||
double cylVol_cc = cylinder.Volume * 1e6;
|
||||
|
||||
double pipeL_bar = exhaustPipe.GetCellPressure(0) / 1e5;
|
||||
double pipeM_bar = exhaustPipe.GetCellPressure(midCell) / 1e5;
|
||||
double pipeR_bar = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
|
||||
|
||||
double mdotExh = exhaustPort.LastMassFlowRate; // kg/s, positive into cylinder
|
||||
double mdotOpen = pipeOpenEnd.LastMassFlowRate; // kg/s, positive out
|
||||
|
||||
Console.WriteLine(
|
||||
$"Step {stepCount}: Angle={crankAngle*180.0/Math.PI % 360.0:F1}°, " +
|
||||
$"CylP={cylP_bar:F2} bar, CylT={cylT_K:F0} K, Vol={cylVol_cc:F1} cc, " +
|
||||
$"PipeL={pipeL_bar:F2} bar, PipeM={pipeM_bar:F2} bar, PipeR={pipeR_bar:F2} bar, " +
|
||||
$"mdot_exh={mdotExh:E4} kg/s, mdot_open={mdotOpen:E4} kg/s"
|
||||
);
|
||||
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " +
|
||||
$"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " +
|
||||
$"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar");
|
||||
Console.WriteLine($"Throttle area = {ThrottleArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
|
||||
}
|
||||
|
||||
if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
|
||||
{
|
||||
Console.WriteLine("NaN detected – stopping.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// Audio from open end
|
||||
return soundProcessor.Process(pipeOpenEnd);
|
||||
return soundProcessor.Process(exhaustOpenEnd);
|
||||
}
|
||||
|
||||
public override void Draw(RenderWindow target)
|
||||
{
|
||||
float winWidth = target.GetView().Size.X;
|
||||
float winHeight = target.GetView().Size.Y;
|
||||
float pipeCenterY = winHeight / 2f;
|
||||
float margin = 60f;
|
||||
float pipeStartX = margin;
|
||||
float pipeEndX = winWidth - margin;
|
||||
DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
float winW = target.GetView().Size.X;
|
||||
float winH = target.GetView().Size.Y;
|
||||
|
||||
// Fixed vertical centres for intake and exhaust
|
||||
float intakeY = winH / 2f - 40f;
|
||||
float exhaustY = winH / 2f + 80f;
|
||||
|
||||
// ---- 1. Open end (ambient air source) ----
|
||||
float openEndX = 40f;
|
||||
var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan };
|
||||
openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f);
|
||||
target.Draw(openEndMark);
|
||||
|
||||
// ---- 2. First intake pipe (ambient → throttle) ----
|
||||
float pipe1StartX = openEndX;
|
||||
float pipe1EndX = pipe1StartX + 120f;
|
||||
DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX);
|
||||
|
||||
// ---- 3. Throttle (symbolic restriction) ----
|
||||
float throttleX = pipe1EndX + 5f;
|
||||
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
|
||||
{
|
||||
FillColor = Color.Yellow,
|
||||
Position = new Vector2f(throttleX, intakeY - 15f)
|
||||
};
|
||||
target.Draw(throttleRect);
|
||||
|
||||
// ---- 4. Plenum ----
|
||||
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);
|
||||
|
||||
// ---- 5. Runner pipe (plenum → cylinder) ----
|
||||
float runnerStartX = plenLeftX + plenW + 5f;
|
||||
float runnerEndX = runnerStartX + 100f;
|
||||
DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX);
|
||||
|
||||
// ---- 6. Cylinder ----
|
||||
float cylCX = runnerEndX + 50f; // center X
|
||||
float cylTopY = intakeY - 120f; // top of cylinder (so it sits above the pipe)
|
||||
float cylW = 80f, cylMaxH = 240f;
|
||||
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
|
||||
|
||||
// ---- 7. Exhaust pipe (cylinder → open end) ----
|
||||
float exhStartX = cylCX + cylW / 2f + 20f;
|
||||
float exhEndX = winW - 60f;
|
||||
DrawPipe(target, exhaustPipe, exhaustY, exhStartX, exhEndX);
|
||||
|
||||
// Exhaust open end marker
|
||||
var exhOpenEndMark = new CircleShape(5f) { FillColor = Color.Magenta };
|
||||
exhOpenEndMark.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f);
|
||||
target.Draw(exhOpenEndMark);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user