This commit is contained in:
2026-05-05 14:02:07 +02:00
parent f16a1aa763
commit 547e8706f1
6 changed files with 425 additions and 355 deletions

52
Components/Crankshaft.cs Normal file
View File

@@ -0,0 +1,52 @@
// Components/Crankshaft.cs
using System;
namespace FluidSim.Components
{
public class Crankshaft
{
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 Inertia { get; set; } = 0.2;
public double FrictionConstant { get; set; } = 2.0; // N·m
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
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;
CrankAngle = 0.0;
PreviousAngle = 0.0;
}
public void AddTorque(double torque) => externalTorque += torque;
public void Step(double dt)
{
// Save previous angle
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
CrankAngle += AngularVelocity * dt;
// Wrap to [0, 4π)
if (CrankAngle >= 4.0 * Math.PI)
CrankAngle -= 4.0 * Math.PI;
else if (CrankAngle < 0)
CrankAngle += 4.0 * Math.PI;
externalTorque = 0.0;
}
}
}

View File

@@ -0,0 +1,215 @@
// EngineCylinder.cs (in Core namespace)
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;
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 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 burnDuration = 40.0 * Math.PI / 180.0;
private double targetBurnEnergy;
private double totalBurnMass;
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; }
public EngineCylinder(Crankshaft crankshaft,
double bore, double stroke, double compressionRatio,
double pipeArea, int sampleRate)
{
this.crankshaft = crankshaft;
this.bore = bore;
this.stroke = stroke;
conRodLength = 2.0 * stroke;
this.compressionRatio = compressionRatio;
maxOrificeArea = pipeArea;
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;
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);
Cylinder = new Volume0D(V_clear, p_tdc, T_bdc * Math.Pow(V_bdc / V_clear, 1.4 - 1.0), sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
Cylinder.Volume = V_clear;
Cylinder.Mass = freshMass;
Cylinder.InternalEnergy = p_tdc * V_clear / (1.4 - 1.0);
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
}
// ---- Piston kinematics (uses full cycle angle for position) ----
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);
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 lift ----
private double ValveLift()
{
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 rampFrac = ramp / duration;
if (t < rampFrac)
return t / rampFrac;
else if (t > 1.0 - rampFrac)
return (1.0 - t) / rampFrac;
else
return 1.0;
}
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;
}
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)
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);
preIgnitionMass = Cylinder.Mass;
preIgnitionInternalEnergy = Cylinder.InternalEnergy;
if (misfireCurrent)
{
MisfireCount++;
}
else
{
double V = V_clear;
targetBurnEnergy = TargetPeakPressure * V / (gamma - 1.0);
totalBurnMass = TargetPeakPressure * V / (287.0 * PeakTemperature);
burnInProgress = true;
burnStartAngle = cycleAngle;
CombustionCount++;
}
}
// ----- Burn progress -----
if (burnInProgress)
{
double angleFromIgnition = cycleAngle - burnStartAngle;
if (angleFromIgnition < 0) angleFromIgnition += 4.0 * Math.PI; // wrap if needed
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;
}
}
// ----- Piston motion -----
var (vol, dvdt) = PistonKinematics(cycleAngle);
Cylinder.Volume = vol;
Cylinder.Dvdt = dvdt;
// ----- Torque contribution -----
double torque = ComputeTorque();
crankshaft.AddTorque(torque);
}
}
}

View File

