Files
FluidSim/Scenarios/EngineScenario.cs
2026-05-05 19:39:11 +02:00

299 lines
12 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 FluidSim.Components;
using FluidSim.Utils;
using FluidSim.Interfaces;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe;
private Pipe1D intakePipe;
private PipeVolumeConnection couplingExhaust;
private PipeVolumeConnection couplingIntake;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private Port exhaustPort = new Port();
private Port intakePort = new Port();
private double dt;
private double exhPipeArea, intPipeArea;
private const double AmbientPressure = 101325.0;
private double time;
private int stepCount = 0;
private const int LogInterval = 1000;
public double Throttle { get; set; } = 0.15;
private const double FullLoadPeakPressure = 60.0 * Units.bar;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// Crankshaft
crankshaft = new Crankshaft(initialRPM: 2000.0)
{
Inertia = 0.05,
FrictionConstant = 0.5,
FrictionViscous = 0.01
};
// Exhaust pipe (longer, larger)
double exhLength = 1;
double exhRadius = 0.02;
exhPipeArea = Math.PI * exhRadius * exhRadius;
exhaustPipe = new Pipe1D(exhLength, exhPipeArea, sampleRate, forcedCellCount: 100);
exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure);
exhaustPipe.DampingMultiplier = 0.0;
exhaustPipe.EnergyRelaxationRate = 100.0f;
// Intake pipe (shorter, narrower)
double intLength = 1;
double intRadius = 0.01;
intPipeArea = Math.PI * intRadius * intRadius;
intakePipe = new Pipe1D(intLength, intPipeArea, sampleRate, forcedCellCount: 50);
intakePipe.SetUniformState(1.225, 0.0, AmbientPressure);
// Cylinder (starts at BDC, fresh charge)
engineCyl = new EngineCylinder(crankshaft,
bore: 0.065, stroke: 0.0565, compressionRatio: 8.0,
exhPipeArea: exhPipeArea, intPipeArea: intPipeArea, sampleRate: sampleRate);
engineCyl.ignition = true;
// Set crank to BDC (180°) and sync
crankshaft.CrankAngle = Math.PI;
crankshaft.PreviousAngle = Math.PI; // make sure this property is settable (public setter)
// Couplings
couplingExhaust = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
couplingIntake = new PipeVolumeConnection(engineCyl.Cylinder, intakePipe, false, orificeArea: 0.0);
// Solver
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe);
solver.AddPipe(intakePipe);
solver.AddConnection(couplingExhaust);
solver.AddConnection(couplingIntake);
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure);
solver.SetPipeBoundary(intakePipe, true, BoundaryType.GhostCell); // cylinder side left
solver.SetPipeBoundary(intakePipe, false, BoundaryType.OpenEnd, AmbientPressure); // ambient side right
// Sound
exhaustSoundProcessor = new SoundProcessor(sampleRate, exhRadius * 2);
exhaustSoundProcessor.Gain = 0.001f;
intakeSoundProcessor = new SoundProcessor(sampleRate, intRadius * 2);
intakeSoundProcessor.Gain = 0.001f;
// Reverb
reverb = new OutdoorExhaustReverb(sampleRate);
reverb.DryMix = 1.0f;
reverb.EarlyMix = 0.5f;
reverb.TailMix = 0.9f;
reverb.Feedback = 0.9f;
reverb.DampingFreq = 6000f;
Console.WriteLine("=== Engine with intake & cycleaware valves ===");
}
public override float Process()
{
double throttle = Math.Clamp(Throttle, 0.2, 1.0);
double targetPressure = throttle * FullLoadPeakPressure;
engineCyl.TargetPeakPressure = targetPressure;
engineCyl.Step(dt);
crankshaft.Step(dt);
couplingExhaust.OrificeArea = engineCyl.ExhaustOrificeArea;
couplingIntake.OrificeArea = engineCyl.IntakeOrificeArea;
solver.Step();
UpdateExhaustPort();
UpdateIntakePort();
float dryExhaust = exhaustSoundProcessor.Process(exhaustPort);
float dryIntake = intakeSoundProcessor.Process(intakePort);
float dry = dryExhaust + dryIntake;
float wet = reverb.Process(dry);
if (++stepCount % LogInterval == 0) Log();
return wet;
}
private void Log()
{
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
double cycleDeg = (engineCyl.CycleAngle * 180.0 / Math.PI) % 720.0;
string stroke = cycleDeg < 180.0 ? "Power" :
cycleDeg < 360.0 ? "Exhaust" :
cycleDeg < 540.0 ? "Intake" : "Compression";
// Cylinder
double pCyl = engineCyl.Cylinder.Pressure;
double TCyl = engineCyl.Cylinder.Temperature;
double VCyl = engineCyl.Cylinder.Volume;
double mCyl = engineCyl.Cylinder.Mass;
double exhArea = engineCyl.ExhaustOrificeArea * 1e6; // mm²
double intArea = engineCyl.IntakeOrificeArea * 1e6; // mm²
// Exhaust pipe
int exhLast = exhaustPipe.GetCellCount() - 1;
double pExhEnd = exhaustPipe.GetCellPressure(exhLast);
double mdotExhOut = exhaustPipe.GetOpenEndMassFlow(); // positive out
// Intake pipe
double mdotIntIn = couplingIntake.LastMassFlowIntoVolume;
double pIntAmbEnd = intakePort.Pressure;
Console.WriteLine(
$"{stepCount,8} {stroke,-11} {cycleDeg,6:F1}° " +
$"RPM:{rpm,5:F0} " +
$"Cyl: p={pCyl/1e5,6:F3}bar T={TCyl,6:F0}K V={VCyl*1e6,6:F0}cm³ m={mCyl*1e3,6:F6}g " +
$"Valves: Exh={exhArea,5:F0}mm² Int={intArea,5:F0}mm² " +
$"Intake: p_end={pIntAmbEnd/1e5,6:F3}bar mdot_in={mdotIntIn,7:F4}kg/s " +
$"Exhaust: p_end={pExhEnd/1e5,6:F3}bar mdot_out={mdotExhOut,7:F4}kg/s");
}
private void UpdateExhaustPort()
{
int last = exhaustPipe.GetCellCount() - 1;
double p = exhaustPipe.GetCellPressure(last);
double rho = exhaustPipe.GetCellDensity(last);
double vel = exhaustPipe.GetCellVelocity(last);
// Safety clamps
rho = Math.Clamp(rho, 0.01, 50.0);
vel = Math.Clamp(vel, -500.0, 500.0);
p = Math.Clamp(p, 1e4, 2e6);
double outflowMassFlow = rho * vel * exhPipeArea;
exhaustPort.Pressure = p;
exhaustPort.Density = rho;
exhaustPort.Temperature = p / (rho * 287.05);
exhaustPort.MassFlowRate = -outflowMassFlow;
exhaustPort.SpecificEnthalpy = 0.0;
}
private void UpdateIntakePort()
{
// Use the actual valve mass flow (positive = into cylinder)
double mdotIntoEngine = couplingIntake.LastMassFlowIntoVolume;
// Use cylinder pressure/density for the port state (or intake pipe last cell)
double pCyl = engineCyl.Cylinder.Pressure;
double rhoCyl = engineCyl.Cylinder.Density;
intakePort.Pressure = Math.Max(pCyl, 100);
intakePort.Density = Math.Max(rhoCyl, 1e-6);
intakePort.Temperature = engineCyl.Cylinder.Temperature;
intakePort.MassFlowRate = mdotIntoEngine;
intakePort.SpecificEnthalpy = 0.0;
}
// ==================== Drawing ====================
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
const float T_ambient = 293.15f;
const float T_hot = 1500f;
const float T_cold = 0f;
const float R = 287.05f;
float deltaHot = T_hot - T_ambient;
float deltaCold = T_ambient - T_cold;
float NormaliseTemperature(double T)
{
double t;
if (T >= T_ambient)
t = (T - T_ambient) / deltaHot;
else
t = (T - T_ambient) / deltaCold;
return (float)Math.Clamp(t, -1.0, 1.0);
}
// ---- Cylinder ----
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(200f, centerY - cylH / 2f);
double tempCyl = engineCyl.Cylinder.Temperature;
float tnCyl = NormaliseTemperature(tempCyl);
byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(rC, gC, bC);
target.Draw(cylRect);
// ---- Piston ----
float pistonWidth = cylW - 12f;
float pistonHeight = 16f;
float pistonFraction = (float)engineCyl.PistonPositionFraction;
float pistonTopY = cylRect.Position.Y + pistonFraction * (cylH - pistonHeight);
var pistonRect = new RectangleShape(new Vector2f(pistonWidth, pistonHeight))
{
Position = new Vector2f(cylRect.Position.X + 6f, pistonTopY),
FillColor = new Color(80, 80, 80)
};
target.Draw(pistonRect);
// ---- Exhaust pipe (rightwards) ----
DrawPipe(target, exhaustPipe, startX: 280f, endX: winW - 60f, centerY,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, true);
// ---- Intake pipe (leftwards) ----
DrawPipe(target, intakePipe, startX: 200f, endX: 20f, centerY,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, false);
}
private void DrawPipe(RenderWindow target, Pipe1D pipe,
float startX, float endX, float centerY,
float T_ambient, float T_hot, float T_cold, float R,
Func<double, float> normaliseTemp, bool leftToRight)
{
int n = pipe.GetCellCount();
float dir = leftToRight ? 1f : -1f;
float pipeLen = Math.Abs(endX - startX);
float dx = pipeLen / (n - 1) * dir;
float baseRadius = leftToRight ? 20f : 14f; // exhaust thicker, intake thinner
var vertices = new Vertex[n * 2];
float ambPress = 101325f;
for (int i = 0; i < n; i++)
{
float x = startX + i * dx;
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / (rho * R);
float r = baseRadius * 0.3f * (float)(1.0 + (p - ambPress) / ambPress);
if (r < 2f) r = 2f;
float tn = normaliseTemp(T);
byte rC = (byte)(tn > 0 ? 255 * tn : 0);
byte bC = (byte)(tn < 0 ? -255 * tn : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tn)));
var col = new Color(rC, gC, bC);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
}
}
}