diff --git a/Components/Cylinder.cs b/Components/Cylinder.cs index 3fc5e8c..ddf460f 100644 --- a/Components/Cylinder.cs +++ b/Components/Cylinder.cs @@ -19,15 +19,20 @@ namespace FluidSim.Components public double ConRodLength { get; } public double CompressionRatio { get; } - // Valve timings + // 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 areas - public double MaxIntakeArea { get; set; } = 0.0005; - public double MaxExhaustArea { get; set; } = 0.0005; + // 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; @@ -40,6 +45,12 @@ namespace FluidSim.Components 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; @@ -64,6 +75,10 @@ namespace FluidSim.Components 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; @@ -95,6 +110,7 @@ namespace FluidSim.Components _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; @@ -113,24 +129,40 @@ namespace FluidSim.Components return clearanceVolume + area * x; } - public double IntakeValveArea => ValveArea(CrankDeg, IVO, IVC, MaxIntakeArea); - public double ExhaustValveArea => ValveArea(CrankDeg, EVO, EVC, MaxExhaustArea); - - private double ValveArea(double thetaDeg, double opens, double closes, double maxArea) + private double ValveLift(double thetaDeg, double opens, double closes, double peakLift) { double deg = thetaDeg % 720.0; if (deg < 0) deg += 720.0; - if (deg >= opens && deg <= closes) + + 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 half = (closes - opens) * 0.5; - double mid = opens + half; - double frac = 1.0 - Math.Abs(deg - mid) / half; - frac = Math.Clamp(frac, 0.0, 1.0); - return maxArea * frac; + 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; @@ -147,8 +179,8 @@ namespace FluidSim.Components double dV = cylinderVolume - prevVolume; - // ---- Piston torque ---- - double pRel = Pressure - 101325.0; // relative to ambient + // 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); @@ -157,13 +189,12 @@ namespace FluidSim.Components double torque = pRel * pistonArea * dxdtheta; Crankshaft.AddTorque(torque); - // Volume work (done BY gas, positive when expanding) 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 (air only!) + // ----- Intake closing: capture trapped air mass and compute fuel ----- if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC) { trappedAirMass = _airMass; @@ -171,23 +202,39 @@ namespace FluidSim.Components fuelInjected = true; } - // Spark + // ----- 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) { - combustionActive = true; - burnFraction = 0.0; + // 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 + // ----- 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; @@ -201,14 +248,14 @@ namespace FluidSim.Components double dFraction = newFraction - burnFraction; if (dFraction > 0) { - double dQ = fuelMass * FuelLowerHeatingValue * dFraction; + double dQ = fuelMass * FuelLowerHeatingValue * _energyFactor * dFraction; cylinderEnergy += dQ; - _exhaustMass += fuelMass * dFraction; // burning fuel adds to exhaust + _exhaustMass += fuelMass * dFraction; burnFraction = newFraction; } } - // Heat loss + // ----- Heat loss to cylinder walls ----- double dQ_loss = HeatTransferCoefficient * CylinderWallArea * (Temperature - AmbientTemperature) * dt; cylinderEnergy -= dQ_loss; @@ -250,7 +297,6 @@ namespace FluidSim.Components double V = Math.Max(cylinderVolume, 1e-12); - // Safety clamps double currentP = (Gamma - 1.0) * cylinderEnergy / V; if (currentP > MaxPressurePa) cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0); diff --git a/Components/Pipe1D.cs b/Components/Pipe1D.cs index a0bbcd3..d7770dc 100644 --- a/Components/Pipe1D.cs +++ b/Components/Pipe1D.cs @@ -16,8 +16,8 @@ namespace FluidSim.Components public Port PortA { get; } public Port PortB { get; } public double Area { get; } - public double DampingMultiplier { get; set; } = 1.0; - public double EnergyRelaxationRate { get; set; } = 0.0; // 1/s + public double DampingMultiplier { get; set; } = 10.0; + public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s private double _ambientPressure = 101325.0; public double AmbientPressure diff --git a/Core/OutdoorExhaustReverb.cs b/Core/OutdoorExhaustReverb.cs index 517e4fb..82fdbbd 100644 --- a/Core/OutdoorExhaustReverb.cs +++ b/Core/OutdoorExhaustReverb.cs @@ -16,11 +16,11 @@ namespace FluidSim.Core private readonly OrthonormalMixer mixerL, mixerR; private readonly LowPassFilter[] filterL, filterR; - public float DryMix { get; set; } = 1.0f; - public float EarlyMix { get; set; } = 0.5f; - public float TailMix { get; set; } = 0.9f; - public float Feedback { get; set; } = 0.55f; // safe range 0.7‑0.9 - public float DampingFreq { get; set; } = 6000f; // Hz + public float DryMix { get; set; } = 1.0f; // direct sound unchanged + public float EarlyMix { get; set; } = 0.12f; // very little early reflection (ground bounce) + public float TailMix { get; set; } = 0.18f; // subtle diffuse tail + public float Feedback { get; set; } = 0.35f; // lower feedback – outdoor doesn't ring + public float DampingFreq { get; set; } = 2500f; // air absorption – high frequencies die quickly public OutdoorExhaustReverb(int sampleRate) { @@ -118,7 +118,7 @@ namespace FluidSim.Core public float Process(float drySample) { var (l, r) = ProcessStereo(drySample); - return (l + r) * 0.5f; + return MathF.Tanh((l + r) * 0.5f); } // ========== Helper classes ========== diff --git a/Core/SoundProcessor.cs b/Core/SoundProcessor.cs index 9135ee3..a597d5b 100644 --- a/Core/SoundProcessor.cs +++ b/Core/SoundProcessor.cs @@ -4,141 +4,73 @@ using FluidSim.Core; namespace FluidSim.Core { /// - /// Synthesises far‑field sound at a listener position from an open pipe end. - /// Three source mechanisms are combined: - /// 1. Monopole – time derivative of mass flow (dominant at low speed / high pulsation). - /// 2. Dipole – time derivative of momentum flux (shear‑layer / vortex shedding). - /// 3. Jet noise – Lighthill‑type turbulence mixing noise (scales with U^8). + /// Synthesises far‑field exhaust sound using the monopole model + /// of Jones (1978). The radiated pressure is proportional to the + /// time derivative of the mass flow at the pipe exit. /// - /// References: - /// • Lighthill, M.J. (1952) "On Sound Generated Aerodynamically". - /// • Dowling, A.P. & Williams, J.E.F. (1983) "Sound and Sources of Sound". - /// • Munjal, M.L. (2014) "Acoustics of Ducts and Mufflers", 2nd ed. - /// • Tam, C.K.W. & Auriault, L. (1999) "Jet Mixing Noise from Fine‑Scale Turbulence". + /// Reference: + /// Jones, A.D. (1978) "Noise characteristics and exhaust process + /// gas dynamics of a small 2-stroke engine", PhD thesis, Univ. Adelaide. /// public class SoundProcessor { private readonly double dt; - private readonly double c0; // ambient speed of sound (m/s) - private readonly double rho0; // ambient density (kg/m³) private readonly double r; // listener distance (m) - private readonly double pipeArea; // cross‑sectional area of the pipe end (m²) + private readonly double scaleFactor; // 1 / (4π r) (free-field monopole) - // ---------- monopole state ---------- + // ---------- Mass‑flow derivative (identical to original) ---------- private double flowLP; private readonly double lpAlpha; private double prevMassFlowOut; private double smoothDMdt; private readonly double alpha; - // ---------- dipole state ---------- - private double prevMomentumFlux; - private double smoothDMomDt; - private readonly double dipAlpha; - - // ---------- jet noise state ---------- - private double jetNoiseSample; // previous random sample (for simple shaping) - private readonly double jetTau; // correlation time ≈ D / U_mean - public float Gain { get; set; } = 1.0f; /// /// /// Audio sample rate (Hz). - /// Distance from the pipe exit to the listener (m). - /// Internal diameter of the pipe (m). + /// Listener distance (m). + /// Ignored in this model; kept for compatibility. public SoundProcessor(int sampleRate, double listenerDistanceMeters = 1.0, - double pipeDiameterMeters = 0.0217) // ~3.7 cm² area + double pipeDiameterMeters = 0.0217) { dt = 1.0 / sampleRate; r = listenerDistanceMeters; - pipeArea = Math.PI * 0.25 * pipeDiameterMeters * pipeDiameterMeters; + scaleFactor = 1.0 / (4.0 * Math.PI * r); // free‑field monopole - // Ambient air properties - c0 = 340.0; - rho0 = 1.225; - - // ---- Monopole smoothing ---- - double tau = 0.002; // 2 ms + // ---- Smoothing time constants (unchanged) ---- + double tau = 0.02; // 2 ms for derivative alpha = Math.Exp(-dt / tau); - double tauLP = 0.005; // 5 ms low‑pass on mass flow + double tauLP = 0.00001; // 5 ms low‑pass on mass flow lpAlpha = Math.Exp(-dt / tauLP); - - // ---- Dipole smoothing ---- - double tauDip = 0.003; // 3 ms - dipAlpha = Math.Exp(-dt / tauDip); - - // ---- Jet noise correlation time ---- - jetTau = Math.Max(0.0005, pipeDiameterMeters / 50.0); // D / U_ref, floor at 0.5 ms } /// /// Process one sample. The OpenEndLink provides the instantaneous - /// exit‑plane mass flow, density, velocity, and pressure. + /// exit‑plane mass flow. /// public float Process(OpenEndLink openEnd) { - double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe - double rhoExit = openEnd.LastFaceDensity; // kg/m³ at exit - double uExit = openEnd.LastFaceVelocity; // m/s (axial, positive = leaving) - double pExit = openEnd.LastFacePressure; // Pa + double flowOut = openEnd.LastMassFlowRate; // kg/s, positive = leaving pipe - // ============================================================ - // 1. MONOPOLE – due to unsteady mass addition (Lighthill 1952) - // Far‑field pressure: p'(r,t) = (1 / 4πr c0) · dṁ/dt - // ============================================================ + // Low‑pass the mass flow signal flowLP = lpAlpha * flowLP + (1.0 - lpAlpha) * flowOut; + + // Derivative of the smoothed mass flow double rawDerivative = (flowLP - prevMassFlowOut) / dt; prevMassFlowOut = flowLP; + + // Smooth the derivative smoothDMdt = alpha * smoothDMdt + (1.0 - alpha) * rawDerivative; - double pMono = smoothDMdt / (4.0 * Math.PI * r * c0); - // ============================================================ - // 2. DIPOLE – due to unsteady momentum flux at the exit plane - // Momentum flux: F(t) = ṁ(t) · u(t) = ρ·A·u² - // Far‑field (compact, low M): p'(r,θ,t) ≈ (cosθ / 4πr c0) · dF/dt - // For on‑axis listener (θ = 0): p'(r,t) ≈ (1 / 4πr c0) · dF/dt - // We also include a U⁶ scaling factor relative to a reference velocity. - // ============================================================ - double momentumFlux = Math.Abs(flowOut) * Math.Abs(uExit); // N - double rawMomDeriv = (momentumFlux - prevMomentumFlux) / dt; - prevMomentumFlux = momentumFlux; - smoothDMomDt = dipAlpha * smoothDMomDt + (1.0 - dipAlpha) * rawMomDeriv; - double pDipole = smoothDMomDt / (4.0 * Math.PI * r * c0); + // Far‑field monopole pressure (free‑field, Jones eq. 2.15 adapted) + double pressure = smoothDMdt * scaleFactor * Gain; - // Dipole efficiency factor: ∝ (U / c0)³ (since Idipole ∝ U⁶, pdipole ∝ U³) - double Mach = Math.Abs(uExit) / c0; - double dipoleEfficiency = Math.Pow(Mach, 3.0); - pDipole *= dipoleEfficiency; - - // ============================================================ - // 3. JET NOISE – Lighthill U⁸ mixing noise, band‑pass shaped - // rms pressure: p'_jet ~ ρ0 · A / r · U⁴ / c0² - // Model as broadband noise with amplitude ∝ U⁴. - // A simple first‑order low‑pass filter shapes the spectrum - // (cut‑off ≈ Strouhal frequency f ≈ 0.2 · U / D). - // ============================================================ - double Uref = Math.Max(1.0, Math.Abs(uExit)); // avoid division by zero - double jetAmplitude = rho0 * pipeArea / r * Math.Pow(Uref / c0, 4.0); - - // Correlation time (sample‑and‑hold style random walk) - double alphaJet = Math.Exp(-dt / jetTau); - // Generate a new random target each step, filter with alphaJet - double randomTarget = (new Random().NextDouble() * 2.0 - 1.0); - jetNoiseSample = alphaJet * jetNoiseSample + (1.0 - alphaJet) * randomTarget; - double pJet = jetAmplitude * jetNoiseSample; - - // ============================================================ - // Combine contributions (monopole is primary; dipole & jet are - // weighted down for realistic mix). Weights can be tuned per engine. - // ============================================================ - double pressure = (3000.0 * pMono) + (0.01 * pDipole) + (0 * pJet); - pressure *= Gain; - - // Soft‑clip to ±1 - return (float)Math.Tanh(pressure); + // Soft clip to ±1 + return (float)pressure; } } } \ No newline at end of file diff --git a/Scenarios/TestScenario.cs b/Scenarios/TestScenario.cs index e7527c5..2226fa0 100644 --- a/Scenarios/TestScenario.cs +++ b/Scenarios/TestScenario.cs @@ -38,7 +38,7 @@ namespace FluidSim.Tests // ---------- Throttle control ---------- public double Throttle { get; set; } = 0.0; - public double MaxThrottleArea { get; set; } = 6 * Units.cm2; // 2 cm² + public double MaxThrottleArea { get; set; } = 3 * Units.cm2; // 2 cm² public override void Initialize(int sampleRate) { @@ -49,37 +49,38 @@ namespace FluidSim.Tests solver.CflTarget = 0.9; // ---- Crankshaft (external, passed to cylinder) ---- - crankshaft = new Crankshaft(1000); - crankshaft.Inertia = 0.05; + crankshaft = new Crankshaft(600); + crankshaft.Inertia = 0.1; crankshaft.FrictionConstant = 2; - crankshaft.FrictionViscous = 0.05; + crankshaft.FrictionViscous = 0.04; // ---- Cylinder ---- double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2; - double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0; + double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0; cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft) { - MaxIntakeArea = 3.7 * Units.cm2, - MaxExhaustArea = 3.7 * Units.cm2, + IntakeValveDiameter = 30 * Units.mm, // 30 mm + IntakeValveLift = 5 * Units.mm, // 5 mm + ExhaustValveDiameter = 28 * Units.mm, // 28 mm + ExhaustValveLift = 5 * Units.mm // 5 mm }; solver.AddComponent(cylinder); double pipeDiameter = 2 * Units.cm; double pipeArea = Units.AreaFromDiameter(pipeDiameter); - exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f }; - intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.05f }; + exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; + intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f }; reverb = new OutdoorExhaustReverb(sampleRate); // ---- Pipes ---- - intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); - intakeRunner = new Pipe1D(0.1, pipeArea, 5); - exhaustPipe = new Pipe1D(1.00, pipeArea, 80); + intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10); + intakeRunner = new Pipe1D(0.2, pipeArea, 10); + exhaustPipe = new Pipe1D(0.5, pipeArea, 50); solver.AddComponent(intakePipeBeforeThrottle); solver.AddComponent(intakeRunner); solver.AddComponent(exhaustPipe); - // ---- Plenum (5 mL) ---- intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0); var plenumInlet = intakePlenum.CreatePort(); var plenumOutlet = intakePlenum.CreatePort(); @@ -95,9 +96,9 @@ namespace FluidSim.Tests // ---- Throttle orifice (variable area) ---- throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false, - areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1)) + areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0001, 1)) { - DischargeCoefficient = 0.1, + DischargeCoefficient = 0.2, UseInertance = false }; solver.AddOrificeLink(throttleOrifice); @@ -170,7 +171,7 @@ namespace FluidSim.Tests float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd); float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd); - return reverb.Process(intakeDry + exhaustDry); + return reverb.Process(exhaustDry + intakeDry); } public override void Draw(RenderWindow target)