Files
FluidSim/Components/Cylinder.cs
2026-05-08 00:38:26 +02:00

334 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using FluidSim.Interfaces;
namespace FluidSim.Components
{
public class Cylinder : IComponent
{
public Port IntakePort { get; }
public Port ExhaustPort { get; }
public Crankshaft Crankshaft { get; }
private readonly Port[] _ports;
IReadOnlyList<Port> IComponent.Ports => _ports;
// Geometry
public double Bore { get; }
public double Stroke { get; }
public double ConRodLength { get; }
public double CompressionRatio { get; }
// Valve timings (degrees, 0 = TDC compression, 720° full cycle)
public double IVO { get; }
public double IVC { get; }
public double EVO { get; }
public double EVC { get; }
// Valve geometry
public double IntakeValveDiameter { get; set; } = 0.030;
public double ExhaustValveDiameter { get; set; } = 0.028;
public double IntakeValveLift { get; set; } = 0.005;
public double ExhaustValveLift { get; set; } = 0.005;
public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift;
public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift;
// Ignition and combustion
public double SparkAdvance { get; set; } = 20.0;
public double WiebeA { get; set; } = 5.0;
public double WiebeM { get; set; } = 2.0;
public double WiebeDuration { get; set; } = 60.0;
public double WiebeStart { get; set; } = 5.0;
// Fuel
public double StoichiometricAFR { get; set; } = 14.7;
public double FuelLowerHeatingValue { get; set; } = 44e6;
// Cycletocycle randomness
public double EnergyVariationFraction { get; set; } = 0.05;
public double MisfireProbability { get; set; } = 0.01;
// Heat loss
public double CylinderWallArea { get; set; } = 0.02;
public double HeatTransferCoefficient { get; set; } = 100.0;
public double AmbientTemperature { get; set; } = 300.0;
// ---- Multicylinder support ----
/// <summary>
/// Phase offset (radians) added to the crankshaft angle for this cylinder.
/// Used for multicylinder engines; set to 0 for singlecylinder.
/// </summary>
public double PhaseOffset { get; set; } = 0.0;
// State (public for drawing)
public double Volume => cylinderVolume;
public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double Density => Mass / Math.Max(cylinderVolume, 1e-12);
public double Mass => _airMass + _exhaustMass;
public double AirFraction => _airMass / Math.Max(Mass, 1e-12);
public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;
private double cylinderVolume;
private double cylinderEnergy;
private double _airMass;
private double _exhaustMass;
private double trappedAirMass;
private double fuelMass;
private double burnFraction;
private bool combustionActive;
private bool fuelInjected;
private double _energyFactor = 1.0;
private readonly Random _random = new Random();
private const double Gamma = 1.4;
private const double GasConstant = 287.0;
private const double MaxPressurePa = 200e5;
private const double MaxTemperatureK = 3500.0;
public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
double ivo, double ivc, double evo, double evc, Crankshaft crankshaft)
{
Bore = bore;
Stroke = stroke;
ConRodLength = conRodLength;
CompressionRatio = compressionRatio;
IVO = ivo;
IVC = ivc;
EVO = evo;
EVC = evc;
Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft));
cylinderVolume = clearanceVolume;
double initRho = 1.225;
_airMass = initRho * clearanceVolume;
_exhaustMass = 0.0;
cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0);
IntakePort = new Port { Owner = this };
ExhaustPort = new Port { Owner = this };
_ports = new[] { IntakePort, ExhaustPort };
}
// Derived volumes
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
private double CrankRadius => Stroke / 2.0;
private double Obliquity => CrankRadius / ConRodLength;
// Offset-aware crank angle in degrees
private double CrankDeg =>
((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
public double ComputeVolume(double thetaRad)
{
double r = CrankRadius;
double l = ConRodLength;
double cosTh = Math.Cos(thetaRad);
double sinTh = Math.Sin(thetaRad);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
double x = r * (1.0 - cosTh) + l * (1.0 - term);
double area = Math.PI * 0.25 * Bore * Bore;
return clearanceVolume + area * x;
}
private double ValveLift(double thetaDeg, double opens, double closes, double peakLift)
{
double deg = thetaDeg % 720.0;
if (deg < 0) deg += 720.0;
double duration = closes - opens;
if (duration <= 0) return 0.0;
double rampDur = duration * 0.25;
double holdDur = duration - 2.0 * rampDur;
if (deg >= opens && deg < opens + rampDur)
{
double t = (deg - opens) / rampDur;
return peakLift * t * t * (3.0 - 2.0 * t);
}
else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur)
{
return peakLift;
}
else if (deg >= opens + rampDur + holdDur && deg <= closes)
{
double t = (deg - (opens + rampDur + holdDur)) / rampDur;
return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t);
}
return 0.0;
}
public double IntakeValveArea =>
Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
public double ExhaustValveArea =>
Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
private double Wiebe(double angleSinceSpark)
{
if (angleSinceSpark < WiebeStart) return 0.0;
double phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
if (phi <= 0) return 0.0;
return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1));
}
public void PreStep(double dt)
{
double prevVolume = cylinderVolume;
// ----- Use phaseoffset crank angle for this cylinder -----
double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
cylinderVolume = ComputeVolume(crankAngleRad);
double dV = cylinderVolume - prevVolume;
// Piston torque
double pRel = Pressure - 101325.0;
double sinTh = Math.Sin(crankAngleRad);
double cosTh = Math.Cos(crankAngleRad);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
double dxdtheta = CrankRadius * sinTh * (1.0 + Obliquity * cosTh / term);
double pistonArea = Math.PI * 0.25 * Bore * Bore;
double torque = pRel * pistonArea * dxdtheta;
Crankshaft.AddTorque(torque);
cylinderEnergy -= Pressure * dV;
// Also use offset angle for event detection
double crankshaftPrevAngle = Crankshaft.PreviousAngle;
double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0;
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
// ----- Intake closing: capture trapped air mass and compute fuel -----
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
{
trappedAirMass = _airMass;
fuelMass = trappedAirMass / StoichiometricAFR;
fuelInjected = true;
}
// ----- Spark ignition -----
double sparkAngle = 0.0 - SparkAdvance;
if (sparkAngle < 0) sparkAngle += 720.0;
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
if (crossedSpark && !combustionActive && fuelInjected)
{
bool misfire = _random.NextDouble() < MisfireProbability;
if (misfire)
{
combustionActive = false;
}
else
{
combustionActive = true;
burnFraction = 0.0;
double range = EnergyVariationFraction;
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
}
}
// ----- Combustion progress -----
if (combustionActive)
{
double angleSinceSpark = currDeg - sparkAngle;
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
double newFraction = Wiebe(angleSinceSpark);
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
{
newFraction = 1.0;
combustionActive = false;
double totalMass = _airMass + _exhaustMass;
_airMass = 0.0;
_exhaustMass = totalMass;
}
double dFraction = newFraction - burnFraction;
if (dFraction > 0)
{
double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
cylinderEnergy += dQ;
_exhaustMass += fuelMass * dFraction;
burnFraction = newFraction;
}
}
// ----- Heat loss -----
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
(Temperature - AmbientTemperature) * dt;
cylinderEnergy -= dQ_loss;
// Update port states
double p = Pressure, rho = Density, T = Temperature;
double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12);
double af = AirFraction;
IntakePort.Pressure = p;
IntakePort.Density = rho;
IntakePort.Temperature = T;
IntakePort.SpecificEnthalpy = h;
IntakePort.AirFraction = af;
ExhaustPort.Pressure = p;
ExhaustPort.Density = rho;
ExhaustPort.Temperature = T;
ExhaustPort.SpecificEnthalpy = h;
ExhaustPort.AirFraction = af;
}
public void UpdateState(double dt)
{
double dmAir = 0.0, dmExhaust = 0.0, dE = 0.0;
foreach (var port in _ports)
{
double mdot = port.MassFlowRate;
double af = mdot >= 0 ? port.AirFraction : AirFraction;
dmAir += mdot * af * dt;
dmExhaust += mdot * (1.0 - af) * dt;
dE += mdot * port.SpecificEnthalpy * dt;
}
_airMass += dmAir;
_exhaustMass += dmExhaust;
cylinderEnergy += dE;
double V = Math.Max(cylinderVolume, 1e-12);
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
if (currentP > MaxPressurePa)
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
double currentRho = (_airMass + _exhaustMass) / V;
double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
if (currentT > MaxTemperatureK)
{
double pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
cylinderEnergy = pAtTlimit * V / (Gamma - 1.0);
}
double totalMass = _airMass + _exhaustMass;
if (totalMass < 1e-9)
{
_airMass = 1e-9;
_exhaustMass = 0.0;
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
}
else if (cylinderEnergy < 0.0)
{
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
}
if (_airMass < 0.0) _airMass = 0.0;
if (_exhaustMass < 0.0) _exhaustMass = 0.0;
}
}
}