Files
FluidSim/Scenarios/EngineScenario.cs
2026-05-05 10:32:30 +02:00

238 lines
9.7 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 SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Volume0D cylinder;
private Pipe1D exhaustPipe;
private PipeVolumeConnection coupling;
private SoundProcessor soundProcessor;
private double dt;
private double ambientPressure = 101325.0;
private double time;
// Crankshaft
private double crankAngle = 0.0;
private const double TargetRPM = 4000.0;
private double angularVelocity;
// Combustion
private const double CombustionPressure = 8.0 * 101325.0;
private const double CombustionTemperature = 1800.0;
// Valve timing
private const double ValveOpenStart = 120.0 * Math.PI / 180.0;
private const double ValveOpenEnd = 480.0 * Math.PI / 180.0;
private const double ValveRampWidth = 30.0 * Math.PI / 180.0;
private double maxOrificeArea;
// Misfire
private Random rand = new Random();
private const double MisfireProbability = 0.02;
private bool isMisfiring = false;
// Lowpass filter for pressure
private double lastFilteredPressure;
private const double PressureCutoffHz = 50.0;
// Logging
private int stepCount = 0;
private const int LogStepInterval = 1000;
private int combustionCount = 0;
private int misfireCount = 0;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
angularVelocity = TargetRPM * 2.0 * Math.PI / 60.0;
// Cylinder: 0.5 litre, initially at ambient
double cylVolume = 0.5e-3;
double initialPressure = ambientPressure;
double initialTemperature = 300.0;
cylinder = new Volume0D(cylVolume, initialPressure, initialTemperature, sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
// Exhaust pipe: length 2.5 m, radius 2 cm
double pipeLength = 2.5;
double pipeRadius = 0.02;
double pipeArea = Math.PI * pipeRadius * pipeRadius;
maxOrificeArea = pipeArea;
exhaustPipe = new Pipe1D(pipeLength, pipeArea, sampleRate, forcedCellCount: 70);
exhaustPipe.SetUniformState(1.225, 0.0, ambientPressure);
// Coupling (valve starts closed)
coupling = new PipeVolumeConnection(cylinder, exhaustPipe, true, orificeArea: 0.0);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(cylinder);
solver.AddPipe(exhaustPipe);
solver.AddConnection(coupling);
// Use ZeroPressureOpen for strong reflections
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.ZeroPressureOpen, ambientPressure);
// Sound processor (tuned to pipe length)
soundProcessor = new SoundProcessor(sampleRate, pipeLength, pipeRadius * 2, reverbTimeMs: 200.0f);
soundProcessor.MasterGain = 0.02f; // boosted from 0.0008
soundProcessor.PressureGain = 4.0f; // boosted from6 0.12
soundProcessor.TurbulenceGain = 0.0002f; // reduced from 0.02
soundProcessor.SetAmbientPressure(ambientPressure);
lastFilteredPressure = ambientPressure;
Console.WriteLine("=== EngineScenario (ZeroPressureOpen, boosted gains) ===");
Console.WriteLine($"Target RPM: {TargetRPM}, Misfire prob: {MisfireProbability * 100:F1}%");
Console.WriteLine($"Pipe length: {pipeLength} m, fundamental: {340/(4*pipeLength):F1} Hz");
Console.WriteLine($"Valve opens at {ValveOpenStart*180/Math.PI:F0}°, closes at {ValveOpenEnd*180/Math.PI:F0}°, ramp {ValveRampWidth*180/Math.PI:F0}°");
Console.WriteLine($"Sample rate: {sampleRate} Hz, dt = {dt*1000:F3} ms");
Console.WriteLine("Time[s] Crank[°] Valve[%] MassFlow[kg/s] Comb# Misfire");
Console.WriteLine("---------------------------------------------------------");
}
private double ValveOpenRatio(double crankRad)
{
double cycleAngle = crankRad % (4.0 * Math.PI);
double openStart = ValveOpenStart;
double openEnd = ValveOpenEnd;
if (cycleAngle < openStart || cycleAngle > openEnd)
return 0.0;
double fullOpenWindow = openEnd - openStart;
double closedWindow = 2.0 * ValveRampWidth;
if (fullOpenWindow <= closedWindow)
return 1.0;
double tmid = (openStart + openEnd) / 2.0;
double dist = Math.Abs(cycleAngle - tmid);
double rampHalf = (fullOpenWindow - closedWindow) / 2.0;
if (dist <= rampHalf)
return 1.0;
else
{
double frac = (dist - rampHalf) / ValveRampWidth;
frac = Math.Clamp(frac, 0.0, 1.0);
double lift = Math.Cos(frac * Math.PI / 2.0);
return lift * lift;
}
}
public override float Process()
{
// Update crank angle
crankAngle += angularVelocity * dt;
if (crankAngle >= 2.0 * Math.PI)
{
crankAngle -= 2.0 * Math.PI;
isMisfiring = rand.NextDouble() < MisfireProbability;
}
// Power stroke at TDC
if (crankAngle < angularVelocity * dt && crankAngle >= 0.0)
{
if (isMisfiring)
{
double vol = cylinder.Volume;
double R = cylinder.GasConstant;
double T0 = 300.0;
double newMass = ambientPressure * vol / (R * T0);
double newInternalEnergy = ambientPressure * vol / (cylinder.Gamma - 1.0);
cylinder.Mass = newMass;
cylinder.InternalEnergy = newInternalEnergy;
misfireCount++;
}
else
{
double volume = cylinder.Volume;
double gamma = cylinder.Gamma;
double newInternalEnergy = CombustionPressure * volume / (gamma - 1.0);
double R = cylinder.GasConstant;
double newMass = CombustionPressure * volume / (R * CombustionTemperature);
cylinder.InternalEnergy = newInternalEnergy;
cylinder.Mass = newMass;
combustionCount++;
}
}
// Update valve area
double valveOpen = ValveOpenRatio(crankAngle);
coupling.OrificeArea = maxOrificeArea * valveOpen;
float massFlow = solver.Step();
float endPressure = (float)exhaustPipe.GetCellPressure(exhaustPipe.GetCellCount() - 1);
// Lowpass filter the pressure (emphasise low frequencies)
double rc = 1.0 / (2.0 * Math.PI * PressureCutoffHz);
double alpha = dt / (rc + dt);
double filteredPressure = alpha * endPressure + (1.0 - alpha) * lastFilteredPressure;
lastFilteredPressure = filteredPressure;
float audioSample = soundProcessor.Process(massFlow, (float)filteredPressure);
time += dt;
stepCount++;
// Logging
if (stepCount % LogStepInterval == 0 || (crankAngle < angularVelocity * dt * 2 && !isMisfiring && combustionCount > 0))
{
Console.WriteLine($"{time,7:F3} {crankAngle * 180.0 / Math.PI,6:F1} " +
$"{valveOpen * 100,6:F1} {massFlow,10:F4} " +
$"{combustionCount,3} {(isMisfiring ? "X" : "")}");
}
return audioSample;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(40f, centerY - cylH / 2f);
double pNorm = (cylinder.Pressure - ambientPressure) / ambientPressure;
if (double.IsNaN(pNorm)) pNorm = 0;
byte red = (byte)(Math.Clamp(pNorm * 128, 0, 255));
byte blue = (byte)(Math.Clamp(-pNorm * 128, 0, 255));
cylRect.FillColor = new Color(red, 0, blue);
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];
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = exhaustPipe.GetCellPressure(i);
float r = baseRadius * (float)(1.0 + (p - ambientPressure) / ambientPressure);
if (r < 2f) r = 2f;
double t = (p - ambientPressure) / ambientPressure;
t = Math.Clamp(t, -1.0, 1.0);
byte rCol = (byte)(t > 0 ? 255 * t : 0);
byte bCol = (byte)(t < 0 ? -255 * t : 0);
byte gCol = (byte)(255 * (1 - Math.Abs(t)));
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);
}
}
}