@@ -53,6 +53,10 @@ namespace FluidSim.Components
// Precomputed for damping
private float _laminarCoeff; // 8*mu / r^2, then multiplied by DampingMultiplier
// ---- Energy loss (Newton cooling) ----
private float _ambientEnergyReference; // total energy density at ambient (Pamb / (γ-1))
public float EnergyRelaxationRate { get; set; } = 0.0f; // 1/s
public Pipe1D(double length, double area, int sampleRate, int forcedCellCount = 0)
{
float dtGlobal = 1f / sampleRate;
@@ -89,12 +93,14 @@ namespace FluidSim.Components
float radius = _diameter * 0.5f;
_laminarCoeff = 8f * mu_air / (radius * radius); // will be multiplied by DampingMultiplier at each step
// Ambient reference energy (internal energy per unit volume at 101325 Pa)
_ambientEnergyReference = 101325f / (_gamma - 1f); // ≈ 253312.5 J/m³
PortA = new Port();
PortB = new Port();
}
// ==================== PUBLIC API (unchanged) ============================
// ==================== PUBLIC API ============================
public void SetABoundaryType(BoundaryType type) => _aBCType = type;
public void SetBBoundaryType(BoundaryType type) => _bBCType = type;
public void SetAAmbientPressure(double p) => _aAmbientPressure = (float)p;
@@ -237,8 +243,6 @@ namespace FluidSim.Components
// --- 2. Internal faces (1 .. n-1) SIMD ---------------------------
int vectorSize = Vector<float>.Count;
int lastSimdFace = n - vectorSize; // highest face index that starts a full vector block
// Face index f is between cell f-1 (left) and cell f (right).
// We want to cover faces 1..n-1.
for (int f = 1; f <= lastSimdFace; f += vectorSize)
{
SimdInternalFluxBlock(f, vectorSize);
@@ -262,7 +266,7 @@ namespace FluidSim.Components
float pR = PressureScalar(n - 1);
ComputeRightBoundaryFlux(rhoR, uR, pR, out _fluxM[n], out _fluxP[n], out _fluxE[n]);
// --- 4. Cell update + damping SIMD ------------------------------
// --- 4. Cell update + damping + energy loss SIMD -----------------
SimdCellUpdate(dt);
}
@@ -417,14 +421,9 @@ namespace FluidSim.Components
// ==================== SIMD INTERNAL FACE ROUTINE ========================
private void SimdInternalFluxBlock(int startFace, int count)
{
// startFace is the first face index; we process 'count' consecutive faces.
// For face f, left cell = f-1, right cell = f.
// We load left and right states for faces [startFace .. startFace+count-1].
int leftIdx = startFace - 1;
int rightIdx = startFace;
// Load conserved variables for left cells and right cells as vectors.
Vector<float> rL = new Vector<float>(_rho, leftIdx);
Vector<float> ruL = new Vector<float>(_rhou, leftIdx);
Vector<float> EL = new Vector<float>(_E, leftIdx);
@@ -433,7 +432,6 @@ namespace FluidSim.Components
Vector<float> ruR = new Vector<float>(_rhou, rightIdx);
Vector<float> ER = new Vector<float>(_E, rightIdx);
// Derived quantities: u = ru / r, p = (gamma-1)*(E - 0.5*ru^2 / r)
Vector<float> uL = ruL / rL;
Vector<float> uR = ruR / rR;
@@ -444,35 +442,30 @@ namespace FluidSim.Components
Vector<float> pL = gammaMinus1 * (EL - half * ruL * ruL / rL);
Vector<float> pR = gammaMinus1 * (ER - half * ruR * ruR / rR);
// Sound speeds
Vector<float> cL = Vector.SquareRoot(gammaVec * pL / rL);
Vector<float> cR = Vector.SquareRoot(gammaVec * pR / rR);
// Wave speeds
Vector<float> SL = Vector.Min(uL - cL, uR - cR);
Vector<float> SR = Vector.Max(uL + cL, uR + cR);
// Star speed
Vector<float> num = (pR - pL) + rL * uL * (SL - uL) - rR * uR * (SR - uR);
Vector<float> den = rL * (SL - uL) - rR * (SR - uR);
Vector<float> Ss = num / den;
// Total energy per unit mass (E/rho) for left/right (needed for star region)
Vector<float> eL = EL / rL;
Vector<float> eR = ER / rR;
// --- Compute all four possible flux vectors ---
// Left flux
Vector<float> Fm_L = ruL;
Vector<float> Fp_L = ruL * uL + pL;
Vector<float> Fe_L = (EL + pL) * uL; // (r*E + p)*u
Vector<float> Fe_L = (EL + pL) * uL;
// Right flux
Vector<float> Fm_R = ruR;
Vector<float> Fp_R = ruR * uR + pR;
Vector<float> Fe_R = (ER + pR) * uR;
// Starleft fluxes (when SL < 0 < Ss)
// Starleft fluxes
Vector<float> diffL = SL - uL;
Vector<float> dL_den = SL - Ss;
Vector<float> rsL = rL * diffL / dL_den;
@@ -482,7 +475,7 @@ namespace FluidSim.Components
Vector<float> Fp_starL = rsL * Ss * Ss + psSL;
Vector<float> Fe_starL = (rsL * EsL + psSL) * Ss;
// Starright fluxes (when Ss < 0 < SR)
// Starright fluxes
Vector<float> diffR = SR - uR;
Vector<float> dR_den = SR - Ss;
Vector<float> rsR = rR * diffR / dR_den;
@@ -492,14 +485,12 @@ namespace FluidSim.Components
Vector<float> Fp_starR = rsR * Ss * Ss + psSR;
Vector<float> Fe_starR = (rsR * EsR + psSR) * Ss;
// --- Select the correct flux based on wave signs ---
var maskSLge0 = Vector.GreaterThanOrEqual(SL, Vector<float>.Zero);
var maskSRle0 = Vector.LessThanOrEqual(SR, Vector<float>.Zero);
var maskMiddle = ~(maskSLge0 | maskSRle0); // SL<0 && SR>0
var maskMiddle = ~(maskSLge0 | maskSRle0);
var maskStarL = maskMiddle & Vector.GreaterThanOrEqual(Ss, Vector<float>.Zero);
var maskStarR = maskMiddle & Vector.LessThan(Ss, Vector<float>.Zero);
// Start with left flux, override with right/star as needed
Vector<float> fm = Vector.ConditionalSelect(maskSLge0, Fm_L,
Vector.ConditionalSelect(maskSRle0, Fm_R,
Vector.ConditionalSelect(maskStarL, Fm_starL,
@@ -515,13 +506,12 @@ namespace FluidSim.Components
Vector.ConditionalSelect(maskStarL, Fe_starL,
Vector.ConditionalSelect(maskStarR, Fe_starR, Vector<float>.Zero))));
// Store to flux arrays at indices startFace .. startFace+count-1
fm.CopyTo(_fluxM, startFace);
fp.CopyTo(_fluxP, startFace);
fe.CopyTo(_fluxE, startFace);
}
// ==================== SIMD CELL UPDATE + DAMPING ========================
// ==================== SIMD CELL UPDATE + DAMPING + ENERGY LOSS =========
private void SimdCellUpdate(float dt)
{
float dt_dx = dt / _dx;
@@ -534,9 +524,11 @@ namespace FluidSim.Components
int n = _n;
int lastSimdCell = n - vectorSize;
// Predefine constants used in clamping
// Predefined constants used in clamping
Vector<float> half = new Vector<float>(0.5f);
Vector<float> gammaMinus1 = new Vector<float>(_gamma - 1f);
Vector<float> ambientEnergyVec = new Vector<float>(_ambientEnergyReference);
Vector<float> energyRelaxRateVec = new Vector<float>(EnergyRelaxationRate);
for (int i = 0; i <= lastSimdCell; i += vectorSize)
{
@@ -559,8 +551,12 @@ namespace FluidSim.Components
Vector<float> newE = E - vDtDx * (flxE_R - flxE_L);
// Damping
Vector<float> factor = Vector.Exp(-vCoeff / r * vDt);
newRu *= factor;
Vector<float> dampingFactor = Vector.Exp(-vCoeff / r * vDt);
newRu *= dampingFactor;
// Energy loss (Newton cooling toward ambient)
Vector<float> relaxFactor = Vector.Exp(-energyRelaxRateVec * vDt);
newE = ambientEnergyVec + (newE - ambientEnergyVec) * relaxFactor;
// Clamp density
newR = Vector.Max(newR, new Vector<float>(1e-12f));
@@ -576,6 +572,7 @@ namespace FluidSim.Components
}
// Scalar remainder
float relaxRate = EnergyRelaxationRate;
for (int i = Math.Max(0, lastSimdCell + 1); i < n; i++)
{
float r = _rho[i];
@@ -584,15 +581,19 @@ namespace FluidSim.Components
float dM = _fluxM[i + 1] - _fluxM[i];
float dP = _fluxP[i + 1] - _fluxP[i];
float dE = _fluxE[i + 1] - _fluxE[i];
float dE_flux = _fluxE[i + 1] - _fluxE[i];
float newR = r - dt_dx * dM;
float newRu = ru - dt_dx * dP;
float newE = E - dt_dx * dE;
float newE = E - dt_dx * dE_flux;
// Damping
float factor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
newRu *= factor;
float dampingFactor = MathF.Exp(-coeff / Math.Max(r, 1e-12f) * dt);
newRu *= dampingFactor;
// Energy loss
float relaxFactor = MathF.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
// Clamps
if (newR < 1e-12f) newR = 1e-12f;