238 lines
9.7 KiB
C#
238 lines
9.7 KiB
C#
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;
|
||
|
||
// Low‑pass 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);
|
||
|
||
// Low‑pass 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);
|
||
}
|
||
}
|
||
} |