added 4 cyl
This commit is contained in:
@@ -46,9 +46,7 @@ namespace FluidSim.Components
|
|||||||
public double FuelLowerHeatingValue { get; set; } = 44e6;
|
public double FuelLowerHeatingValue { get; set; } = 44e6;
|
||||||
|
|
||||||
// Cycle‑to‑cycle randomness
|
// Cycle‑to‑cycle randomness
|
||||||
/// <summary>Fractional variation in fuel energy (±). 0.05 = ±5%.</summary>
|
|
||||||
public double EnergyVariationFraction { get; set; } = 0.05;
|
public double EnergyVariationFraction { get; set; } = 0.05;
|
||||||
/// <summary>Probability of a misfire (0‑1).</summary>
|
|
||||||
public double MisfireProbability { get; set; } = 0.01;
|
public double MisfireProbability { get; set; } = 0.01;
|
||||||
|
|
||||||
// Heat loss
|
// Heat loss
|
||||||
@@ -56,7 +54,14 @@ namespace FluidSim.Components
|
|||||||
public double HeatTransferCoefficient { get; set; } = 100.0;
|
public double HeatTransferCoefficient { get; set; } = 100.0;
|
||||||
public double AmbientTemperature { get; set; } = 300.0;
|
public double AmbientTemperature { get; set; } = 300.0;
|
||||||
|
|
||||||
// State
|
// ---- Multi‑cylinder support ----
|
||||||
|
/// <summary>
|
||||||
|
/// Phase offset (radians) added to the crankshaft angle for this cylinder.
|
||||||
|
/// Used for multi‑cylinder engines; set to 0 for single‑cylinder.
|
||||||
|
/// </summary>
|
||||||
|
public double PhaseOffset { get; set; } = 0.0;
|
||||||
|
|
||||||
|
// State (public for drawing)
|
||||||
public double Volume => cylinderVolume;
|
public double Volume => cylinderVolume;
|
||||||
public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
|
public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
|
||||||
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
|
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
|
||||||
@@ -75,8 +80,7 @@ namespace FluidSim.Components
|
|||||||
private bool combustionActive;
|
private bool combustionActive;
|
||||||
private bool fuelInjected;
|
private bool fuelInjected;
|
||||||
|
|
||||||
// per‑cycle randomness
|
private double _energyFactor = 1.0;
|
||||||
private double _energyFactor = 1.0; // applied to FuelLowerHeatingValue this cycle
|
|
||||||
private readonly Random _random = new Random();
|
private readonly Random _random = new Random();
|
||||||
|
|
||||||
private const double Gamma = 1.4;
|
private const double Gamma = 1.4;
|
||||||
@@ -115,7 +119,10 @@ namespace FluidSim.Components
|
|||||||
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
|
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
|
||||||
private double CrankRadius => Stroke / 2.0;
|
private double CrankRadius => Stroke / 2.0;
|
||||||
private double Obliquity => CrankRadius / ConRodLength;
|
private double Obliquity => CrankRadius / ConRodLength;
|
||||||
private double CrankDeg => (Crankshaft.CrankAngle % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
|
|
||||||
|
// Offset-aware crank angle in degrees
|
||||||
|
private double CrankDeg =>
|
||||||
|
((Crankshaft.CrankAngle + PhaseOffset) % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
|
||||||
|
|
||||||
public double ComputeVolume(double thetaRad)
|
public double ComputeVolume(double thetaRad)
|
||||||
{
|
{
|
||||||
@@ -174,7 +181,9 @@ namespace FluidSim.Components
|
|||||||
public void PreStep(double dt)
|
public void PreStep(double dt)
|
||||||
{
|
{
|
||||||
double prevVolume = cylinderVolume;
|
double prevVolume = cylinderVolume;
|
||||||
double crankAngleRad = Crankshaft.CrankAngle;
|
|
||||||
|
// ----- Use phase‑offset crank angle for this cylinder -----
|
||||||
|
double crankAngleRad = Crankshaft.CrankAngle + PhaseOffset;
|
||||||
cylinderVolume = ComputeVolume(crankAngleRad);
|
cylinderVolume = ComputeVolume(crankAngleRad);
|
||||||
|
|
||||||
double dV = cylinderVolume - prevVolume;
|
double dV = cylinderVolume - prevVolume;
|
||||||
@@ -191,7 +200,9 @@ namespace FluidSim.Components
|
|||||||
|
|
||||||
cylinderEnergy -= Pressure * dV;
|
cylinderEnergy -= Pressure * dV;
|
||||||
|
|
||||||
double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0;
|
// Also use offset angle for event detection
|
||||||
|
double crankshaftPrevAngle = Crankshaft.PreviousAngle;
|
||||||
|
double prevDeg = (crankshaftPrevAngle + PhaseOffset) * 180.0 / Math.PI % 720.0;
|
||||||
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
|
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
|
||||||
|
|
||||||
// ----- Intake closing: capture trapped air mass and compute fuel -----
|
// ----- Intake closing: capture trapped air mass and compute fuel -----
|
||||||
@@ -202,7 +213,7 @@ namespace FluidSim.Components
|
|||||||
fuelInjected = true;
|
fuelInjected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Spark ignition (once per cycle, with misfire chance) -----
|
// ----- Spark ignition -----
|
||||||
double sparkAngle = 0.0 - SparkAdvance;
|
double sparkAngle = 0.0 - SparkAdvance;
|
||||||
if (sparkAngle < 0) sparkAngle += 720.0;
|
if (sparkAngle < 0) sparkAngle += 720.0;
|
||||||
|
|
||||||
@@ -210,19 +221,15 @@ namespace FluidSim.Components
|
|||||||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
|
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
|
||||||
if (crossedSpark && !combustionActive && fuelInjected)
|
if (crossedSpark && !combustionActive && fuelInjected)
|
||||||
{
|
{
|
||||||
// Decide misfire
|
|
||||||
bool misfire = _random.NextDouble() < MisfireProbability;
|
bool misfire = _random.NextDouble() < MisfireProbability;
|
||||||
if (misfire)
|
if (misfire)
|
||||||
{
|
{
|
||||||
combustionActive = false; // no combustion this cycle
|
combustionActive = false;
|
||||||
// fuel is not burned – will remain in cylinder and eventually exit as unburned mixture
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
combustionActive = true;
|
combustionActive = true;
|
||||||
burnFraction = 0.0;
|
burnFraction = 0.0;
|
||||||
|
|
||||||
// Energy variation factor for this cycle
|
|
||||||
double range = EnergyVariationFraction;
|
double range = EnergyVariationFraction;
|
||||||
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
|
_energyFactor = 1.0 + range * (2.0 * _random.NextDouble() - 1.0);
|
||||||
}
|
}
|
||||||
@@ -239,7 +246,6 @@ namespace FluidSim.Components
|
|||||||
{
|
{
|
||||||
newFraction = 1.0;
|
newFraction = 1.0;
|
||||||
combustionActive = false;
|
combustionActive = false;
|
||||||
// All gas becomes exhaust
|
|
||||||
double totalMass = _airMass + _exhaustMass;
|
double totalMass = _airMass + _exhaustMass;
|
||||||
_airMass = 0.0;
|
_airMass = 0.0;
|
||||||
_exhaustMass = totalMass;
|
_exhaustMass = totalMass;
|
||||||
@@ -255,7 +261,7 @@ namespace FluidSim.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Heat loss to cylinder walls -----
|
// ----- Heat loss -----
|
||||||
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
|
||||||
(Temperature - AmbientTemperature) * dt;
|
(Temperature - AmbientTemperature) * dt;
|
||||||
cylinderEnergy -= dQ_loss;
|
cylinderEnergy -= dQ_loss;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace FluidSim.Components
|
|||||||
public double Area { get; }
|
public double Area { get; }
|
||||||
public double DampingMultiplier { get; set; } = 10.0;
|
public double DampingMultiplier { get; set; } = 10.0;
|
||||||
public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s
|
public double EnergyRelaxationRate { get; set; } = 5.0; // 1/s
|
||||||
|
public string Name = "Pipe";
|
||||||
|
|
||||||
private double _ambientPressure = 101325.0;
|
private double _ambientPressure = 101325.0;
|
||||||
public double AmbientPressure
|
public double AmbientPressure
|
||||||
|
|||||||
@@ -20,17 +20,11 @@ namespace FluidSim.Core
|
|||||||
|
|
||||||
// ---------- Timing accumulators (reset every LogInterval steps) ----------
|
// ---------- Timing accumulators (reset every LogInterval steps) ----------
|
||||||
private long _stepCount;
|
private long _stepCount;
|
||||||
private double _timeTotal;
|
private double _timeTotal, _timeCFL, _timeOrifice, _timeOpenEnd,
|
||||||
private double _timeCFL;
|
_timePipe, _timeClearGhosts, _timeUpdateState;
|
||||||
private double _timeOrifice;
|
|
||||||
private double _timeOpenEnd;
|
|
||||||
private double _timeJunction;
|
|
||||||
private double _timePipe;
|
|
||||||
private double _timeClearGhosts;
|
|
||||||
private double _timeUpdateState;
|
|
||||||
|
|
||||||
private const int LogInterval = 5000; // print once per second (at 44.1 kHz)
|
private const int LogInterval = 5000;
|
||||||
private const bool EnableLogging = false;
|
private const bool EnableLogging = false; // temporarily ON for debugging
|
||||||
|
|
||||||
public void SetTimeStep(double dt) => _dt = dt;
|
public void SetTimeStep(double dt) => _dt = dt;
|
||||||
|
|
||||||
@@ -45,18 +39,44 @@ namespace FluidSim.Core
|
|||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
// CFL count
|
// CFL count – track which pipe demands the most sub‑steps
|
||||||
int nSub = 1;
|
int nSub = 1;
|
||||||
|
Pipe1D worstPipe = pipes[0];
|
||||||
foreach (var p in pipes)
|
foreach (var p in pipes)
|
||||||
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt, CflTarget));
|
{
|
||||||
|
int n = p.GetRequiredSubSteps(_dt, CflTarget);
|
||||||
|
if (n > nSub)
|
||||||
|
{
|
||||||
|
nSub = n;
|
||||||
|
worstPipe = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
double dtSub = _dt / nSub;
|
double dtSub = _dt / nSub;
|
||||||
|
|
||||||
|
// ----- Diagnostic: warn if nSub is high -----
|
||||||
|
if (nSub > 50)
|
||||||
|
{
|
||||||
|
double maxW = 0;
|
||||||
|
for (int i = 0; i < worstPipe.CellCount; i++)
|
||||||
|
{
|
||||||
|
double rho = worstPipe.GetCellDensity(i);
|
||||||
|
double u = Math.Abs(worstPipe.GetCellVelocity(i));
|
||||||
|
double p = worstPipe.GetCellPressure(i);
|
||||||
|
double c = Math.Sqrt(1.4 * p / Math.Max(rho, 1e-12));
|
||||||
|
if (u + c > maxW) maxW = u + c;
|
||||||
|
}
|
||||||
|
Console.WriteLine($"nSub = {nSub} (worst pipe: {worstPipe.Name}, maxW = {maxW:F0} m/s)");
|
||||||
|
}
|
||||||
|
|
||||||
_timeCFL += sw.Elapsed.TotalSeconds;
|
_timeCFL += sw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
|
// ----- Safety cap – prevent the solver from hanging -----
|
||||||
const int maxSubSteps = 10000;
|
const int maxSubSteps = 10000;
|
||||||
if (nSub > maxSubSteps)
|
const int hardLimit = 500; // temporary low cap for debugging
|
||||||
|
|
||||||
|
if (nSub > hardLimit)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Warning: required sub‑steps {nSub} exceeds limit. Simulation stopped.");
|
Console.WriteLine($"nSub ({nSub}) exceeds hard limit {hardLimit}. Simulation step skipped.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,49 +110,33 @@ namespace FluidSim.Core
|
|||||||
comp.UpdateState(_dt);
|
comp.UpdateState(_dt);
|
||||||
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS;
|
_timeUpdateState += sw.Elapsed.TotalSeconds - tUS;
|
||||||
|
|
||||||
// accumulate total step time (includes CFL, sub‑steps, clear ghosts, update state)
|
|
||||||
_timeTotal += sw.Elapsed.TotalSeconds;
|
_timeTotal += sw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
// ---------- Periodic report ----------
|
|
||||||
_stepCount++;
|
_stepCount++;
|
||||||
if (_stepCount % LogInterval == 0 && EnableLogging)
|
if (_stepCount % LogInterval == 0 && EnableLogging)
|
||||||
{
|
{
|
||||||
if (_timeTotal > 0)
|
if (_timeTotal > 0)
|
||||||
{
|
{
|
||||||
double totalMs = _timeTotal * 1000.0;
|
double stepsPerSec = LogInterval / _timeTotal;
|
||||||
double avgUs = (_timeTotal / LogInterval) * 1e6; // µs per step
|
double avgUs = (_timeTotal / LogInterval) * 1e6;
|
||||||
double stepsPerSec = LogInterval / _timeTotal; // steps per second
|
|
||||||
|
|
||||||
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---");
|
Console.WriteLine($"--- Solver timing ({LogInterval} steps) ---");
|
||||||
Console.WriteLine($" Steps per second: {stepsPerSec:F1}");
|
Console.WriteLine($" Steps per second: {stepsPerSec:F1}");
|
||||||
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
|
Console.WriteLine($" Avg step time: {avgUs:F1} µs (last nSub = {nSub})");
|
||||||
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} % ({_timeCFL * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" CFL calc: {_timeCFL / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" Sub‑step loop:");
|
Console.WriteLine($" Sub‑step loop:");
|
||||||
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} % ({_timeOrifice * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" Orifice: {_timeOrifice / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} % ({_timeOpenEnd * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" OpenEnd: {_timeOpenEnd / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" Junctions: {_timeJunction / _timeTotal * 100:F1} % ({_timeJunction * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" Pipe steps: {_timePipe / _timeTotal * 100:F1} % ({_timePipe * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" Clear ghosts: {_timeClearGhosts / _timeTotal * 100:F1} % ({_timeClearGhosts * 1e6 / LogInterval:F1} µs/step)");
|
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} %");
|
||||||
Console.WriteLine($" Update state: {_timeUpdateState / _timeTotal * 100:F1} % ({_timeUpdateState * 1e6 / LogInterval:F1} µs/step)");
|
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
// ---------- Optional detailed pipe profiling ----------
|
|
||||||
if (Pipe1D.EnableDetailedProfiling)
|
|
||||||
{
|
|
||||||
foreach (var pipe in pipes)
|
|
||||||
{
|
|
||||||
Console.WriteLine(pipe.GetDetailProfileReport());
|
|
||||||
pipe.ResetDetailCounters();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset accumulators for next interval
|
|
||||||
_timeTotal = 0;
|
_timeTotal = 0;
|
||||||
_timeCFL = 0;
|
_timeCFL = 0;
|
||||||
_timeOrifice = 0;
|
_timeOrifice = 0;
|
||||||
_timeOpenEnd = 0;
|
_timeOpenEnd = 0;
|
||||||
_timeJunction = 0;
|
|
||||||
_timePipe = 0;
|
_timePipe = 0;
|
||||||
_timeClearGhosts = 0;
|
_timeClearGhosts = 0;
|
||||||
_timeUpdateState = 0;
|
_timeUpdateState = 0;
|
||||||
|
|||||||
12
Program.cs
12
Program.cs
@@ -33,7 +33,7 @@ public class Program
|
|||||||
// Audio & simulation
|
// Audio & simulation
|
||||||
private static SimulationRingBuffer _simRingBuffer = null!;
|
private static SimulationRingBuffer _simRingBuffer = null!;
|
||||||
private static SoundEngine _soundEngine = null!;
|
private static SoundEngine _soundEngine = null!;
|
||||||
private static TestScenario _scenario = null!; // cast to access ThrottleArea
|
private static Scenario _scenario = null!; // cast to access ThrottleArea
|
||||||
private static Font? _overlayFont;
|
private static Font? _overlayFont;
|
||||||
private static Text? _overlayText;
|
private static Text? _overlayText;
|
||||||
|
|
||||||
@@ -50,7 +50,8 @@ public class Program
|
|||||||
{
|
{
|
||||||
var window = CreateWindow();
|
var window = CreateWindow();
|
||||||
LoadFont();
|
LoadFont();
|
||||||
_scenario = (TestScenario)InitializeScenario();
|
_scenario = new TestScenario();
|
||||||
|
_scenario.Initialize(SampleRate);
|
||||||
_lastThrottleUpdateTime = 0.0;
|
_lastThrottleUpdateTime = 0.0;
|
||||||
|
|
||||||
_simRingBuffer = new SimulationRingBuffer(131072);
|
_simRingBuffer = new SimulationRingBuffer(131072);
|
||||||
@@ -170,13 +171,6 @@ public class Program
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Scenario InitializeScenario()
|
|
||||||
{
|
|
||||||
var sc = new TestScenario();
|
|
||||||
sc.Initialize(SampleRate);
|
|
||||||
return sc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
||||||
{
|
{
|
||||||
if (_timeWarpActive) return;
|
if (_timeWarpActive) return;
|
||||||
|
|||||||
303
Scenarios/Inline4Scenario.cs
Normal file
303
Scenarios/Inline4Scenario.cs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
using System;
|
||||||
|
using SFML.Graphics;
|
||||||
|
using SFML.System;
|
||||||
|
using FluidSim.Components;
|
||||||
|
using FluidSim.Core;
|
||||||
|
using FluidSim.Utils;
|
||||||
|
|
||||||
|
namespace FluidSim.Tests
|
||||||
|
{
|
||||||
|
public class Inline4Scenario : Scenario
|
||||||
|
{
|
||||||
|
// Crankshaft
|
||||||
|
private Crankshaft crankshaft;
|
||||||
|
|
||||||
|
// Cylinders
|
||||||
|
private Cylinder cyl1, cyl2, cyl3, cyl4;
|
||||||
|
|
||||||
|
// Intake
|
||||||
|
private Pipe1D intakePipeBeforeThrottle;
|
||||||
|
private Volume0D intakePlenum;
|
||||||
|
|
||||||
|
// Runners
|
||||||
|
private Pipe1D runner1, runner2, runner3, runner4;
|
||||||
|
|
||||||
|
// Exhaust pipes
|
||||||
|
private Pipe1D exh1, exh2, exh3, exh4;
|
||||||
|
|
||||||
|
// Links – intake
|
||||||
|
private OpenEndLink intakeOpenEnd;
|
||||||
|
private OrificeLink throttleOrifice;
|
||||||
|
|
||||||
|
// Plenum‑to‑runner orifices
|
||||||
|
private OrificeLink plenumToRunner1, plenumToRunner2, plenumToRunner3, plenumToRunner4;
|
||||||
|
|
||||||
|
// Intake valves
|
||||||
|
private OrificeLink intakeValve1, intakeValve2, intakeValve3, intakeValve4;
|
||||||
|
|
||||||
|
// Exhaust valves
|
||||||
|
private OrificeLink exhaustValve1, exhaustValve2, exhaustValve3, exhaustValve4;
|
||||||
|
|
||||||
|
// Exhaust open ends
|
||||||
|
private OpenEndLink exhaustOpenEnd1, exhaustOpenEnd2, exhaustOpenEnd3, exhaustOpenEnd4;
|
||||||
|
|
||||||
|
private Solver solver;
|
||||||
|
private SoundProcessor exhaustSoundProcessor;
|
||||||
|
private SoundProcessor intakeSoundProcessor;
|
||||||
|
private OutdoorExhaustReverb reverb;
|
||||||
|
private double dt;
|
||||||
|
private int stepCount;
|
||||||
|
|
||||||
|
public double MaxThrottleArea { get; set; } = 3 * Units.cm2;
|
||||||
|
|
||||||
|
public override void Initialize(int sampleRate)
|
||||||
|
{
|
||||||
|
dt = 1.0 / sampleRate;
|
||||||
|
|
||||||
|
solver = new Solver();
|
||||||
|
solver.SetTimeStep(dt);
|
||||||
|
solver.CflTarget = 0.9;
|
||||||
|
|
||||||
|
// ---- Shared crankshaft ----
|
||||||
|
crankshaft = new Crankshaft(800);
|
||||||
|
crankshaft.Inertia = 1;
|
||||||
|
crankshaft.FrictionConstant = 16;
|
||||||
|
crankshaft.FrictionViscous = 0.5;
|
||||||
|
|
||||||
|
// ---- Cylinder geometry ----
|
||||||
|
double bore = 0.056, stroke = 0.057, conRod = 0.110, compRatio = 9.2;
|
||||||
|
double ivo = 350.0, ivc = 580.0, evo = 120.0, evc = 370.0;
|
||||||
|
|
||||||
|
// Firing order 1-3-4-2 → phase offsets in radians
|
||||||
|
double phase0 = 0.0 * Math.PI / 180.0;
|
||||||
|
double phase1 = 180.0 * Math.PI / 180.0;
|
||||||
|
double phase2 = 540.0 * Math.PI / 180.0;
|
||||||
|
double phase3 = 360.0 * Math.PI / 180.0;
|
||||||
|
|
||||||
|
cyl1 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
||||||
|
{
|
||||||
|
IntakeValveDiameter = 30 * Units.mm,
|
||||||
|
IntakeValveLift = 5 * Units.mm,
|
||||||
|
ExhaustValveDiameter = 28 * Units.mm,
|
||||||
|
ExhaustValveLift = 5 * Units.mm,
|
||||||
|
PhaseOffset = phase0,
|
||||||
|
EnergyVariationFraction = 0.03,
|
||||||
|
MisfireProbability = 0.01
|
||||||
|
};
|
||||||
|
cyl2 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
||||||
|
{
|
||||||
|
IntakeValveDiameter = 30 * Units.mm,
|
||||||
|
IntakeValveLift = 5 * Units.mm,
|
||||||
|
ExhaustValveDiameter = 28 * Units.mm,
|
||||||
|
ExhaustValveLift = 5 * Units.mm,
|
||||||
|
PhaseOffset = phase1,
|
||||||
|
EnergyVariationFraction = 0.03,
|
||||||
|
MisfireProbability = 0.01
|
||||||
|
};
|
||||||
|
cyl3 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
||||||
|
{
|
||||||
|
IntakeValveDiameter = 30 * Units.mm,
|
||||||
|
IntakeValveLift = 5 * Units.mm,
|
||||||
|
ExhaustValveDiameter = 28 * Units.mm,
|
||||||
|
ExhaustValveLift = 5 * Units.mm,
|
||||||
|
PhaseOffset = phase2,
|
||||||
|
EnergyVariationFraction = 0.03,
|
||||||
|
MisfireProbability = 0.01
|
||||||
|
};
|
||||||
|
cyl4 = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
|
||||||
|
{
|
||||||
|
IntakeValveDiameter = 30 * Units.mm,
|
||||||
|
IntakeValveLift = 5 * Units.mm,
|
||||||
|
ExhaustValveDiameter = 28 * Units.mm,
|
||||||
|
ExhaustValveLift = 5 * Units.mm,
|
||||||
|
PhaseOffset = phase3,
|
||||||
|
EnergyVariationFraction = 0.03,
|
||||||
|
MisfireProbability = 0.01
|
||||||
|
};
|
||||||
|
solver.AddComponent(cyl1);
|
||||||
|
solver.AddComponent(cyl2);
|
||||||
|
solver.AddComponent(cyl3);
|
||||||
|
solver.AddComponent(cyl4);
|
||||||
|
|
||||||
|
double pipeDiameter = 2 * Units.cm;
|
||||||
|
double pipeArea = Units.AreaFromDiameter(pipeDiameter);
|
||||||
|
|
||||||
|
exhaustSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
|
||||||
|
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
|
||||||
|
reverb = new OutdoorExhaustReverb(sampleRate);
|
||||||
|
|
||||||
|
// ---- Intake pipe before throttle ----
|
||||||
|
intakePipeBeforeThrottle = new Pipe1D(0.2, pipeArea, 10);
|
||||||
|
solver.AddComponent(intakePipeBeforeThrottle);
|
||||||
|
|
||||||
|
// ---- Plenum ----
|
||||||
|
intakePlenum = new Volume0D(50 * Units.mL, 101325.0, 300.0);
|
||||||
|
var plenumInlet = intakePlenum.CreatePort(); // port 0
|
||||||
|
var plenumOut1 = intakePlenum.CreatePort(); // port 1
|
||||||
|
var plenumOut2 = intakePlenum.CreatePort(); // port 2
|
||||||
|
var plenumOut3 = intakePlenum.CreatePort(); // port 3
|
||||||
|
var plenumOut4 = intakePlenum.CreatePort(); // port 4
|
||||||
|
solver.AddComponent(intakePlenum);
|
||||||
|
|
||||||
|
// ---- Runners ----
|
||||||
|
runner1 = new Pipe1D(0.2, pipeArea, 5);
|
||||||
|
runner2 = new Pipe1D(0.2, pipeArea, 5);
|
||||||
|
runner3 = new Pipe1D(0.2, pipeArea, 5);
|
||||||
|
runner4 = new Pipe1D(0.2, pipeArea, 5);
|
||||||
|
solver.AddComponent(runner1);
|
||||||
|
solver.AddComponent(runner2);
|
||||||
|
solver.AddComponent(runner3);
|
||||||
|
solver.AddComponent(runner4);
|
||||||
|
|
||||||
|
// ---- Exhaust pipes ----
|
||||||
|
exh1 = new Pipe1D(0.2, pipeArea, 10);
|
||||||
|
exh2 = new Pipe1D(0.2, pipeArea, 10);
|
||||||
|
exh3 = new Pipe1D(0.2, pipeArea, 10);
|
||||||
|
exh4 = new Pipe1D(0.2, pipeArea, 10);
|
||||||
|
solver.AddComponent(exh1);
|
||||||
|
solver.AddComponent(exh2);
|
||||||
|
solver.AddComponent(exh3);
|
||||||
|
solver.AddComponent(exh4);
|
||||||
|
|
||||||
|
// ---- Plenum → runner orifices ----
|
||||||
|
plenumToRunner1 = new OrificeLink(plenumOut1, runner1, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
plenumToRunner2 = new OrificeLink(plenumOut2, runner2, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
plenumToRunner3 = new OrificeLink(plenumOut3, runner3, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
plenumToRunner4 = new OrificeLink(plenumOut4, runner4, isPipeLeftEnd: true, areaProvider: () => pipeArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
solver.AddOrificeLink(plenumToRunner1);
|
||||||
|
solver.AddOrificeLink(plenumToRunner2);
|
||||||
|
solver.AddOrificeLink(plenumToRunner3);
|
||||||
|
solver.AddOrificeLink(plenumToRunner4);
|
||||||
|
|
||||||
|
// ---- Intake valves ----
|
||||||
|
intakeValve1 = new OrificeLink(cyl1.IntakePort, runner1, isPipeLeftEnd: false, areaProvider: () => cyl1.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
intakeValve2 = new OrificeLink(cyl2.IntakePort, runner2, isPipeLeftEnd: false, areaProvider: () => cyl2.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
intakeValve3 = new OrificeLink(cyl3.IntakePort, runner3, isPipeLeftEnd: false, areaProvider: () => cyl3.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
intakeValve4 = new OrificeLink(cyl4.IntakePort, runner4, isPipeLeftEnd: false, areaProvider: () => cyl4.IntakeValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
solver.AddOrificeLink(intakeValve1);
|
||||||
|
solver.AddOrificeLink(intakeValve2);
|
||||||
|
solver.AddOrificeLink(intakeValve3);
|
||||||
|
solver.AddOrificeLink(intakeValve4);
|
||||||
|
|
||||||
|
// ---- Exhaust valves ----
|
||||||
|
exhaustValve1 = new OrificeLink(cyl1.ExhaustPort, exh1, isPipeLeftEnd: true, areaProvider: () => cyl1.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
exhaustValve2 = new OrificeLink(cyl2.ExhaustPort, exh2, isPipeLeftEnd: true, areaProvider: () => cyl2.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
exhaustValve3 = new OrificeLink(cyl3.ExhaustPort, exh3, isPipeLeftEnd: true, areaProvider: () => cyl3.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
exhaustValve4 = new OrificeLink(cyl4.ExhaustPort, exh4, isPipeLeftEnd: true, areaProvider: () => cyl4.ExhaustValveArea) { DischargeCoefficient = 1.0, UseInertance = false };
|
||||||
|
solver.AddOrificeLink(exhaustValve1);
|
||||||
|
solver.AddOrificeLink(exhaustValve2);
|
||||||
|
solver.AddOrificeLink(exhaustValve3);
|
||||||
|
solver.AddOrificeLink(exhaustValve4);
|
||||||
|
|
||||||
|
// ---- Exhaust open ends ----
|
||||||
|
exhaustOpenEnd1 = new OpenEndLink(exh1, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
|
||||||
|
exhaustOpenEnd2 = new OpenEndLink(exh2, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
|
||||||
|
exhaustOpenEnd3 = new OpenEndLink(exh3, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
|
||||||
|
exhaustOpenEnd4 = new OpenEndLink(exh4, isLeftEnd: false) { AmbientPressure = 101325.0, Gamma = 1.4 };
|
||||||
|
solver.AddOpenEndLink(exhaustOpenEnd1);
|
||||||
|
solver.AddOpenEndLink(exhaustOpenEnd2);
|
||||||
|
solver.AddOpenEndLink(exhaustOpenEnd3);
|
||||||
|
solver.AddOpenEndLink(exhaustOpenEnd4);
|
||||||
|
|
||||||
|
// ---- Intake open end ----
|
||||||
|
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
|
||||||
|
{
|
||||||
|
AmbientPressure = 101325.0,
|
||||||
|
Gamma = 1.4
|
||||||
|
};
|
||||||
|
solver.AddOpenEndLink(intakeOpenEnd);
|
||||||
|
|
||||||
|
// ---- Throttle ----
|
||||||
|
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||||||
|
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.001, 1.0))
|
||||||
|
{
|
||||||
|
DischargeCoefficient = 0.2,
|
||||||
|
UseInertance = false
|
||||||
|
};
|
||||||
|
solver.AddOrificeLink(throttleOrifice);
|
||||||
|
|
||||||
|
stepCount = 0;
|
||||||
|
Console.WriteLine("Inline-4 engine test");
|
||||||
|
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
|
||||||
|
Console.WriteLine("Firing order 1-3-4-2, 180° intervals");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override float Process()
|
||||||
|
{
|
||||||
|
crankshaft.Step(dt);
|
||||||
|
|
||||||
|
cyl1.PreStep(dt);
|
||||||
|
cyl2.PreStep(dt);
|
||||||
|
cyl3.PreStep(dt);
|
||||||
|
cyl4.PreStep(dt);
|
||||||
|
|
||||||
|
solver.Step();
|
||||||
|
stepCount++;
|
||||||
|
|
||||||
|
if (stepCount % 10000 == 0)
|
||||||
|
{
|
||||||
|
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
|
||||||
|
Console.WriteLine($"Step {stepCount}, RPM = {rpm:F0}, " +
|
||||||
|
$"cyl1 P = {cyl1.Pressure / 1e5:F2} bar, " +
|
||||||
|
$"plenum P = {intakePlenum.Pressure / 1e5:F2} bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix all exhaust sounds
|
||||||
|
float exhaustMix = exhaustSoundProcessor.Process(exhaustOpenEnd1)
|
||||||
|
+ exhaustSoundProcessor.Process(exhaustOpenEnd2)
|
||||||
|
+ exhaustSoundProcessor.Process(exhaustOpenEnd3)
|
||||||
|
+ exhaustSoundProcessor.Process(exhaustOpenEnd4);
|
||||||
|
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
|
||||||
|
return reverb.Process(exhaustMix * 0.25f + intakeDry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw(RenderWindow target)
|
||||||
|
{
|
||||||
|
float winW = target.GetView().Size.X;
|
||||||
|
float winH = target.GetView().Size.Y;
|
||||||
|
|
||||||
|
float startX = 60f;
|
||||||
|
float spacing = 80f;
|
||||||
|
float intakeY = winH / 2f - 80f;
|
||||||
|
float exhaustY = winH / 2f + 80f;
|
||||||
|
|
||||||
|
// Plenum
|
||||||
|
float plenW = 50f, plenH = 120f;
|
||||||
|
float plenX = startX;
|
||||||
|
float plenTopY = intakeY - plenH / 2f;
|
||||||
|
DrawVolume(target, intakePlenum, plenX, plenTopY, plenW, plenH);
|
||||||
|
|
||||||
|
// Helper arrays just for drawing (no closures)
|
||||||
|
var cyls = new[] { cyl1, cyl2, cyl3, cyl4 };
|
||||||
|
var runners = new[] { runner1, runner2, runner3, runner4 };
|
||||||
|
var exhausts = new[] { exh1, exh2, exh3, exh4 };
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
float cylX = plenX + plenW + 30f + i * spacing;
|
||||||
|
float runnerStartX = plenX + plenW + 5f;
|
||||||
|
float runnerEndX = cylX - 20f;
|
||||||
|
DrawPipe(target, runners[i], intakeY, runnerStartX, runnerEndX);
|
||||||
|
|
||||||
|
float cylTopY = intakeY - 120f;
|
||||||
|
DrawCylinder(target, cyls[i], cylX, cylTopY, 70f, 200f);
|
||||||
|
|
||||||
|
float exhStartX = cylX + 35f;
|
||||||
|
float exhEndX = exhStartX + 100f;
|
||||||
|
DrawPipe(target, exhausts[i], exhaustY, exhStartX, exhEndX);
|
||||||
|
|
||||||
|
var mark = new CircleShape(4f) { FillColor = Color.Magenta };
|
||||||
|
mark.Position = new Vector2f(exhEndX - 4f, exhaustY - 4f);
|
||||||
|
target.Draw(mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle symbol
|
||||||
|
var throttleRect = new RectangleShape(new Vector2f(6f, 30f))
|
||||||
|
{
|
||||||
|
FillColor = Color.Yellow,
|
||||||
|
Position = new Vector2f(plenX - 16f, intakeY - 15f)
|
||||||
|
};
|
||||||
|
target.Draw(throttleRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ namespace FluidSim.Tests
|
|||||||
|
|
||||||
protected const double AmbientPressure = 101325.0;
|
protected const double AmbientPressure = 101325.0;
|
||||||
protected const double AmbientTemperature = 300.0;
|
protected const double AmbientTemperature = 300.0;
|
||||||
|
public double Throttle { get; set; } = 0.0;
|
||||||
|
|
||||||
// ---------- Color from pressure (volumes) ----------
|
// ---------- Color from pressure (volumes) ----------
|
||||||
protected Color PressureColor(double pressurePa)
|
protected Color PressureColor(double pressurePa)
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ namespace FluidSim.Tests
|
|||||||
private int stepCount;
|
private int stepCount;
|
||||||
|
|
||||||
// ---------- Throttle control ----------
|
// ---------- Throttle control ----------
|
||||||
public double Throttle { get; set; } = 0.0;
|
public double MaxThrottleArea { get; set; } = 1 * Units.cm2; // 2 cm²
|
||||||
public double MaxThrottleArea { get; set; } = 3 * Units.cm2; // 2 cm²
|
|
||||||
|
|
||||||
public override void Initialize(int sampleRate)
|
public override void Initialize(int sampleRate)
|
||||||
{
|
{
|
||||||
@@ -50,7 +49,7 @@ namespace FluidSim.Tests
|
|||||||
|
|
||||||
// ---- Crankshaft (external, passed to cylinder) ----
|
// ---- Crankshaft (external, passed to cylinder) ----
|
||||||
crankshaft = new Crankshaft(600);
|
crankshaft = new Crankshaft(600);
|
||||||
crankshaft.Inertia = 0.1;
|
crankshaft.Inertia = 0.2;
|
||||||
crankshaft.FrictionConstant = 2;
|
crankshaft.FrictionConstant = 2;
|
||||||
crankshaft.FrictionViscous = 0.04;
|
crankshaft.FrictionViscous = 0.04;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user