241 lines
9.2 KiB
C#
241 lines
9.2 KiB
C#
using System;
|
||
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 components
|
||
private Volume0D cylinder;
|
||
private Pipe1D exhaustPipe;
|
||
private OrificeLink exhaustPort;
|
||
private OpenEndLink pipeOpenEnd;
|
||
private Crankshaft crankshaft;
|
||
|
||
// Audio
|
||
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 int stepCount;
|
||
|
||
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
|
||
|
||
// 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)
|
||
{
|
||
Dvdt = 0.0
|
||
};
|
||
solver.AddComponent(cylinder);
|
||
|
||
// Exhaust pipe (1 m, 1 cm², 100 cells)
|
||
exhaustPipe = new Pipe1D(0.5, 10e-4, 20);
|
||
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);
|
||
|
||
// Pipe open end
|
||
pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
|
||
{
|
||
AmbientPressure = 101325.0,
|
||
Gamma = 1.4
|
||
};
|
||
solver.AddOpenEndLink(pipeOpenEnd);
|
||
|
||
// Crankshaft (3000 rpm)
|
||
crankshaft = new Crankshaft(initialRPM: 10000.0);
|
||
crankAngle = crankshaft.CrankAngle;
|
||
engineSpeed = crankshaft.AngularVelocity;
|
||
combustionPending = false; // first combustion will occur at next TDC
|
||
|
||
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);
|
||
}
|
||
|
||
public override float Process()
|
||
{
|
||
// Previous crank angle for detecting TDC crossing
|
||
double prevAngle = crankshaft.CrankAngle;
|
||
|
||
// 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
|
||
solver.Step();
|
||
stepCount++;
|
||
|
||
// Log every 500 steps
|
||
if (stepCount % 50000 == 0)
|
||
{
|
||
int midCell = exhaustPipe.CellCount / 2;
|
||
|
||
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"
|
||
);
|
||
}
|
||
|
||
if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
|
||
{
|
||
Console.WriteLine("NaN detected – stopping.");
|
||
return 0f;
|
||
}
|
||
|
||
// Audio from open end
|
||
return soundProcessor.Process(pipeOpenEnd);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
} |