using System; using System.Collections.Generic; using FluidSim.Interfaces; namespace FluidSim.Components { /// Common base for all reciprocating engine cylinders. public abstract class EngineCylinder : IComponent { public Port IntakePort { get; } public Port ExhaustPort { get; } public Crankshaft Crankshaft { get; } private readonly Port[] _ports; IReadOnlyList IComponent.Ports => _ports; // ----- Geometry ----- public float Bore { get; } public float Stroke { get; } public float ConRodLength { get; } public float CompressionRatio { get; } // ----- Valve / port sizes (used for curtain area) ----- public float IntakeValveDiameter = 0.03f; public float ExhaustValveDiameter = 0.028f; public float IntakeValveLift = 0.005f; public float ExhaustValveLift = 0.005f; // ----- Combustion ----- public float SparkAdvance = 20f; public float WiebeA = 5f, WiebeM = 2f, WiebeDuration = 60f, WiebeStart = 5f; public float StoichiometricAFR = 14.7f; public float FuelLowerHeatingValue = 44e6f; public float EnergyVariationFraction = 0.05f; public float MisfireProbability = 0f; public float CylinderWallArea = 0.02f; public float HeatTransferCoefficient = 100f; public float AmbientTemperature = 300f; public float PhaseOffset; // radians // ----- State (public, used by drawing) ----- public float Volume => cylinderVolume; public float Pressure => (Gamma - 1f) * cylinderEnergy / MathF.Max(cylinderVolume, 1e-12f); public float Temperature => Pressure / MathF.Max(Density * GasConstant, 1e-12f); public float Density => Mass / MathF.Max(cylinderVolume, 1e-12f); public float Mass => _airMass + _exhaustMass; public float AirFraction => _airMass / MathF.Max(Mass, 1e-12f); public float PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume; protected float cylinderVolume, cylinderEnergy; protected float _airMass, _exhaustMass; protected float trappedAirMass, fuelMass, burnFraction; protected bool combustionActive, fuelInjected; protected float _energyFactor = 1f; protected readonly Random _random = new Random(); protected const float Gamma = 1.4f; protected const float GasConstant = 287f; protected const float MaxPressurePa = 200e5f; protected const float MaxTemperatureK = 3500f; // ----- Derived geometry (cycle‑independent) ----- protected float SweptVolume => MathF.PI * 0.25f * Bore * Bore * Stroke; protected float clearanceVolume => SweptVolume / (CompressionRatio - 1f); protected float CrankRadius => Stroke * 0.5f; protected float Obliquity => CrankRadius / ConRodLength; // ----- Abstract members (cycle‑specific) ----- protected abstract float CycleLengthRad { get; } // 4π or 2π protected abstract float MaxCycleDeg { get; } // 720 or 360 public abstract float IntakeValveArea { get; } public abstract float ExhaustValveArea { get; } protected abstract void HandleCycleEvents(float prevDeg, float currDeg, float dt); protected EngineCylinder(float bore, float stroke, float conRodLength, float compressionRatio, Crankshaft crankshaft) { Bore = bore; Stroke = stroke; ConRodLength = conRodLength; CompressionRatio = compressionRatio; Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft)); cylinderVolume = clearanceVolume; float initRho = 1.225f; _airMass = initRho * clearanceVolume; _exhaustMass = 0f; cylinderEnergy = 101325f * clearanceVolume / (Gamma - 1f); IntakePort = new Port { Owner = this }; ExhaustPort = new Port { Owner = this }; _ports = new[] { IntakePort, ExhaustPort }; // Set crankshaft cycle length crankshaft.CycleLength = CycleLengthRad; } public float ComputeVolume(float thetaRad) { float r = CrankRadius, l = ConRodLength; float cosTh = MathF.Cos(thetaRad), sinTh = MathF.Sin(thetaRad); float term = MathF.Sqrt(1f - Obliquity * Obliquity * sinTh * sinTh); float x = r * (1f - cosTh) + l * (1f - term); float area = MathF.PI * 0.25f * Bore * Bore; return clearanceVolume + area * x; } protected float CrankDeg => ((Crankshaft.CrankAngle + PhaseOffset) % CycleLengthRad) * 180f / MathF.PI; protected float Wiebe(float angleSinceSpark) { if (angleSinceSpark < WiebeStart) return 0f; float phi = (angleSinceSpark - WiebeStart) / WiebeDuration; return 1f - MathF.Exp(-WiebeA * MathF.Pow(phi, WiebeM + 1f)); } // ----- Main update called before flow solver ----- public 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); // Piston work float dV = cylinderVolume - prevVolume; float pRel = Pressure - 101325f; 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 prevDeg = (Crankshaft.PreviousAngle + PhaseOffset) * 180f / MathF.PI % MaxCycleDeg; float currDeg = crankAngleRad * 180f / MathF.PI % MaxCycleDeg; // Let derived class handle valve events, spark, fuel 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; } // ----- State update (mass/energy balance) ----- public void UpdateState(float dt) { float dmAir = 0f, dmExhaust = 0f, dE = 0f; foreach (var port in _ports) { float mdot = port.MassFlowRate; float af = mdot >= 0f ? port.AirFraction : AirFraction; dmAir += mdot * af * dt; dmExhaust += mdot * (1f - af) * dt; dE += mdot * port.SpecificEnthalpy * dt; } _airMass += dmAir; _exhaustMass += dmExhaust; cylinderEnergy += dE; float V = MathF.Max(cylinderVolume, 1e-12f); float currentP = (Gamma - 1f) * cylinderEnergy / V; if (currentP > MaxPressurePa) cylinderEnergy = MaxPressurePa * V / (Gamma - 1f); float currentRho = (_airMass + _exhaustMass) / V; float currentT = currentP / MathF.Max(currentRho * GasConstant, 1e-12f); if (currentT > MaxTemperatureK) { float pAtTlimit = currentRho * GasConstant * MaxTemperatureK; cylinderEnergy = pAtTlimit * V / (Gamma - 1f); } float totalMass = _airMass + _exhaustMass; if (totalMass < 1e-9f) { _airMass = 1e-9f; _exhaustMass = 0f; cylinderEnergy = 101325f * V / (Gamma - 1f); } else if (cylinderEnergy < 0f) { cylinderEnergy = 101325f * V / (Gamma - 1f); } if (_airMass < 0f) _airMass = 0f; if (_exhaustMass < 0f) _exhaustMass = 0f; } } }