325 lines
13 KiB
C#
325 lines
13 KiB
C#
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 = 140.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.2,
|
||
FrictionViscous = 0.025
|
||
};
|
||
|
||
// Exhaust pipe (longer, larger)
|
||
double exhLength = 0.5;
|
||
double exhRadius = 1.5 * Units.cm;
|
||
exhPipeArea = Math.PI * exhRadius * exhRadius;
|
||
exhaustPipe = new Pipe1D(exhLength, exhPipeArea, sampleRate, forcedCellCount: 30);
|
||
exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure);
|
||
exhaustPipe.DampingMultiplier = 0.0;
|
||
exhaustPipe.EnergyRelaxationRate = 100.0f;
|
||
|
||
// Intake pipe (shorter, narrower)
|
||
double intLength = 0.1;
|
||
double intRadius = 1 * Units.cm;
|
||
intPipeArea = Math.PI * intRadius * intRadius;
|
||
intakePipe = new Pipe1D(intLength, intPipeArea, sampleRate, forcedCellCount: 10);
|
||
intakePipe.SetUniformState(1.225, 0.0, AmbientPressure);
|
||
|
||
// Cylinder (starts at BDC, fresh charge)
|
||
engineCyl = new EngineCylinder(crankshaft,
|
||
bore: 56 * Units.mm, stroke: 57 * Units.mm, compressionRatio: 9.5,
|
||
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 & cycle‑aware valves ===");
|
||
}
|
||
|
||
public override float Process()
|
||
{
|
||
double idleThrottle = 0.1;
|
||
if (crankshaft.AngularVelocity < 80) idleThrottle = 0.2;
|
||
double throttle = Math.Clamp(Throttle, idleThrottle, 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);
|
||
|
||
// ---------- NEW: Valve lift indicators ----------
|
||
float barWidth = 30f;
|
||
float barHeight = 10f;
|
||
float exhLift = (float)engineCyl.ExhaustValveLiftCurrent;
|
||
float intLift = (float)engineCyl.IntakeValveLiftCurrent;
|
||
|
||
// Exhaust valve indicator (right side of cylinder)
|
||
var exhBar = new RectangleShape(new Vector2f(barWidth, barHeight))
|
||
{
|
||
Position = new Vector2f(cylRect.Position.X + cylW - 10,
|
||
cylRect.Position.Y - 20 - exhLift * 20),
|
||
FillColor = new Color(200, 200, 200)
|
||
};
|
||
target.Draw(exhBar);
|
||
|
||
// Intake valve indicator (left side of cylinder)
|
||
var intBar = new RectangleShape(new Vector2f(barWidth, barHeight))
|
||
{
|
||
Position = new Vector2f(cylRect.Position.X - 20,
|
||
cylRect.Position.Y - 20 - intLift * 20),
|
||
FillColor = new Color(200, 200, 200)
|
||
};
|
||
target.Draw(intBar);
|
||
|
||
// ---- Exhaust pipe (rightwards) ----
|
||
DrawPipe(target, exhaustPipe, startX: 280f, endX: winW - 60f, centerY + 10 - cylRect.Size.Y / 2,
|
||
T_ambient, T_hot, T_cold, R, NormaliseTemperature, true);
|
||
|
||
// ---- Intake pipe (leftwards) ----
|
||
DrawPipe(target, intakePipe, startX: 200f, endX: 20f, centerY + 10 - cylRect.Size.Y / 2,
|
||
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.2f * (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);
|
||
}
|
||
}
|
||
} |