seemingly working, added display text
This commit is contained in:
@@ -9,124 +9,222 @@ namespace FluidSim.Tests
|
||||
{
|
||||
public class TestScenario : Scenario
|
||||
{
|
||||
// Simulation core
|
||||
private Solver solver;
|
||||
private Volume0D volume;
|
||||
private Pipe1D pipe;
|
||||
private OrificeLink orifice; // volume → pipe left
|
||||
private OpenEndLink openEnd; // pipe right → atmosphere
|
||||
private SoundProcessor soundProcessor;
|
||||
private int stepCount;
|
||||
|
||||
// Pressure reset scheduling
|
||||
private double simTime;
|
||||
private double dt;
|
||||
private double resetInterval = 0.2; // seconds between resets
|
||||
private double nextResetTime;
|
||||
private double targetPressure = 10 * Units.atm;
|
||||
private double rampDuration = 0.002; // 2 ms ramp
|
||||
private double rampStartTime;
|
||||
private double rampStartPressure; // pressure when ramp started
|
||||
private bool ramping;
|
||||
|
||||
// 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;
|
||||
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1);
|
||||
soundProcessor.Gain = 2.0f; // lower gain to avoid clipping
|
||||
// Audio
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||||
|
||||
// Solver
|
||||
solver = new Solver();
|
||||
solver.SetTimeStep(dt);
|
||||
solver.CflTarget = 0.4;
|
||||
solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses
|
||||
|
||||
volume = new Volume0D(1e-3, targetPressure, 300.0);
|
||||
solver.AddComponent(volume);
|
||||
// Compute engine volumes
|
||||
double boreArea = Math.PI * 0.25 * Bore * Bore;
|
||||
sweptVolume = boreArea * Stroke;
|
||||
clearanceVolume = sweptVolume / (CompressionRatio - 1.0);
|
||||
double initialVolume = clearanceVolume; // at TDC
|
||||
|
||||
pipe = new Pipe1D(1.0, 1e-4, 200);
|
||||
pipe.EnergyRelaxationRate = 10;
|
||||
solver.AddComponent(pipe);
|
||||
|
||||
var volPort = volume.CreatePort();
|
||||
double orificeArea = 1e-5;
|
||||
orifice = new OrificeLink(volPort, pipe, isPipeLeftEnd: true,
|
||||
areaProvider: () => orificeArea)
|
||||
// Cylinder
|
||||
cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
|
||||
{
|
||||
DischargeCoefficient = 0.62,
|
||||
UseInertance = true,
|
||||
EffectiveLength = 0.001
|
||||
Dvdt = 0.0
|
||||
};
|
||||
solver.AddOrificeLink(orifice);
|
||||
solver.AddComponent(cylinder);
|
||||
|
||||
openEnd = new OpenEndLink(pipe, isLeftEnd: false)
|
||||
// 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(openEnd);
|
||||
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;
|
||||
simTime = 0.0;
|
||||
nextResetTime = resetInterval;
|
||||
ramping = false;
|
||||
|
||||
Console.WriteLine("Pressure reset test with smooth ramp");
|
||||
Console.WriteLine($"Volume 1L, reset to {targetPressure} Pa every {resetInterval*1000} ms, ramp {rampDuration*1000} ms");
|
||||
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++;
|
||||
simTime += dt;
|
||||
|
||||
// ---- Smooth pressure ramp ----
|
||||
if (ramping)
|
||||
{
|
||||
double elapsed = simTime - rampStartTime;
|
||||
if (elapsed >= rampDuration)
|
||||
{
|
||||
// Ramp finished, set exactly to target
|
||||
volume.SetPressure(targetPressure);
|
||||
ramping = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double frac = elapsed / rampDuration;
|
||||
double currentTarget = rampStartPressure + (targetPressure - rampStartPressure) * frac;
|
||||
volume.SetPressure(currentTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Trigger a new reset ----
|
||||
if (!ramping && simTime >= nextResetTime)
|
||||
{
|
||||
rampStartPressure = volume.Pressure; // current pressure before reset
|
||||
rampStartTime = simTime;
|
||||
ramping = true;
|
||||
nextResetTime += resetInterval;
|
||||
}
|
||||
|
||||
// Log every 500 steps
|
||||
if (stepCount % 500 == 0)
|
||||
if (stepCount % 50000 == 0)
|
||||
{
|
||||
double volP = volume.Pressure;
|
||||
double pipeL = pipe.GetCellPressure(0);
|
||||
double pipeR = pipe.GetCellPressure(pipe.CellCount - 1);
|
||||
double mdotOrif = orifice.LastMassFlowRate;
|
||||
double mdotOpen = openEnd.LastMassFlowRate;
|
||||
int midCell = exhaustPipe.CellCount / 2;
|
||||
|
||||
Console.WriteLine($"Step {stepCount}: " +
|
||||
$"VolP={volP:F1} Pa, PipeL={pipeL:F1}, PipeR={pipeR:F1}, " +
|
||||
$"mdot_orif={mdotOrif:E4} kg/s, mdot_open={mdotOpen:E4} kg/s");
|
||||
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(pipe.GetCellPressure(0)))
|
||||
if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
|
||||
{
|
||||
Console.WriteLine("NaN detected – stopping.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return soundProcessor.Process(openEnd);
|
||||
// Audio from open end
|
||||
return soundProcessor.Process(pipeOpenEnd);
|
||||
}
|
||||
|
||||
public override void Draw(RenderWindow target)
|
||||
@@ -137,7 +235,7 @@ namespace FluidSim.Tests
|
||||
float margin = 60f;
|
||||
float pipeStartX = margin;
|
||||
float pipeEndX = winWidth - margin;
|
||||
DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user