This commit is contained in:
2026-05-05 19:39:11 +02:00
parent 608dabff12
commit d6b1d214f5
11 changed files with 493 additions and 277 deletions

View File

@@ -13,139 +13,197 @@ namespace FluidSim.Core
private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
private Pipe1D intakePipe;
private PipeVolumeConnection couplingExhaust;
private PipeVolumeConnection couplingIntake;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private Port exitPort = new Port();
private Port exhaustPort = new Port();
private Port intakePort = new Port();
private double dt;
private double pipeArea;
private double exhPipeArea, intPipeArea;
private const double AmbientPressure = 101325.0;
private double time;
private int stepCount = 0;
private const int LogInterval = 10000;
private const int LogInterval = 1000;
// 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 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: inertia + friction that gives ~800 RPM at idle ----
crankshaft = new Crankshaft(initialRPM: 600.0) // start a bit low
// Crankshaft
crankshaft = new Crankshaft(initialRPM: 2000.0)
{
Inertia = 0.005, // slightly heavier flywheel
FrictionConstant = 0.8, // static friction
FrictionViscous = 0.01 // viscous (linear with RPM)
Inertia = 0.05,
FrictionConstant = 0.5,
FrictionViscous = 0.01
};
// ---- 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);
// 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 = 5;
exhaustPipe.EnergyRelaxationRate = 50;
exhaustPipe.DampingMultiplier = 0.0;
exhaustPipe.EnergyRelaxationRate = 100.0f;
// ---- Cylinder ----
// 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: 10.0,
pipeArea: pipeArea, sampleRate: sampleRate);
bore: 0.065, stroke: 0.0565, compressionRatio: 8.0,
exhPipeArea: exhPipeArea, intPipeArea: intPipeArea, sampleRate: sampleRate);
engineCyl.ignition = true;
// ---- Coupling ----
coupling = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
// Set crank to BDC (180°) and sync
crankshaft.CrankAngle = Math.PI;
crankshaft.PreviousAngle = Math.PI; // make sure this property is settable (public setter)
// ---- Solver ----
// 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.AddConnection(coupling);
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 processor (stable version) ----
soundProcessor = new SoundProcessor(sampleRate, pipeRadius * 2);
soundProcessor.Gain = 0.00001f;
// Sound
exhaustSoundProcessor = new SoundProcessor(sampleRate, exhRadius * 2);
exhaustSoundProcessor.Gain = 0.001f;
// ---- Reverb ----
intakeSoundProcessor = new SoundProcessor(sampleRate, intRadius * 2);
intakeSoundProcessor.Gain = 0.001f;
// 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
reverb.DryMix = 1.0f;
reverb.EarlyMix = 0.5f;
reverb.TailMix = 0.9f;
reverb.Feedback = 0.9f;
reverb.DampingFreq = 6000f;
Console.WriteLine("=== EngineScenario (Stable) ===");
Console.WriteLine($"Crankshaft inertia: {crankshaft.Inertia}");
Console.WriteLine($"Pipe: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
Console.WriteLine("=== Engine with intake & cycleaware valves ===");
}
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 throttle = Math.Clamp(Throttle, 0.2, 1.0);
double targetPressure = throttle * FullLoadPeakPressure;
engineCyl.TargetPeakPressure = targetPressure;
// ---- Simulate one timestep ----
engineCyl.Step(dt);
crankshaft.Step(dt);
coupling.OrificeArea = engineCyl.OrificeArea;
couplingExhaust.OrificeArea = engineCyl.ExhaustOrificeArea;
couplingIntake.OrificeArea = engineCyl.IntakeOrificeArea;
solver.Step();
// ---- Update exit port with safety clamps ----
UpdateExitPort();
UpdateExhaustPort();
UpdateIntakePort();
float dryExhaust = exhaustSoundProcessor.Process(exhaustPort);
float dryIntake = intakeSoundProcessor.Process(intakePort);
float dry = dryExhaust + dryIntake;
// ---- Generate audio ----
float dry = soundProcessor.Process(exitPort);
float wet = reverb.Process(dry);
time += dt;
stepCount++;
if (++stepCount % LogInterval == 0) Log();
return wet;
}
private void UpdateExitPort()
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);
// 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)
// 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 * pipeArea;
double outflowMassFlow = rho * vel * exhPipeArea;
// 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;
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;
@@ -169,10 +227,10 @@ namespace FluidSim.Core
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(40f, centerY - cylH / 2f);
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);
@@ -181,33 +239,60 @@ namespace FluidSim.Core
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;
// ---- 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 = pipeStartX + i * dx;
double p = exhaustPipe.GetCellPressure(i);
double rho = exhaustPipe.GetCellDensity(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 = 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);
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);
}
}