Files
FluidSim/Components/TwoStrokeCylinder.cs

192 lines
7.8 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.
// ============================================================
// File: TwoStrokeCylinder.cs
// ============================================================
using System;
using FluidSim.Interfaces;
using FluidSim.Components; // for Crankcase (if in same namespace)
namespace FluidSim.Components
{
/// <summary>
/// Twostroke cylinder with forced symmetrical port timings around BDC (180°).
/// Uses crankcase backpressure 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 backpressure -----
public new void PreStep(float dt)
{
// Speeddependent 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 backpressure, 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;
}
}
}