engine almost working, backup before adding gas types.

This commit is contained in:
max
2026-05-07 20:07:15 +02:00
parent 14f5ba925f
commit 92d84eacfe
18 changed files with 1236 additions and 587 deletions

View File

@@ -7,39 +7,28 @@ namespace FluidSim.Tests
{
public abstract class Scenario
{
/// <summary>Initialize the scenario with a given audio sample rate.</summary>
public abstract void Initialize(int sampleRate);
/// <summary>Advance one simulation step and return an audio sample.</summary>
public abstract float Process();
/// <summary>Draw the current simulation state onto the given SFML render target.</summary>
public abstract void Draw(RenderWindow target);
// ---------- Shared drawing helpers ----------
protected const double AmbientPressure = 101325.0;
protected const double AmbientTemperature = 300.0; // K
protected const double AmbientTemperature = 300.0;
/// <summary>Map temperature [0K … 2000K] to a color: blue (0K) → green (300K) → red (2000K).</summary>
// ---------- Color helper ----------
protected Color TemperatureColor(double temperature)
{
// Clamp to the range we want to display
double t = Math.Clamp(temperature, 0.0, 2000.0);
byte r, g, b;
if (t < AmbientTemperature)
{
// Blue → Green
double factor = t / AmbientTemperature; // 0 at 0K, 1 at 300K
double factor = t / AmbientTemperature;
r = 0;
g = (byte)(255 * factor);
b = (byte)(255 * (1.0 - factor));
}
else
{
// Green → Red
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); // 0 at 300K, 1 at 2000K
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature);
r = (byte)(255 * factor);
g = (byte)(255 * (1.0 - factor));
b = 0;
@@ -47,30 +36,84 @@ namespace FluidSim.Tests
return new Color(r, g, b);
}
/// <summary>
/// Draws the pipe as a smooth trianglestrip whose radius varies with cell pressure (for visibility),
/// but colored by temperature.
/// </summary>
// ---------- Draw a generic volume (e.g. plenum) ----------
protected void DrawVolume(RenderWindow target, Volume0D volume,
float centerX, float topY, float width, float height)
{
var rect = new RectangleShape(new Vector2f(width, height))
{
FillColor = TemperatureColor(volume.Temperature),
Position = new Vector2f(centerX - width / 2f, topY)
};
target.Draw(rect);
var border = new RectangleShape(new Vector2f(width, height))
{
FillColor = Color.Transparent,
OutlineColor = Color.White,
OutlineThickness = 1f,
Position = new Vector2f(centerX - width / 2f, topY)
};
target.Draw(border);
}
// ---------- Draw an engine cylinder ----------
protected void DrawCylinder(RenderWindow target, Cylinder cylinder,
float centerX, float topY, float width, float maxHeight)
{
double fraction = cylinder.PistonFraction; // 0 = TDC, 1 = BDC
float currentHeight = (float)(maxHeight * fraction);
// Walls
var wall = new RectangleShape(new Vector2f(width, maxHeight));
wall.FillColor = new Color(60, 60, 60);
wall.Position = new Vector2f(centerX - width / 2f, topY);
target.Draw(wall);
// Gas
float gasTop = topY;
var gasRect = new RectangleShape(new Vector2f(width, currentHeight));
gasRect.FillColor = TemperatureColor(cylinder.Temperature);
gasRect.Position = new Vector2f(centerX - width / 2f, gasTop);
target.Draw(gasRect);
// Piston line
var pistonLine = new RectangleShape(new Vector2f(width, 4f));
pistonLine.FillColor = Color.White;
pistonLine.Position = new Vector2f(centerX - width / 2f, topY + currentHeight);
target.Draw(pistonLine);
// Valve indicators
float valveW = 6f, valveH = 10f, valveY = topY + 4f;
var intakeValve = new RectangleShape(new Vector2f(valveW, valveH));
intakeValve.FillColor = cylinder.IntakeValveArea > 0 ? Color.Green : Color.Red;
intakeValve.Position = new Vector2f(centerX - width / 2f - valveW - 2f, valveY);
target.Draw(intakeValve);
var exhaustValve = new RectangleShape(new Vector2f(valveW, valveH));
exhaustValve.FillColor = cylinder.ExhaustValveArea > 0 ? Color.Green : Color.Red;
exhaustValve.Position = new Vector2f(centerX + width / 2f + 2f, valveY);
target.Draw(exhaustValve);
}
// ---------- Draw a pipe ----------
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
{
int n = pipe.CellCount;
if (n < 2) return;
float pipeLengthPx = pipeEndX - pipeStartX;
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float dx = pipeLengthPx / (n - 1);
float baseRadius = 25f;
float rangeFactor = 2f;
float scaleFactor = 2f;
// ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
// ----- Precompute cell positions, radii, and temperatures -----
var centers = new float[n];
var radii = new float[n];
var temperatures = new double[n];
@@ -80,7 +123,7 @@ namespace FluidSim.Tests
{
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / Math.Max(rho * R_gas, 1e-12); // ideal gas
double T = p / Math.Max(rho * R_gas, 1e-12);
temperatures[i] = T;
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
@@ -89,7 +132,6 @@ namespace FluidSim.Tests
centers[i] = pipeStartX + i * dx;
}
// ----- Build trianglestrip vertices -----
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
@@ -112,7 +154,7 @@ namespace FluidSim.Tests
float st = SmoothStep(0f, 1f, t);
float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; // linear interpolation
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
Color coli = TemperatureColor(Ti);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);

View File

@@ -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 highpressure 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("2Stroke 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 (slidercrank) ----
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("4Stroke 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 & prestep
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);
}
}
}