Files
FluidSim/Scenarios/Inline4Scenario.cs
2026-05-08 00:38:26 +02:00

303 lines
14 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
private Pipe1D runner1, runner2, runner3, runner4;
// Exhaust pipes
private Pipe1D exh1, exh2, exh3, exh4;
// 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
private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4;
// Exhaust open ends
private OpenEndLink exhaustOpenEnd1, exhaustOpenEnd2, exhaustOpenEnd3, exhaustOpenEnd4;
private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private double dt;
private int stepCount;
public double MaxThrottleArea { get; set; } = 3 * Units.cm2;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
solver = new Solver();
solver.SetTimeStep(dt);
solver.CflTarget = 0.9;
// ---- Shared crankshaft ----
crankshaft = new Crankshaft(800);
crankshaft.Inertia = 1;
crankshaft.FrictionConstant = 16;
crankshaft.FrictionViscous = 0.5;
// ---- Cylinder geometry ----
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2;
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
// Firing order 1-3-4-2 → phase offsets in radians
double phase0 = 0.0 * Math.PI / 180.0;
double phase1 = 180.0 * Math.PI / 180.0;
double phase2 = 540.0 * Math.PI / 180.0;
double phase3 = 360.0 * Math.PI / 180.0;
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 = phase0,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.01
};
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 = phase1,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.01
};
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 = phase2,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.01
};
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 = phase3,
EnergyVariationFraction = 0.03,
MisfireProbability = 0.01
};
solver.AddComponent(cyl1);
solver.AddComponent(cyl2);
solver.AddComponent(cyl3);
solver.AddComponent(cyl4);
double pipeDiameter = 2 * Units.cm;
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
reverb = new OutdoorExhaustReverb(sampleRate);
// ---- Intake pipe before throttle ----
intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10);
solver.AddComponent(intakePipeBeforeThrottle);
// ---- Plenum ----
intakePlenum = new Volume0D(50 * 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);
// ---- Runners ----
runner1 = new Pipe1D(0.2, pipeArea, 5);
runner2 = new Pipe1D(0.2, pipeArea, 5);
runner3 = new Pipe1D(0.2, pipeArea, 5);
runner4 = new Pipe1D(0.2, pipeArea, 5);
solver.AddComponent(runner1);
solver.AddComponent(runner2);
solver.AddComponent(runner3);
solver.AddComponent(runner4);
// ---- Exhaust pipes ----
exh1 = new Pipe1D(0.2, pipeArea, 10);
exh2 = new Pipe1D(0.2, pipeArea, 10);
exh3 = new Pipe1D(0.2, pipeArea, 10);
exh4 = new Pipe1D(0.2, pipeArea, 10);
solver.AddComponent(exh1);
solver.AddComponent(exh2);
solver.AddComponent(exh3);
solver.AddComponent(exh4);
// ---- Plenum → runner orifices ----
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 ----
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 ----
exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exh1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exh2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exh3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exh4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
solver.AddOrificeLink(exhaustValve1);
solver.AddOrificeLink(exhaustValve2);
solver.AddOrificeLink(exhaustValve3);
solver.AddOrificeLink(exhaustValve4);
// ---- Exhaust open ends ----
exhaustOpenEnd1 = new OpenEndLink(exh1, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
exhaustOpenEnd2 = new OpenEndLink(exh2, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
exhaustOpenEnd3 = new OpenEndLink(exh3, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
exhaustOpenEnd4 = new OpenEndLink(exh4, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
solver.AddOpenEndLink(exhaustOpenEnd1);
solver.AddOpenEndLink(exhaustOpenEnd2);
solver.AddOpenEndLink(exhaustOpenEnd3);
solver.AddOpenEndLink(exhaustOpenEnd4);
// ---- 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.001, 1.0))
{
DischargeCoefficient = 0.2,
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");
}
// Mix all exhaust sounds
float exhaustMix = exhaustSoundProcessor.Process(exhaustOpenEnd1)
+ exhaustSoundProcessor.Process(exhaustOpenEnd2)
+ exhaustSoundProcessor.Process(exhaustOpenEnd3)
+ exhaustSoundProcessor.Process(exhaustOpenEnd4);
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
return reverb.Process(exhaustMix * 0.25f + intakeDry);
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float startX = 60f;
float spacing = 80f;
float intakeY = winH / 2f - 80f;
float exhaustY = winH / 2f + 80f;
// Plenum
float plenW = 50f, plenH = 120f;
float plenX = startX;
float plenTopY = intakeY - plenH / 2f;
DrawVolume(target, intakePlenum, plenX, plenTopY, plenW, plenH);
// Helper arrays just for drawing (no closures)
var cyls = new[] { cyl1, cyl2, cyl3, cyl4 };
var runners = new[] { runner1, runner2, runner3, runner4 };
var exhausts = new[] { exh1, exh2, exh3, exh4 };
for (int i = 0; i < 4; i++)
{
float cylX = plenX + plenW + 30f + i * spacing;
float runnerStartX = plenX + plenW + 5f;
float runnerEndX = cylX - 20f;
DrawPipe(target, runners[i], intakeY, runnerStartX, runnerEndX);
float cylTopY = intakeY - 120f;
DrawCylinder(target, cyls[i], cylX, cylTopY, 70f, 200f);
float exhStartX = cylX + 35f;
float exhEndX = exhStartX + 100f;
DrawPipe(target, exhausts[i], exhaustY, exhStartX, exhEndX);
var mark = new CircleShape(4f) { FillColor = Color.Magenta };
mark.Position = new Vector2f(exhEndX - 4f, exhaustY - 4f);
target.Draw(mark);
}
// Throttle symbol
var throttleRect = new RectangleShape(new Vector2f(6f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(plenX - 16f, intakeY - 15f)
};
target.Draw(throttleRect);
}
}
}