using System; using System.Collections.Generic; using FluidSim.Interfaces; namespace FluidSim.Components { public class Cylinder : IComponent { public Port IntakePort { get; } public Port ExhaustPort { get; } public Crankshaft Crankshaft { get; } private readonly Port[] _ports; IReadOnlyList IComponent.Ports => _ports; // Geometry public double Bore { get; } public double Stroke { get; } public double ConRodLength { get; } public double CompressionRatio { get; } // Valve timings (degrees, 0 = TDC compression, 720° full cycle) public double IVO { get; } public double IVC { get; } public double EVO { get; } public double EVC { get; } // Valve geometry public double IntakeValveDiameter { get; set; } = 0.030; public double ExhaustValveDiameter { get; set; } = 0.028; public double IntakeValveLift { get; set; } = 0.005; public double ExhaustValveLift { get; set; } = 0.005; public double IntakeValveMaxArea => Math.PI * IntakeValveDiameter * IntakeValveLift; public double ExhaustValveMaxArea => Math.PI * ExhaustValveDiameter * ExhaustValveLift; // Ignition and combustion public double SparkAdvance { get; set; } = 20.0; public double WiebeA { get; set; } = 5.0; public double WiebeM { get; set; } = 2.0; public double WiebeDuration { get; set; } = 60.0; public double WiebeStart { get; set; } = 5.0; // Fuel public double StoichiometricAFR { get; set; } = 14.7; public double FuelLowerHeatingValue { get; set; } = 44e6; // Cycle‑to‑cycle randomness /// Fractional variation in fuel energy (±). 0.05 = ±5%. public double EnergyVariationFraction { get; set; } = 0.05; /// Probability of a misfire (0‑1). public double MisfireProbability { get; set; } = 0.01; // Heat loss public double CylinderWallArea { get; set; } = 0.02; public double HeatTransferCoefficient { get; set; } = 100.0; public double AmbientTemperature { get; set; } = 300.0; // State public double Volume => cylinderVolume; public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12); public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12); public double Density => Mass / Math.Max(cylinderVolume, 1e-12); public double Mass => _airMass + _exhaustMass; public double AirFraction => _airMass / Math.Max(Mass, 1e-12); public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume; private double cylinderVolume; private double cylinderEnergy; private double _airMass; private double _exhaustMass; private double trappedAirMass; private double fuelMass; private double burnFraction; private bool combustionActive; private bool fuelInjected; // per‑cycle randomness private double _energyFactor = 1.0; // applied to FuelLowerHeatingValue this cycle private readonly Random _random = new Random(); private const double Gamma = 1.4; private const double GasConstant = 287.0; private const double MaxPressurePa = 200e5; private const double MaxTemperatureK = 3500.0; public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio, double ivo, double ivc, double evo, double evc, Crankshaft crankshaft) { Bore = bore; Stroke = stroke; ConRodLength = conRodLength; CompressionRatio = compressionRatio; IVO = ivo; IVC = ivc; EVO = evo; EVC = evc; Crankshaft = crankshaft ?? throw new ArgumentNullException(nameof(crankshaft)); cylinderVolume = clearanceVolume; double initRho = 1.225; _airMass = initRho * clearanceVolume; _exhaustMass = 0.0; cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0); IntakePort = new Port { Owner = this }; ExhaustPort = new Port { Owner = this }; _ports = new[] { IntakePort, ExhaustPort }; } // Derived volumes private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke; private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0); private double CrankRadius => Stroke / 2.0; private double Obliquity => CrankRadius / ConRodLength; private double CrankDeg => (Crankshaft.CrankAngle % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0; public double ComputeVolume(double thetaRad) { double r = CrankRadius; double l = ConRodLength; double cosTh = Math.Cos(thetaRad); double sinTh = Math.Sin(thetaRad); double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh); double x = r * (1.0 - cosTh) + l * (1.0 - term); double area = Math.PI * 0.25 * Bore * Bore; return clearanceVolume + area * x; } private double ValveLift(double thetaDeg, double opens, double closes, double peakLift) { double deg = thetaDeg % 720.0; if (deg < 0) deg += 720.0; double duration = closes - opens; if (duration <= 0) return 0.0; double rampDur = duration * 0.25; double holdDur = duration - 2.0 * rampDur; if (deg >= opens && deg < opens + rampDur) { double t = (deg - opens) / rampDur; return peakLift * t * t * (3.0 - 2.0 * t); } else if (deg >= opens + rampDur && deg < opens + rampDur + holdDur) { return peakLift; } else if (deg >= opens + rampDur + holdDur && deg <= closes) { double t = (deg - (opens + rampDur + holdDur)) / rampDur; return peakLift * (1.0 - t) * (1.0 - t) * (1.0 + 2.0 * t); } return 0.0; } public double IntakeValveArea => Math.PI * IntakeValveDiameter * ValveLift(CrankDeg, IVO, IVC, IntakeValveLift); public double ExhaustValveArea => Math.PI * ExhaustValveDiameter * ValveLift(CrankDeg, EVO, EVC, ExhaustValveLift); private double Wiebe(double angleSinceSpark) { if (angleSinceSpark < WiebeStart) return 0.0; double phi = (angleSinceSpark - WiebeStart) / WiebeDuration; if (phi <= 0) return 0.0; return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1)); } public void PreStep(double dt) { double prevVolume = cylinderVolume; double crankAngleRad = Crankshaft.CrankAngle; cylinderVolume = ComputeVolume(crankAngleRad); double dV = cylinderVolume - prevVolume; // Piston torque double pRel = Pressure - 101325.0; double sinTh = Math.Sin(crankAngleRad); double cosTh = Math.Cos(crankAngleRad); double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh); double dxdtheta = CrankRadius * sinTh * (1.0 + Obliquity * cosTh / term); double pistonArea = Math.PI * 0.25 * Bore * Bore; double torque = pRel * pistonArea * dxdtheta; Crankshaft.AddTorque(torque); cylinderEnergy -= Pressure * dV; double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0; double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0; // ----- Intake closing: capture trapped air mass and compute fuel ----- if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC) { trappedAirMass = _airMass; fuelMass = trappedAirMass / StoichiometricAFR; fuelInjected = true; } // ----- Spark ignition (once per cycle, with misfire chance) ----- double sparkAngle = 0.0 - SparkAdvance; if (sparkAngle < 0) sparkAngle += 720.0; bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) || (prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle); if (crossedSpark && !combustionActive && fuelInjected) { // Decide misfire bool misfire = _random.NextDouble() < MisfireProbability; if (misfire) { combustionActive = false; // no combustion this cycle // fuel is not burned – will remain in cylinder and eventually exit as unburned mixture } else { combustionActive = true; burnFraction = 0.0; // Energy variation factor for this cycle double range = EnergyVariationFraction; _energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0); } } // ----- Combustion progress ----- if (combustionActive) { double angleSinceSpark = currDeg - sparkAngle; if (angleSinceSpark < 0) angleSinceSpark += 720.0; double newFraction = Wiebe(angleSinceSpark); if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance)) { newFraction = 1.0; combustionActive = false; // All gas becomes exhaust double totalMass = _airMass + _exhaustMass; _airMass = 0.0; _exhaustMass = totalMass; } double dFraction = newFraction - burnFraction; if (dFraction > 0) { double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction; cylinderEnergy += dQ; _exhaustMass += fuelMass * dFraction; burnFraction = newFraction; } } // ----- Heat loss to cylinder walls ----- double dQ_loss = HeatTransferCoefficient * CylinderWallArea * (Temperature - AmbientTemperature) * dt; cylinderEnergy -= dQ_loss; // Update port states double p = Pressure, rho = Density, T = Temperature; double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12); double 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; } public void UpdateState(double dt) { double dmAir = 0.0, dmExhaust = 0.0, dE = 0.0; foreach (var port in _ports) { double mdot = port.MassFlowRate; double af = mdot >= 0 ? port.AirFraction : AirFraction; dmAir += mdot * af * dt; dmExhaust += mdot * (1.0 - af) * dt; dE += mdot * port.SpecificEnthalpy * dt; } _airMass += dmAir; _exhaustMass += dmExhaust; cylinderEnergy += dE; double V = Math.Max(cylinderVolume, 1e-12); double currentP = (Gamma - 1.0) * cylinderEnergy / V; if (currentP > MaxPressurePa) cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0); double currentRho = (_airMass + _exhaustMass) / V; double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12); if (currentT > MaxTemperatureK) { double pAtTlimit = currentRho * GasConstant * MaxTemperatureK; cylinderEnergy = pAtTlimit * V / (Gamma - 1.0); } double totalMass = _airMass + _exhaustMass; if (totalMass < 1e-9) { _airMass = 1e-9; _exhaustMass = 0.0; cylinderEnergy = 101325.0 * V / (Gamma - 1.0); } else if (cylinderEnergy < 0.0) { cylinderEnergy = 101325.0 * V / (Gamma - 1.0); } if (_airMass < 0.0) _airMass = 0.0; if (_exhaustMass < 0.0) _exhaustMass = 0.0; } } }