tuff
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// EngineCylinder.cs (in Core namespace)
|
||||
using System;
|
||||
using FluidSim.Components;
|
||||
|
||||
@@ -11,21 +10,32 @@ namespace FluidSim.Core
|
||||
|
||||
private double bore, stroke, conRodLength, compressionRatio;
|
||||
private double pistonArea;
|
||||
private double V_disp, V_clear;
|
||||
private double maxOrificeArea;
|
||||
|
||||
private double valveOpenStart = 120.0 * Math.PI / 180.0;
|
||||
private double valveOpenEnd = 480.0 * Math.PI / 180.0;
|
||||
private double valveRampWidth = 30.0 * Math.PI / 180.0;
|
||||
public double OrificeArea => ValveLift() * maxOrificeArea;
|
||||
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 = 120.0 * Math.PI / 180.0; // 120° (EVO)
|
||||
private double exhValveOpenEnd = 480.0 * Math.PI / 180.0; // 480° (EVC)
|
||||
private double exhValveRampWidth = 30.0 * Math.PI / 180.0;
|
||||
public double ExhaustOrificeArea => ExhaustValveLift() * exhMaxOrificeArea;
|
||||
|
||||
// ---- Intake valve ----
|
||||
private double intMaxOrificeArea;
|
||||
private double intValveOpenStart = 380.0 * Math.PI / 180.0; // 380° (IVO)
|
||||
private double intValveOpenEnd = 560.0 * Math.PI / 180.0; // 560° (IVC)
|
||||
private double intValveRampWidth = 30.0 * Math.PI / 180.0;
|
||||
public double IntakeOrificeArea => IntakeValveLift() * intMaxOrificeArea;
|
||||
|
||||
// ---- Combustion ----
|
||||
public double TargetPeakPressure { get; set; } = 50.0 * 101325.0;
|
||||
private const double PeakTemperature = 2500.0;
|
||||
private bool burnInProgress = false;
|
||||
private double burnStartAngle; // full cycle angle when ignition began
|
||||
private double burnStartAngle; // cycle angle (0–4π)
|
||||
private double burnDuration = 40.0 * Math.PI / 180.0;
|
||||
private double targetBurnEnergy;
|
||||
private double totalBurnMass;
|
||||
private double preIgnitionMass, preIgnitionInternalEnergy;
|
||||
|
||||
private Random rand = new Random();
|
||||
@@ -35,46 +45,64 @@ namespace FluidSim.Core
|
||||
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 pipeArea, int sampleRate)
|
||||
double exhPipeArea, double intPipeArea, int sampleRate)
|
||||
{
|
||||
this.crankshaft = crankshaft;
|
||||
this.bore = bore;
|
||||
this.stroke = stroke;
|
||||
conRodLength = 2.0 * stroke;
|
||||
this.compressionRatio = compressionRatio;
|
||||
maxOrificeArea = pipeArea;
|
||||
exhMaxOrificeArea = exhPipeArea;
|
||||
intMaxOrificeArea = intPipeArea;
|
||||
pistonArea = Math.PI / 4.0 * bore * bore;
|
||||
|
||||
V_disp = pistonArea * stroke;
|
||||
V_clear = V_disp / (compressionRatio - 1.0);
|
||||
|
||||
// Initial compressed charge at TDC (no burn)
|
||||
double T_bdc = 300.0;
|
||||
double p_bdc = 101325.0;
|
||||
// Start at BDC with fresh ambient charge
|
||||
double V_bdc = V_clear + V_disp;
|
||||
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
|
||||
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
|
||||
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, 1.4);
|
||||
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_clear, p_tdc, T_bdc * Math.Pow(V_bdc / V_clear, 1.4 - 1.0), sampleRate)
|
||||
Cylinder = new Volume0D(V_bdc, p_amb, T_amb, sampleRate)
|
||||
{
|
||||
Gamma = 1.4,
|
||||
GasConstant = 287.0
|
||||
};
|
||||
Cylinder.Volume = V_clear;
|
||||
Cylinder.Mass = freshMass;
|
||||
Cylinder.InternalEnergy = p_tdc * V_clear / (1.4 - 1.0);
|
||||
Cylinder.Volume = V_bdc;
|
||||
Cylinder.Mass = mass0;
|
||||
Cylinder.InternalEnergy = energy0;
|
||||
|
||||
prevCycleAngle = CycleAngle;
|
||||
|
||||
preIgnitionMass = Cylinder.Mass;
|
||||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||||
}
|
||||
|
||||
// ---- Piston kinematics (uses full cycle angle for position) ----
|
||||
// ---- Piston kinematics ----
|
||||
private (double volume, double dvdt) PistonKinematics(double cycleAngle)
|
||||
{
|
||||
// Slider-crank uses 0–2π, but we want the same motion for 0–2π (power/exhaust) and 2π–4π (intake/compression)
|
||||
double theta = cycleAngle % (2.0 * Math.PI);
|
||||
double R = stroke / 2.0;
|
||||
double cosT = Math.Cos(theta);
|
||||
@@ -90,26 +118,34 @@ namespace FluidSim.Core
|
||||
return (V, dvdt);
|
||||
}
|
||||
|
||||
// ---- Valve lift ----
|
||||
private double ValveLift()
|
||||
// ---- Valve lifts (cycle‑aware) ----
|
||||
private double ExhaustValveLift()
|
||||
{
|
||||
double cycleRad = crankshaft.CrankAngle;
|
||||
if (cycleRad < valveOpenStart || cycleRad > valveOpenEnd)
|
||||
return 0.0;
|
||||
|
||||
double duration = valveOpenEnd - valveOpenStart;
|
||||
double ramp = valveRampWidth;
|
||||
double t = (cycleRad - valveOpenStart) / duration;
|
||||
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;
|
||||
else if (t > 1.0 - rampFrac)
|
||||
return (1.0 - t) / rampFrac;
|
||||
else
|
||||
return 1.0;
|
||||
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;
|
||||
@@ -137,33 +173,24 @@ namespace FluidSim.Core
|
||||
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)
|
||||
{
|
||||
double cycleAngle = crankshaft.CrankAngle;
|
||||
double prevAngle = crankshaft.PreviousAngle;
|
||||
|
||||
// ----- TDC crossing detection (power stroke) -----
|
||||
// Power stroke TDC occurs at angle 0 (mod 4π). We detect when PreviousAngle was near 4π and CrankAngle wraps to near 0.
|
||||
bool crossingTDC = (prevAngle > 3.8 * Math.PI && cycleAngle < 0.2 * Math.PI) // normal forward
|
||||
|| (prevAngle < 0.2 * Math.PI && cycleAngle > 3.8 * Math.PI); // (rare backward, ignore)
|
||||
bool crossingTDC = DetectTDCPowerStroke();
|
||||
|
||||
if (crossingTDC)
|
||||
{
|
||||
misfireCurrent = rand.NextDouble() < MisfireProbability;
|
||||
|
||||
// Fresh charge: trapped at BDC, compressed isentropically to V_clear
|
||||
double T_bdc = 300.0;
|
||||
double p_bdc = 101325.0;
|
||||
double V_bdc = V_clear + V_disp;
|
||||
double freshMass = p_bdc * V_bdc / (287.0 * T_bdc);
|
||||
double freshInternalEnergy = p_bdc * V_bdc / (1.4 - 1.0);
|
||||
double gamma = 1.4;
|
||||
double p_tdc = p_bdc * Math.Pow(V_bdc / V_clear, gamma);
|
||||
|
||||
Cylinder.Volume = V_clear;
|
||||
Cylinder.Mass = freshMass;
|
||||
Cylinder.InternalEnergy = p_tdc * V_clear / (gamma - 1.0);
|
||||
|
||||
// *** Always capture the state at TDC, whether we burn or not ***
|
||||
preIgnitionMass = Cylinder.Mass;
|
||||
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
|
||||
|
||||
@@ -171,43 +198,48 @@ namespace FluidSim.Core
|
||||
{
|
||||
MisfireCount++;
|
||||
}
|
||||
else
|
||||
else if (ignition)
|
||||
{
|
||||
double V = V_clear;
|
||||
targetBurnEnergy = TargetPeakPressure * V / (gamma - 1.0);
|
||||
totalBurnMass = TargetPeakPressure * V / (287.0 * PeakTemperature);
|
||||
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;
|
||||
burnStartAngle = CycleAngle;
|
||||
CombustionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Burn progress -----
|
||||
if (burnInProgress)
|
||||
{
|
||||
double angleFromIgnition = cycleAngle - burnStartAngle;
|
||||
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI; // wrap if needed
|
||||
double angleFromIgnition = CycleAngle - burnStartAngle;
|
||||
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI;
|
||||
|
||||
if (angleFromIgnition >= burnDuration)
|
||||
{
|
||||
Cylinder.Mass = totalBurnMass;
|
||||
Cylinder.InternalEnergy = targetBurnEnergy;
|
||||
burnInProgress = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double fraction = WiebeFraction(angleFromIgnition);
|
||||
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction) + targetBurnEnergy * fraction;
|
||||
Cylinder.Mass = preIgnitionMass * (1.0 - fraction) + totalBurnMass * fraction;
|
||||
Cylinder.InternalEnergy = preIgnitionInternalEnergy * (1.0 - fraction)
|
||||
+ targetBurnEnergy * fraction;
|
||||
Cylinder.Mass = preIgnitionMass;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Piston motion -----
|
||||
var (vol, dvdt) = PistonKinematics(cycleAngle);
|
||||
var (vol, dvdt) = PistonKinematics(CycleAngle);
|
||||
Cylinder.Volume = vol;
|
||||
Cylinder.Dvdt = dvdt;
|
||||
|
||||
// ----- Torque contribution -----
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user