249 lines
9.3 KiB
C#
249 lines
9.3 KiB
C#
using System;
|
||
using FluidSim.Components;
|
||
|
||
namespace FluidSim.Core
|
||
{
|
||
public class EngineCylinder
|
||
{
|
||
public Volume0D Cylinder { get; private set; }
|
||
private Crankshaft crankshaft;
|
||
|
||
private double bore, stroke, conRodLength, compressionRatio;
|
||
private double pistonArea;
|
||
|
||
public double V_disp { get; private set; }
|
||
public double V_clear { get; private set; }
|
||
public bool ignition = false;
|
||
|
||
// ---- Exhaust valve ----
|
||
private double exhMaxOrificeArea;
|
||
private double exhValveOpenStart = 130.0 * Math.PI / 180.0;
|
||
private double exhValveOpenEnd = 390.0 * Math.PI / 180.0;
|
||
private double exhValveRampWidth = 30.0 * Math.PI / 180.0;
|
||
public double ExhaustOrificeArea => ExhaustValveLift() * exhMaxOrificeArea;
|
||
public double ExhaustValveLiftCurrent => ExhaustValveLift();
|
||
|
||
// ---- Intake valve ----
|
||
private double intMaxOrificeArea;
|
||
private double intValveOpenStart = 340.0 * Math.PI / 180.0;
|
||
private double intValveOpenEnd = 600.0 * Math.PI / 180.0;
|
||
private double intValveRampWidth = 30.0 * Math.PI / 180.0;
|
||
public double IntakeOrificeArea => IntakeValveLift() * intMaxOrificeArea;
|
||
public double IntakeValveLiftCurrent => IntakeValveLift();
|
||
|
||
// ---- Combustion ----
|
||
public double TargetPeakPressure { get; set; } = 50.0 * 101325.0;
|
||
private const double PeakTemperature = 2500.0;
|
||
private bool burnInProgress = false;
|
||
private double burnStartAngle; // cycle angle (0–4π)
|
||
private double burnDuration = 40.0 * Math.PI / 180.0;
|
||
private double targetBurnEnergy;
|
||
private double preIgnitionMass, preIgnitionInternalEnergy;
|
||
|
||
private Random rand = new Random();
|
||
public double MisfireProbability { get; set; } = 0.02;
|
||
private bool misfireCurrent = false;
|
||
|
||
public int CombustionCount { get; private set; }
|
||
public int MisfireCount { get; private set; }
|
||
|
||
// Cycle‑aware angle (0 – 4π)
|
||
public double CycleAngle => crankshaft.CrankAngle % (4.0 * Math.PI);
|
||
private double prevCycleAngle;
|
||
|
||
// Piston position fraction (0 = TDC, 1 = BDC)
|
||
public double PistonPositionFraction
|
||
{
|
||
get
|
||
{
|
||
double currentVol = Cylinder.Volume;
|
||
if (currentVol <= V_clear) return 0.0;
|
||
if (currentVol >= V_clear + V_disp) return 1.0;
|
||
return (currentVol - V_clear) / V_disp;
|
||
}
|
||
}
|
||
|
||
public EngineCylinder(Crankshaft crankshaft,
|
||
double bore, double stroke, double compressionRatio,
|
||
double exhPipeArea, double intPipeArea, int sampleRate)
|
||
{
|
||
this.crankshaft = crankshaft;
|
||
this.bore = bore;
|
||
this.stroke = stroke;
|
||
conRodLength = 2.0 * stroke;
|
||
this.compressionRatio = compressionRatio;
|
||
exhMaxOrificeArea = exhPipeArea * 0.5;
|
||
intMaxOrificeArea = intPipeArea * 0.5;
|
||
pistonArea = Math.PI / 4.0 * bore * bore;
|
||
|
||
V_disp = pistonArea * stroke;
|
||
V_clear = V_disp / (compressionRatio - 1.0);
|
||
|
||
// Start at BDC with fresh ambient charge
|
||
double V_bdc = V_clear + V_disp;
|
||
double p_amb = 101325.0;
|
||
double T_amb = 300.0;
|
||
double rho0 = p_amb / (287.0 * T_amb);
|
||
double mass0 = rho0 * V_bdc;
|
||
double energy0 = p_amb * V_bdc / (1.4 - 1.0);
|
||
|
||
Cylinder = new Volume0D(V_bdc, p_amb, T_amb, sampleRate)
|
||
{
|
||
Gamma = 1.4,
|
||
GasConstant = 287.0
|
||
};
|
||
Cylinder.Volume = V_bdc;
|
||
Cylinder.Mass = mass0;
|
||
Cylinder.InternalEnergy = energy0;
|
||
|
||
prevCycleAngle = CycleAngle;
|
||
|
||
preIgnitionMass = Cylinder.Mass;
|
||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||
}
|
||
|
||
// ---- Piston kinematics ----
|
||
private (double volume, double dvdt) PistonKinematics(double cycleAngle)
|
||
{
|
||
double theta = cycleAngle % (2.0 * Math.PI);
|
||
double R = stroke / 2.0;
|
||
double cosT = Math.Cos(theta);
|
||
double sinT = Math.Sin(theta);
|
||
double L = conRodLength;
|
||
|
||
double s = R * (1 - cosT) + L - Math.Sqrt(L * L - R * R * sinT * sinT);
|
||
double V = V_clear + pistonArea * s;
|
||
|
||
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
|
||
double dVdθ = pistonArea * (R * sinT + (R * R * sinT * cosT) / sqrtTerm);
|
||
double dvdt = dVdθ * crankshaft.AngularVelocity;
|
||
return (V, dvdt);
|
||
}
|
||
|
||
// ---- Valve lifts (cycle‑aware) ----
|
||
private double ExhaustValveLift()
|
||
{
|
||
double a = CycleAngle;
|
||
if (a < exhValveOpenStart || a > exhValveOpenEnd) return 0.0;
|
||
double duration = exhValveOpenEnd - exhValveOpenStart;
|
||
double ramp = exhValveRampWidth;
|
||
double t = (a - exhValveOpenStart) / duration;
|
||
double rampFrac = ramp / duration;
|
||
if (t < rampFrac) return t / rampFrac;
|
||
if (t > 1.0 - rampFrac) return (1.0 - t) / rampFrac;
|
||
return 1.0;
|
||
}
|
||
|
||
private double IntakeValveLift()
|
||
{
|
||
double a = CycleAngle;
|
||
if (a < intValveOpenStart || a > intValveOpenEnd) return 0.0;
|
||
double duration = intValveOpenEnd - intValveOpenStart;
|
||
double ramp = intValveRampWidth;
|
||
double t = (a - intValveOpenStart) / duration;
|
||
double rampFrac = ramp / duration;
|
||
if (t < rampFrac) return t / rampFrac;
|
||
if (t > 1.0 - rampFrac) return (1.0 - t) / rampFrac;
|
||
return 1.0;
|
||
}
|
||
|
||
// ---- Wiebe burn fraction ----
|
||
private double WiebeFraction(double angleFromIgnition)
|
||
{
|
||
if (angleFromIgnition >= burnDuration) return 1.0;
|
||
double a = 5.0, m = 2.0;
|
||
double x = angleFromIgnition / burnDuration;
|
||
return 1.0 - Math.Exp(-a * Math.Pow(x, m + 1));
|
||
}
|
||
|
||
// ---- Torque from pressure ----
|
||
private double ComputeTorque()
|
||
{
|
||
double p = Cylinder.Pressure;
|
||
double ambient = 101325.0;
|
||
double force = (p - ambient) * pistonArea;
|
||
if (force <= 0) return 0.0;
|
||
|
||
double theta = crankshaft.CrankAngle % (2.0 * Math.PI);
|
||
double R = stroke / 2.0;
|
||
double L = conRodLength;
|
||
double sinT = Math.Sin(theta);
|
||
double cosT = Math.Cos(theta);
|
||
|
||
double sqrtTerm = Math.Sqrt(L * L - R * R * sinT * sinT);
|
||
double lever = R * sinT * (1.0 + (R * cosT) / sqrtTerm);
|
||
return force * lever;
|
||
}
|
||
|
||
// ---- TDC detection (power stroke, at angle 0 mod 4π) ----
|
||
private bool DetectTDCPowerStroke()
|
||
{
|
||
double current = CycleAngle;
|
||
double previous = prevCycleAngle;
|
||
prevCycleAngle = current;
|
||
return (previous > 3.8 * Math.PI && current < 0.2 * Math.PI);
|
||
}
|
||
|
||
public void Step(double dt)
|
||
{
|
||
bool crossingTDC = DetectTDCPowerStroke();
|
||
|
||
if (crossingTDC)
|
||
{
|
||
misfireCurrent = rand.NextDouble() < MisfireProbability;
|
||
|
||
// *** Always capture the state at TDC, whether we burn or not ***
|
||
preIgnitionMass = Cylinder.Mass;
|
||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||
|
||
if (misfireCurrent)
|
||
{
|
||
MisfireCount++;
|
||
}
|
||
else if (ignition)
|
||
{
|
||
double V = Cylinder.Volume;
|
||
targetBurnEnergy = TargetPeakPressure * V / (Cylinder.Gamma - 1.0);
|
||
if (double.IsNaN(targetBurnEnergy))
|
||
targetBurnEnergy = 101325.0 * V / (Cylinder.Gamma - 1.0);
|
||
burnInProgress = true;
|
||
burnStartAngle = CycleAngle;
|
||
CombustionCount++;
|
||
}
|
||
}
|
||
|
||
if (burnInProgress)
|
||
{
|
||
double angleFromIgnition = CycleAngle - burnStartAngle;
|
||
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI;
|
||
|
||
if (angleFromIgnition >= burnDuration)
|
||
{
|
||
Cylinder.InternalEnergy = targetBurnEnergy;
|
||
burnInProgress = false;
|
||
}
|
||
else
|
||
{
|
||
double fraction = WiebeFraction(angleFromIgnition);
|
||
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction)
|
||
+ targetBurnEnergy * fraction;
|
||
Cylinder.Mass = preIgnitionMass;
|
||
}
|
||
}
|
||
|
||
var (vol, dvdt) = PistonKinematics(CycleAngle);
|
||
Cylinder.Volume = vol;
|
||
Cylinder.Dvdt = dvdt;
|
||
|
||
if (double.IsNaN(Cylinder.Pressure) || double.IsNaN(Cylinder.Temperature) || Cylinder.Mass < 1e-9)
|
||
{
|
||
double V = Math.Max(vol, V_clear);
|
||
Cylinder.Mass = 1.225 * V;
|
||
Cylinder.InternalEnergy = 101325.0 * V / (1.4 - 1.0);
|
||
}
|
||
|
||
double torque = ComputeTorque();
|
||
crankshaft.AddTorque(torque);
|
||
}
|
||
}
|
||
} |