Files
FluidSim/Scenarios/EngineScenario.cs
2026-05-05 16:10:06 +02:00

214 lines
8.4 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
private OutdoorExhaustReverb reverb;
private Port exitPort = new Port();
private double dt;
private double pipeArea;
private const double AmbientPressure = 101325.0;
private double time;
private int stepCount = 0;
private const int LogInterval = 10000;
// Throttle 0..1
public double Throttle { get; set; } = 0.0; // start with a light idle throttle
// ---- Realistic combustion parameters ----
private const double FullLoadPeakPressure = 70.0 * 101325.0; // 15 bar
// ---- Idle speed governor ----
private const double TargetIdleRPM = 800.0; // rad/s = RPM * π/30, we'll convert
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// ---- Crankshaft: inertia + friction that gives ~800 RPM at idle ----
crankshaft = new Crankshaft(initialRPM: 600.0) // start a bit low
{
Inertia = 0.005, // slightly heavier flywheel
FrictionConstant = 0.8, // static friction
FrictionViscous = 0.01 // viscous (linear with RPM)
};
// ---- Pipe: add a tiny bit of damping to prevent unrealistic shocks ----
double pipeLength = 2;
double pipeRadius = 0.1;
pipeArea = Math.PI * pipeRadius * pipeRadius;
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 60);
exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure);
exhaustPipe.DampingMultiplier = 5;
exhaustPipe.EnergyRelaxationRate = 50;
// ---- Cylinder ----
engineCyl = new EngineCylinder(crankshaft,
bore: 0.065, stroke: 0.0565, compressionRatio: 10.0,
pipeArea: pipeArea, sampleRate: sampleRate);
// ---- Coupling ----
coupling = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
// ---- Solver ----
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe);
solver.AddConnection(coupling);
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure);
// ---- Sound processor (stable version) ----
soundProcessor = new SoundProcessor(sampleRate, pipeRadius * 2);
soundProcessor.Gain = 0.00001f;
// ---- Reverb ----
reverb = new OutdoorExhaustReverb(sampleRate);
// Church: vast, highly reflective, bright
reverb.DryMix = 1.0f; // always full dry signal
reverb.EarlyMix = 0.5f; // distinct early reflections from distant walls
reverb.TailMix = 0.9f; // huge tail, almost as loud as the dry sound
reverb.Feedback = 0.9f; // long decay roughly 3s reverb time (with current delay lengths)
reverb.DampingFreq = 6000f; // bright: highfrequency energy stays for a long time
reverb.MatrixCoeff = 0.5f; // default orthogonal mix
Console.WriteLine("=== EngineScenario (Stable) ===");
Console.WriteLine($"Crankshaft inertia: {crankshaft.Inertia}");
Console.WriteLine($"Pipe: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
}
public override float Process()
{
// ---- RPM governor: adjust throttle to maintain idle when no user input ----
double currentRPM = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
double throttle = Math.Clamp(Throttle, 0.05, 1.0); // never let it drop below a tiny value
// ---- Target combustion pressure ----
double targetPressure = throttle * FullLoadPeakPressure;
engineCyl.TargetPeakPressure = targetPressure;
// ---- Simulate one timestep ----
engineCyl.Step(dt);
crankshaft.Step(dt);
coupling.OrificeArea = engineCyl.OrificeArea;
solver.Step();
// ---- Update exit port with safety clamps ----
UpdateExitPort();
// ---- Generate audio ----
float dry = soundProcessor.Process(exitPort);
float wet = reverb.Process(dry);
time += dt;
stepCount++;
return wet;
}
private void UpdateExitPort()
{
int last = exhaustPipe.GetCellCount() - 1;
double p = exhaustPipe.GetCellPressure(last);
double rho = exhaustPipe.GetCellDensity(last);
double vel = exhaustPipe.GetCellVelocity(last);
// Clamp density to physically possible values
if (rho < 0.01) rho = 0.01; // never let it approach zero
if (rho > 50.0) rho = 50.0; // never let it become absurd
// Clamp velocity to ± 500 m/s (safe subsonic)
vel = Math.Clamp(vel, -500.0, 500.0);
double outflowMassFlow = rho * vel * pipeArea;
// Clamp exit pressure to sensible range (0.1 20 bar)
p = Math.Clamp(p, 1e4, 2e6);
exitPort.Pressure = p;
exitPort.Density = rho;
exitPort.Temperature = p / (rho * 287.05);
exitPort.MassFlowRate = -outflowMassFlow;
exitPort.SpecificEnthalpy = 0.0;
}
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);
}
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(40f, 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);
int n = exhaustPipe.GetCellCount();
float pipeStartX = 120f, pipeEndX = winW - 60f;
float pipeLen = pipeEndX - pipeStartX;
float dx = pipeLen / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
float ambPress = 101325f;
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = exhaustPipe.GetCellPressure(i);
double rho = exhaustPipe.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 = NormaliseTemperature(T);
byte rCol = (byte)(tn > 0 ? 255 * tn : 0);
byte bCol = (byte)(tn < 0 ? -255 * tn : 0);
byte gCol = (byte)(255 * (1 - Math.Abs(tn)));
var col = new Color(rCol, gCol, bCol);
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);
}
}
}