Files
FluidSim/Scenarios/Inline4Scenario.cs
2026-05-08 13:16:51 +02:00

472 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Utils;
namespace FluidSim.Tests
{
public class Inline4Scenario : Scenario
{
// Crankshaft
private Crankshaft crankshaft;
// Cylinders
private Cylinder cyl1, cyl2, cyl3, cyl4;
// Intake
private Pipe1D intakePipeBeforeThrottle;
private Volume0D intakePlenum;
// Runners (shorter, fewer cells)
private Pipe1D runner1, runner2, runner3, runner4;
// Exhaust collector + tailpipe
private Volume0D exhaustCollector;
private Pipe1D tailPipe;
// Exhaust stubs (short pipes between cylinders and collector)
private Pipe1D exhStub1, exhStub2, exhStub3, exhStub4;
// Links intake
private OpenEndLink intakeOpenEnd;
private OrificeLink throttleOrifice;
// Plenumtorunner orifices
private OrificeLink plenumToRunner1, plenumToRunner2, plenumToRunner3, plenumToRunner4;
// Intake valves
private OrificeLink intakeValve1, intakeValve2, intakeValve3, intakeValve4;
// Exhaust valves (cylinder → stub)
private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4;
// Stubtocollector orifices
private OrificeLink stubToCollector1, stubToCollector2, stubToCollector3, stubToCollector4;
// Collectortotailpipe orifice
private OrificeLink collectorToTailpipe;
// Exhaust open end (tailpipe exit)
private OpenEndLink exhaustOpenEnd;
private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private double dt;
private int stepCount;
public double MaxThrottleArea { get; set; } = 10 * Units.cm2;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
solver = new Solver();
solver.SetTimeStep(dt);
solver.CflTarget = 1;
// ---- Shared crankshaft ----
crankshaft = new Crankshaft(800);
crankshaft.Inertia = 1;
crankshaft.FrictionConstant = 3;
crankshaft.FrictionViscous = 0.2;
// ---- Cylinder geometry ----
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 10;
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
// Firing order 1-3-4-2 with 180° intervals (0°, 180°, 360°, 540°)
double phaseCyl1 = 0.0;
double phaseCyl3 = Math.PI; // 180°
double phaseCyl4 = 2.0 * Math.PI; // 360°
double phaseCyl2 = 3.0 * Math.PI; // 540°
cyl1 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl1,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl2 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl2,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl3 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl3,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
cyl4 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
IntakeValveDiameter = 30 * Units.mm,
IntakeValveLift = 5 * Units.mm,
ExhaustValveDiameter = 28 * Units.mm,
ExhaustValveLift = 5 * Units.mm,
PhaseOffset = phaseCyl4,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.0
};
solver.AddComponent(cyl1);
solver.AddComponent(cyl2);
solver.AddComponent(cyl3);
solver.AddComponent(cyl4);
double pipeDiameter = 4 * Units.cm;
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
// Sound processors (only one exhaust source now)
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f };
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.2f };
reverb = new OutdoorExhaustReverb(sampleRate);
// ---- Intake pipe before throttle (shorter, fewer cells) ----
intakePipeBeforeThrottle = new Pipe1D(0.1, pipeArea, 10);
intakePipeBeforeThrottle.Name = "Intake pipe";
solver.AddComponent(intakePipeBeforeThrottle);
// ---- Plenum ----
intakePlenum = new Volume0D(100 * Units.mL, 101325.0, 300.0);
var plenumInlet = intakePlenum.CreatePort(); // port 0
var plenumOut1 = intakePlenum.CreatePort(); // port 1
var plenumOut2 = intakePlenum.CreatePort(); // port 2
var plenumOut3 = intakePlenum.CreatePort(); // port 3
var plenumOut4 = intakePlenum.CreatePort(); // port 4
solver.AddComponent(intakePlenum);
// ---- Intake runners (shorter, fewer cells) ----
runner1 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 1" };
runner2 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 2" };
runner3 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 3" };
runner4 = new Pipe1D(0.1, pipeArea, 5) { Name = "Runner 4" };
solver.AddComponent(runner1);
solver.AddComponent(runner2);
solver.AddComponent(runner3);
solver.AddComponent(runner4);
// ---- Exhaust collector volume ----
exhaustCollector = new Volume0D(200 * Units.mL, 101325.0, 800.0);
var colIn1 = exhaustCollector.CreatePort(); // cylinder 1 stub
var colIn2 = exhaustCollector.CreatePort(); // cylinder 2 stub
var colIn3 = exhaustCollector.CreatePort(); // cylinder 3 stub
var colIn4 = exhaustCollector.CreatePort(); // cylinder 4 stub
var colOut = exhaustCollector.CreatePort(); // to tailpipe
solver.AddComponent(exhaustCollector);
// ---- Exhaust stub pipes (short connection cylinder → collector) ----
exhStub1 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 1" };
exhStub2 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 2" };
exhStub3 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 3" };
exhStub4 = new Pipe1D(0.1, pipeArea, 5) { Name = "ExhStub 4" };
solver.AddComponent(exhStub1);
solver.AddComponent(exhStub2);
solver.AddComponent(exhStub3);
solver.AddComponent(exhStub4);
foreach (var p in new[] { runner1, runner2, runner3, runner4, exhStub1, exhStub2, exhStub3, exhStub4, intakePipeBeforeThrottle })
{
p.DampingMultiplier = 0.5;
p.EnergyRelaxationRate = 0.0;
}
// ---- Tailpipe (single exhaust pipe) ----
tailPipe = new Pipe1D(0.5, pipeArea, 20)
{
Name = "Tailpipe",
DampingMultiplier = 0.5,
EnergyRelaxationRate = 0.0
};
solver.AddComponent(tailPipe);
// ---- Plenum → runner orifices (volume port to pipe left end) ----
plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(plenumToRunner1);
solver.AddOrificeLink(plenumToRunner2);
solver.AddOrificeLink(plenumToRunner3);
solver.AddOrificeLink(plenumToRunner4);
// ---- Intake valves (cylinder port to runner right end) ----
intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(intakeValve1);
solver.AddOrificeLink(intakeValve2);
solver.AddOrificeLink(intakeValve3);
solver.AddOrificeLink(intakeValve4);
// ---- Exhaust valves (cylinder port to stub left end) ----
exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exhStub1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exhStub2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exhStub3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exhStub4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(exhaustValve1);
solver.AddOrificeLink(exhaustValve2);
solver.AddOrificeLink(exhaustValve3);
solver.AddOrificeLink(exhaustValve4);
// ---- Stub → collector orifices (collector port to stub right end) ----
stubToCollector1 = new OrificeLink(colIn1, exhStub1, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector2 = new OrificeLink(colIn2, exhStub2, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector3 = new OrificeLink(colIn3, exhStub3, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
stubToCollector4 = new OrificeLink(colIn4, exhStub4, isPipeLeftEnd: false, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(stubToCollector1);
solver.AddOrificeLink(stubToCollector2);
solver.AddOrificeLink(stubToCollector3);
solver.AddOrificeLink(stubToCollector4);
// ---- Collector → tailpipe (collector port to tailpipe left end) ----
collectorToTailpipe = new OrificeLink(colOut, tailPipe, isPipeLeftEnd: true, areaProvider: () => pipeArea)
{ DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(collectorToTailpipe);
// ---- Exhaust open end (tailpipe exit) ----
exhaustOpenEnd = new OpenEndLink(tailPipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(exhaustOpenEnd);
// ---- Intake open end ----
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(intakeOpenEnd);
// ---- Throttle ----
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0005, 1.0))
{
DischargeCoefficient = 0.9,
UseInertance = false
};
solver.AddOrificeLink(throttleOrifice);
stepCount = 0;
Console.WriteLine("Inline-4 engine test");
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
Console.WriteLine("Firing order 1-3-4-2, 180° intervals");
}
public override float Process()
{
crankshaft.Step(dt);
cyl1.PreStep(dt);
cyl2.PreStep(dt);
cyl3.PreStep(dt);
cyl4.PreStep(dt);
solver.Step();
stepCount++;
if (stepCount % 10000 == 0)
{
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
Console.WriteLine($"Step {stepCount}, RPM = {rpm:F0}, " +
$"cyl1 P = {cyl1.Pressure / 1e5:F2} bar, " +
$"plenum P = {intakePlenum.Pressure / 1e5:F2} bar");
}
// Sound: only one exhaust source now
float exhaustSound = exhaustSoundProcessor.Process(exhaustOpenEnd);
float intakeSound = intakeSoundProcessor.Process(intakeOpenEnd);
return reverb.Process(exhaustSound * 0.25f + intakeSound);
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
// --- Layout constants ---
float leftMargin = 40f;
float plenumW = 50f, plenumH = 120f;
float cylinderWidth = 60f, cylinderMaxHeight = 180f;
float cylinderSpacing = 90f;
float cylinderTopY = winH * 0.25f;
// Plenum position
float plenumCenterX = leftMargin + plenumW / 2f;
float plenumTopY = cylinderTopY - 20f;
DrawVolume(target, intakePlenum, plenumCenterX, plenumTopY, plenumW, plenumH);
// Throttle symbol (yellow rectangle) left of plenum
float throttleWidth = 8f, throttleHeight = 30f;
float throttleCenterX = leftMargin - 10f;
var throttleRect = new RectangleShape(new Vector2f(throttleWidth, throttleHeight))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleCenterX - throttleWidth / 2f, plenumTopY + plenumH / 2f - throttleHeight / 2f)
};
target.Draw(throttleRect);
// Intake pipe before throttle (left of throttle)
float intakePipeEndX = throttleCenterX - throttleWidth / 2f;
float intakePipeStartX = intakePipeEndX - 100f;
float intakePipeY = plenumTopY + plenumH / 2f;
DrawPipe(target, intakePipeBeforeThrottle, intakePipeY, intakePipeStartX, intakePipeEndX);
// Intake open end marker
var intakeMark = new CircleShape(4f) { FillColor = Color.Magenta };
intakeMark.Position = new Vector2f(intakePipeStartX - 4f, intakePipeY - 4f);
target.Draw(intakeMark);
// Cylinders and runners
float runnerStartX = leftMargin + plenumW;
Cylinder[] cyls = { cyl1, cyl2, cyl3, cyl4 };
Pipe1D[] runners = { runner1, runner2, runner3, runner4 };
for (int i = 0; i < 4; i++)
{
float cylCenterX = runnerStartX + 40f + i * cylinderSpacing;
float runnerEndX = cylCenterX;
DrawPipe(target, runners[i], plenumTopY + plenumH / 2f, runnerStartX, runnerEndX);
DrawCylinder(target, cyls[i], cylCenterX, cylinderTopY, cylinderWidth, cylinderMaxHeight);
}
// Exhaust collector below cylinders
float collectorLeftX = runnerStartX + 40f - cylinderWidth / 2f;
float collectorWidth = 3 * cylinderSpacing + cylinderWidth;
float collectorTopY = cylinderTopY + cylinderMaxHeight + 40f;
float collectorHeight = 50f;
float collectorCenterX = collectorLeftX + collectorWidth / 2f;
DrawVolume(target, exhaustCollector, collectorCenterX, collectorTopY, collectorWidth, collectorHeight);
// Tailpipe from right edge of collector
float tailStartX = collectorLeftX + collectorWidth;
float tailEndX = tailStartX + 150f;
float tailCenterY = collectorTopY + collectorHeight / 2f;
DrawPipe(target, tailPipe, tailCenterY, tailStartX, tailEndX);
// Exhaust open end marker
var exhaustMark = new CircleShape(4f) { FillColor = Color.Magenta };
exhaustMark.Position = new Vector2f(tailEndX - 4f, tailCenterY - 4f);
target.Draw(exhaustMark);
// Exhaust stubs (vertical connections from cylinder bottom to collector)
Pipe1D[] stubs = { exhStub1, exhStub2, exhStub3, exhStub4 };
for (int i = 0; i < 4; i++)
{
float cylCenterX = runnerStartX + 40f + i * cylinderSpacing;
float vertStartY = cylinderTopY + cylinderMaxHeight;
float vertEndY = collectorTopY;
// Draw stub as a vertical pipe
DrawPipeVertical(target, stubs[i], cylCenterX, vertStartY, vertEndY);
}
}
// Helper to draw a pipe vertically (reuse temperature coloring)
private void DrawPipeVertical(RenderWindow target, Pipe1D pipe, float centerX, float topY, float bottomY)
{
int n = pipe.CellCount;
if (n < 2) return;
float pipeLengthPx = bottomY - topY;
float dy = pipeLengthPx / (n - 1);
float baseRadius = 25f;
float rangeFactor = 2f;
float scaleFactor = 2f;
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);
}
var centersY = new float[n];
var radii = new float[n];
var temperatures = new double[n];
double R_gas = 287.0;
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / Math.Max(rho * R_gas, 1e-12);
temperatures[i] = T;
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centersY[i] = topY + i * dy;
}
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
for (int i = 0; i < n; i++)
{
float y = centersY[i];
float r = radii[i];
Color col = TemperatureColor(temperatures[i]);
stripVertices[idx++] = new Vertex(new Vector2f(centerX - r, y), col);
stripVertices[idx++] = new Vertex(new Vector2f(centerX + r, y), col);
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
float st = SmoothStep(0f, 1f, t);
float yi = centersY[i] + (centersY[i + 1] - centersY[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st;
Color coli = TemperatureColor(Ti);
stripVertices[idx++] = new Vertex(new Vector2f(centerX - ri, yi), coli);
stripVertices[idx++] = new Vertex(new Vector2f(centerX + ri, yi), coli);
}
}
}
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
}
}
}