// ============================================================ // File: TwoStrokeCylinder.cs // ============================================================ using System; using FluidSim.Interfaces; using FluidSim.Components; // for Crankcase (if in same namespace) namespace FluidSim.Components { /// /// Two‑stroke cylinder with forced symmetrical port timings around BDC (180°). /// Uses crankcase back‑pressure for accurate pumping work. /// 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; } } }