added two stroke scenario with vehicle
This commit is contained in:
140
Components/TwoStrokeCylinder.cs
Normal file
140
Components/TwoStrokeCylinder.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
|
||||
namespace FluidSim.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Two‑stroke cylinder with forced symmetrical port timings around BDC (180°).
|
||||
/// All angles are in degrees within a 360° cycle.
|
||||
/// </summary>
|
||||
public class TwoStrokeCylinder : EngineCylinder
|
||||
{
|
||||
// --- Public read‑only properties for drawing ---
|
||||
public float IVO => 180f - transferDuration / 2f;
|
||||
public float IVC => 180f + transferDuration / 2f;
|
||||
public float EVO => 180f - exhaustDuration / 2f;
|
||||
public float EVC => 180f + exhaustDuration / 2f;
|
||||
|
||||
// --- Configurable durations (set in constructor) ---
|
||||
private readonly float transferDuration; // e.g. 120°
|
||||
private readonly float exhaustDuration; // e.g. 180°
|
||||
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Create a two‑stroke cylinder with forced symmetrical port timing.
|
||||
/// </summary>
|
||||
/// <param name="transferDuration">Total transfer port open duration in degrees (e.g. 120°).</param>
|
||||
/// <param name="exhaustDuration">Total exhaust port open duration in degrees (e.g. 180°).</param>
|
||||
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;
|
||||
|
||||
// Safety check: exhaust must open before transfer
|
||||
if (EVO >= IVO)
|
||||
throw new ArgumentException("Exhaust must open before transfer port (exhaust duration > transfer duration).");
|
||||
}
|
||||
|
||||
// ----- Valve lift – same implementation, now uses the computed IVO/IVC/EVO/EVC -----
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user