using System; namespace FluidSim.Components { /// /// Two‑stroke cylinder with forced symmetrical port timings around BDC (180°). /// All angles are in degrees within a 360° cycle. /// 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); /// /// Create a two‑stroke cylinder with forced symmetrical port timing. /// /// Total transfer port open duration in degrees (e.g. 120°). /// Total exhaust port open duration in degrees (e.g. 180°). 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; } } } } }