This commit is contained in:
2026-05-05 19:39:11 +02:00
parent 608dabff12
commit d6b1d214f5
11 changed files with 493 additions and 277 deletions

View File

@@ -7,7 +7,7 @@ namespace FluidSim.Components
{
public double AngularVelocity { get; set; } // rad/s
public double CrankAngle { get; set; } // rad, 0 … 4π (fourstroke cycle)
public double PreviousAngle { get; private set; } // for TDC detection
public double PreviousAngle { get; set; } // ← now has public setter
public double Inertia { get; set; } = 0.2;
public double FrictionConstant { get; set; } = 2.0; // N·m
@@ -15,7 +15,6 @@ namespace FluidSim.Components
private double externalTorque;
/// <param name="initialRPM">Idle speed before any combustion torque is applied.</param>
public Crankshaft(double initialRPM = 400.0)
{
AngularVelocity = initialRPM * 2.0 * Math.PI / 60.0;
@@ -27,20 +26,23 @@ namespace FluidSim.Components
public void Step(double dt)
{
// Save previous angle
// Catch NaN before it propagates
if (double.IsNaN(AngularVelocity) || double.IsInfinity(AngularVelocity))
AngularVelocity = 0.0;
if (double.IsNaN(externalTorque) || double.IsInfinity(externalTorque))
externalTorque = 0.0;
PreviousAngle = CrankAngle;
// Friction
double friction = FrictionConstant * Math.Sign(AngularVelocity) + FrictionViscous * AngularVelocity;
double netTorque = externalTorque - friction;
double alpha = netTorque / Inertia;
AngularVelocity += alpha * dt;
if (AngularVelocity < 0) AngularVelocity = 0; // stall
if (AngularVelocity < 0) AngularVelocity = 0;
CrankAngle += AngularVelocity * dt;
// Wrap to [0, 4π)
if (CrankAngle >= 4.0 * Math.PI)
CrankAngle -= 4.0 * Math.PI;
else if (CrankAngle < 0)

View File

@@ -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 (04π)
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; }
// Cycleaware 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 02π, but we want the same motion for 02π (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 (cycleaware) ----
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);
}

View File

@@ -106,6 +106,12 @@ namespace FluidSim.Components
public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p;
public void SetBAmbientPressure(double p) => _bAmbientPressure = (float)p;
public float GetFaceMassFlux(int faceIndex)
{
if (faceIndex < 0 || faceIndex > _n) return 0f;
return _fluxM[faceIndex];
}
public void SetGhostLeft(double rho, double u, double p)
{
_rhoGhostL = (float)rho;

View File

@@ -1,4 +1,4 @@
using System;
using System;
namespace FluidSim.Components
{
@@ -15,10 +15,10 @@ namespace FluidSim.Components
private double _dt;
public double Density => Mass / Volume;
public double Pressure => (Gamma - 1.0) * InternalEnergy / Volume;
public double Temperature => Pressure / (Density * GasConstant);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Density;
public double Density => Mass / Math.Max(Volume, 1e-12);
public double Pressure => (Gamma - 1.0) * InternalEnergy / Math.Max(Volume, 1e-12);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double SpecificEnthalpy => Gamma / (Gamma - 1.0) * Pressure / Math.Max(Density, 1e-12);
public double MassFlowRateIn { get; set; }
public double SpecificEnthalpyIn { get; set; }
@@ -46,18 +46,20 @@ namespace FluidSim.Components
Mass += dm;
InternalEnergy += dE;
// Safety: if mass becomes extremely small, reset internal energy to zero
if (Mass < 1e-12)
// ---- ABSOLUTE SAFEGUARD: keep at least 1 µg of gas at ambient pressure ----
double minMass = 1e-9;
double V = Math.Max(Volume, 1e-12);
if (Mass < minMass)
{
Mass = 0.0;
InternalEnergy = 0.0;
Mass = minMass;
InternalEnergy = 5000.0 * V / (Gamma - 1.0); // 0.05 bar, not ambient
}
else if (InternalEnergy < 1e-12)
else if (InternalEnergy < 0.0)
{
InternalEnergy = 0.0;
InternalEnergy = 101325.0 * V / (Gamma - 1.0);
}
// Avoid negative mass/energy
// Final nonnegative clamp
if (Mass < 0.0) Mass = 0.0;
if (InternalEnergy < 0.0) InternalEnergy = 0.0;
}