192 lines
7.8 KiB
C#
192 lines
7.8 KiB
C#
// ============================================================
|
||
// File: TwoStrokeCylinder.cs
|
||
// ============================================================
|
||
using System;
|
||
using FluidSim.Interfaces;
|
||
using FluidSim.Components; // for Crankcase (if in same namespace)
|
||
|
||
namespace FluidSim.Components
|
||
{
|
||
/// <summary>
|
||
/// Two‑stroke cylinder with forced symmetrical port timings around BDC (180°).
|
||
/// Uses crankcase back‑pressure for accurate pumping work.
|
||
/// </summary>
|
||
public class TwoStrokeCylinder : EngineCylinder
|
||
{
|
||
// --- Port timing (computed from durations) ---
|
||
public float IVO => 180f - transferDuration / 2f;
|
||
public float IVC => 180f + transferDuration / 2f;
|
||
public float EVO => 180f - exhaustDuration / 2f;
|
||
public float EVC => 180f + exhaustDuration / 2f;
|
||
|
||
private readonly float transferDuration; // degrees
|
||
private readonly float exhaustDuration; // degrees
|
||
|
||
// --- Crankcase reference ---
|
||
private Crankcase? _crankcase;
|
||
|
||
protected override float CycleLengthRad => 2f * MathF.PI;
|
||
protected override float MaxCycleDeg => 360f;
|
||
|
||
public override float IntakeValveArea =>
|
||
MathF.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift);
|
||
public override float ExhaustValveArea =>
|
||
MathF.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift);
|
||
|
||
public TwoStrokeCylinder(float bore, float stroke, float conRodLength,
|
||
float compressionRatio,
|
||
float transferDuration, float exhaustDuration,
|
||
Crankshaft crankshaft)
|
||
: base(bore, stroke, conRodLength, compressionRatio, crankshaft)
|
||
{
|
||
this.transferDuration = transferDuration;
|
||
this.exhaustDuration = exhaustDuration;
|
||
|
||
if (EVO >= IVO)
|
||
throw new ArgumentException("Exhaust must open before transfer port.");
|
||
}
|
||
|
||
public void SetCrankcase(Crankcase crankcase)
|
||
{
|
||
_crankcase = crankcase;
|
||
}
|
||
|
||
// ----- Valve lift -----
|
||
private float ValveLift(float thetaDeg, float opens, float closes, float peakLift)
|
||
{
|
||
float deg = thetaDeg % 360f;
|
||
if (deg < 0f) deg += 360f;
|
||
|
||
float effectiveOpen = opens;
|
||
float effectiveClose = closes;
|
||
if (closes < opens) effectiveClose += 360f;
|
||
float duration = effectiveClose - effectiveOpen;
|
||
if (duration <= 0f) return 0f;
|
||
|
||
float mapped = deg;
|
||
if (mapped < opens) mapped += 360f;
|
||
if (mapped < opens || mapped > effectiveClose) return 0f;
|
||
|
||
float rampDur = duration * 0.25f;
|
||
float holdDur = duration - 2f * rampDur;
|
||
|
||
if (mapped >= opens && mapped < opens + rampDur)
|
||
{
|
||
float t = (mapped - opens) / rampDur;
|
||
return peakLift * t * t * (3f - 2f * t);
|
||
}
|
||
else if (mapped >= opens + rampDur && mapped < opens + rampDur + holdDur)
|
||
{
|
||
return peakLift;
|
||
}
|
||
else if (mapped >= opens + rampDur + holdDur && mapped <= effectiveClose)
|
||
{
|
||
float t = (mapped - (opens + rampDur + holdDur)) / rampDur;
|
||
return peakLift * (1f - t) * (1f - t) * (1f + 2f * t);
|
||
}
|
||
return 0f;
|
||
}
|
||
|
||
protected override void HandleCycleEvents(float prevDeg, float currDeg, float dt)
|
||
{
|
||
// Transfer port closing → fuel injection
|
||
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
|
||
{
|
||
trappedAirMass = _airMass;
|
||
fuelMass = trappedAirMass / StoichiometricAFR;
|
||
fuelInjected = true;
|
||
}
|
||
|
||
// Spark every 360° at TDC (0°) minus advance
|
||
float sparkAngle = (0f - SparkAdvance + 360f) % 360f;
|
||
bool crossedSpark = false;
|
||
if (prevDeg < sparkAngle && currDeg >= sparkAngle)
|
||
crossedSpark = true;
|
||
else if (prevDeg > sparkAngle && currDeg < sparkAngle)
|
||
crossedSpark = true;
|
||
|
||
if (crossedSpark && !combustionActive && fuelInjected)
|
||
{
|
||
if (_random.NextDouble() < MisfireProbability)
|
||
{
|
||
combustionActive = false;
|
||
}
|
||
else
|
||
{
|
||
combustionActive = true; burnFraction = 0f;
|
||
float range = EnergyVariationFraction;
|
||
_energyFactor = 1f + range * (2f * (float)_random.NextDouble() - 1f);
|
||
}
|
||
}
|
||
|
||
if (combustionActive)
|
||
{
|
||
float angleSinceSpark = currDeg - sparkAngle;
|
||
if (angleSinceSpark < 0f) angleSinceSpark += 360f;
|
||
float newFraction = Wiebe(angleSinceSpark);
|
||
if (newFraction >= 1f || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
|
||
{
|
||
newFraction = 1f; combustionActive = false;
|
||
float totalMass = _airMass + _exhaustMass;
|
||
_airMass = 0f; _exhaustMass = totalMass;
|
||
}
|
||
fuelInjected = false;
|
||
|
||
float dFraction = newFraction - burnFraction;
|
||
if (dFraction > 0f)
|
||
{
|
||
float dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction;
|
||
cylinderEnergy += dQ;
|
||
_exhaustMass += fuelMass * dFraction;
|
||
burnFraction = newFraction;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ----- Override torque calculation to use crankcase back‑pressure -----
|
||
public new void PreStep(float dt)
|
||
{
|
||
// Speed‑dependent spark advance
|
||
float rpm = Crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
|
||
SparkAdvance = Math.Clamp(10f + rpm * 0.002f, 5f, 40f);
|
||
|
||
float prevVolume = cylinderVolume;
|
||
float crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
|
||
cylinderVolume = ComputeVolume(crankAngleRad);
|
||
|
||
float dV = cylinderVolume - prevVolume;
|
||
|
||
// Use crankcase pressure as back‑pressure, ambient if not set
|
||
float backPressure = _crankcase?.Pressure ?? 101325f;
|
||
float pRel = Pressure - backPressure;
|
||
|
||
float sinTh = MathF.Sin(crankAngleRad), cosTh = MathF.Cos(crankAngleRad);
|
||
float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh);
|
||
float dxdtheta = CrankRadius * sinTh * (1f + Obliquity * cosTh / term);
|
||
float pistonArea = MathF.PI * 0.25f * Bore * Bore;
|
||
Crankshaft.AddTorque(pRel * pistonArea * dxdtheta);
|
||
|
||
cylinderEnergy -= Pressure * dV;
|
||
|
||
float cycleLenDeg = 360f;
|
||
float prevDeg = (Crankshaft.PreviousAngle + PhaseOffset) * 180f / MathF.PI % cycleLenDeg;
|
||
float currDeg = crankAngleRad * 180f / MathF.PI % cycleLenDeg;
|
||
|
||
HandleCycleEvents(prevDeg, currDeg, dt);
|
||
|
||
// Heat loss
|
||
float dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
||
(Temperature - AmbientTemperature) * dt;
|
||
cylinderEnergy -= dQ_loss;
|
||
|
||
// Update port states
|
||
float p = Pressure, rho = Density, T = Temperature;
|
||
float h = Gamma / (Gamma - 1f) * p / MathF.Max(rho, 1e-12f);
|
||
float 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;
|
||
}
|
||
}
|
||
} |