config tuning

This commit is contained in:
max
2026-06-09 18:05:39 +02:00
parent 5c2a7048c8
commit aba9b76530
2 changed files with 105 additions and 155 deletions

View File

@@ -25,8 +25,7 @@ namespace FluidSim.Core
public float EffectiveLength; public float EffectiveLength;
public float CurrentMdot; // kg/s, positive = volume → pipe public float CurrentMdot; // kg/s, positive = volume → pipe
// --- Loss coefficient (linear resistance) inertance only --- // --- Loss coefficient (linear resistance) ---
// If 0 when UseInertance is true, a stable default is autocomputed at runtime.
public float LossCoefficient; // N·s/m⁵ or kg/(m⁴·s) public float LossCoefficient; // N·s/m⁵ or kg/(m⁴·s)
} }
@@ -58,10 +57,9 @@ namespace FluidSim.Core
public int OpenEndCount { get; private set; } public int OpenEndCount { get; private set; }
// ---------- Add orifice (no inertance) ---------- // ---------- Add orifice (no inertance) ----------
// Simple isentropic nozzle no builtin loss. For dissipation use pipe damping
// or the inertance model if you need a damped resonator.
public void AddOrifice(Port volumePort, int pipeIndex, bool isLeftEnd, public void AddOrifice(Port volumePort, int pipeIndex, bool isLeftEnd,
int areaIndex, float dischargeCoeff = 1f) int areaIndex, float dischargeCoeff = 1f,
float lossCoefficient = 0f)
{ {
_orifices[OrificeCount] = new OrificeDesc _orifices[OrificeCount] = new OrificeDesc
{ {
@@ -73,24 +71,22 @@ namespace FluidSim.Core
UseInertance = false, UseInertance = false,
EffectiveLength = 0f, EffectiveLength = 0f,
CurrentMdot = 0f, CurrentMdot = 0f,
LossCoefficient = 0f LossCoefficient = lossCoefficient
}; };
OrificeCount++; OrificeCount++;
} }
// ---------- Add orifice with inertance ---------- // ---------- Add orifice with inertance ----------
// effectiveLength length of the inertial slug (m), typically the physical neck length.
// lossCoefficient linear resistance (N·s/m⁵). If 0 (or omitted) an automatic stable
// value will be computed from the pipe's characteristic impedance.
public void AddOrificeWithInertance(Port volumePort, int pipeIndex, bool isLeftEnd, public void AddOrificeWithInertance(Port volumePort, int pipeIndex, bool isLeftEnd,
int areaIndex, float dischargeCoeff, int areaIndex, float dischargeCoeff,
float effectiveLength, float lossCoefficient = 0f) float effectiveLength, float lossCoefficient = 0f)
{ {
AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff); // Reuse the base AddOrifice and then override fields
AddOrifice(volumePort, pipeIndex, isLeftEnd, areaIndex, dischargeCoeff, lossCoefficient);
ref var d = ref _orifices[OrificeCount - 1]; ref var d = ref _orifices[OrificeCount - 1];
d.UseInertance = true; d.UseInertance = true;
d.EffectiveLength = effectiveLength; d.EffectiveLength = effectiveLength;
d.LossCoefficient = lossCoefficient; d.LossCoefficient = lossCoefficient; // store the linear resistance
} }
public void AddOpenEnd(int pipeIndex, bool isLeftEnd, public void AddOpenEnd(int pipeIndex, bool isLeftEnd,
@@ -150,7 +146,7 @@ namespace FluidSim.Core
? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex) ? _pipeSystem.GetInteriorAirFractionLeft(d.PipeIndex)
: _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex); : _pipeSystem.GetInteriorAirFractionRight(d.PipeIndex);
// ---- Handle closed orifice as a wall ---- // ---- Handle closed orifice (area ≈ 0) as a wall ----
if (area < 1e-12f || d.VolumePort == null) if (area < 1e-12f || d.VolumePort == null)
{ {
var (rInt, uInt, pInt) = d.IsLeftEnd var (rInt, uInt, pInt) = d.IsLeftEnd
@@ -188,10 +184,10 @@ namespace FluidSim.Core
if (d.UseInertance) if (d.UseInertance)
{ {
// ---- Inertance ODE with (possibly automatic) linear loss ----
float rhoUp = d.CurrentMdot >= 0 ? volRho : pipeRho; float rhoUp = d.CurrentMdot >= 0 ? volRho : pipeRho;
float inertance = rhoUp * d.EffectiveLength / MathF.Max(area, 1e-12f); float inertance = rhoUp * d.EffectiveLength / MathF.Max(area, 1e-12f);
float dp = volP - pipeP; float dp = volP - pipeP;
float Rlin = d.LossCoefficient;
float dmdot_dt = (dp - Rlin * d.CurrentMdot) / MathF.Max(inertance, 1e-12f); float dmdot_dt = (dp - Rlin * d.CurrentMdot) / MathF.Max(inertance, 1e-12f);
float mdotNew = d.CurrentMdot + dmdot_dt * dt; float mdotNew = d.CurrentMdot + dmdot_dt * dt;

View File

@@ -1,6 +1,7 @@
using FluidSim.Components; using FluidSim.Components;
using FluidSim.Core; using FluidSim.Core;
using FluidSim.Interfaces; using FluidSim.Interfaces;
using FluidSim.Utils;
using SFML.Graphics; using SFML.Graphics;
using SFML.System; using SFML.System;
using System; using System;
@@ -9,115 +10,75 @@ namespace FluidSim.Tests
{ {
public class SingleCylScenario : Scenario public class SingleCylScenario : Scenario
{ {
// ---------- Engine components ----------
private Crankshaft crankshaft; private Crankshaft crankshaft;
private Cylinder cylinder; private Cylinder cylinder;
// ---------- Fluid network ----------
private PipeSystem pipeSystem; private PipeSystem pipeSystem;
private BoundarySystem boundaries; private BoundarySystem boundaries;
private Solver solver; private Solver solver;
// Volumes
private Volume0D intakePlenum; private Volume0D intakePlenum;
// Ports
private Port plenumInlet, plenumOutlet; private Port plenumInlet, plenumOutlet;
private Volume0D exhaustCollector;
private Port colIn, colOut;
// Orifice / openend indices private int throttleAreaIdx, plenumRunnerAreaIdx, intakeValveIdx, exhaustValveIdx;
private int throttleAreaIdx, plenumRunnerIdx, intakeValveIdx, exhaustValveIdx;
private int intakeOpenIdx, exhaustOpenIdx;
private float[] orificeAreas; private float[] orificeAreas;
private int intakeOpenIdx, exhaustOpenIdx;
// Sound
private SoundProcessor exhaustSound, intakeSound; private SoundProcessor exhaustSound, intakeSound;
private OutdoorExhaustReverb reverb; private OutdoorExhaustReverb reverb;
// ---------- Simulation state ----------
private double dt; private double dt;
private int stepCount; private int stepCount;
public float MaxThrottleArea = 100e-4f; // 1 cm²
// ---------- Geometry (Lifan YX140) ---------- // Use a private field for the maximum throttle area, avoiding any baseclass conflicts
// Bore 56 mm, Stroke 57 mm, CR 9.5 private float _maxThrottleArea;
private const float Bore = 0.056f;
private const float Stroke = 0.057f;
private const float ConRod = 0.110f; // typical for 57 mm stroke
private const float CompressionRatio = 9.5f;
// Valve diameters (intake 27 mm, exhaust 23 mm) // pipe area for open end calculations
private const float IntakeValveDiam = 0.027f; private float pipeArea;
private const float ExhaustValveDiam = 0.023f;
private const float ValveLift = 0.006f; // 6 mm peak lift
// Valve timings (degrees, 720° fourstroke)
// Intake: 15° BTDC → 45° ABDC
private const float IVO = 345f; // 15° BTDC
private const float IVC = 585f; // 45° ABDC (180°+45°)
// Exhaust: 45° BBDC → 15° ATDC
private const float EVO = 135f; // 45° BBDC (180°-45°)
private const float EVC = 375f; // 15° ATDC (360°+15°)
// Spark advance: 30° BTDC
private const float SparkAdv = 30f;
// Pipe / plenum sizes
private const float PipeDiam = 0.025f; // 25 mm intake / exhaust
private const float PipeArea = 0.00049087f; // π*D²/4
private const float PlenumVolume = 0.0005f; // 500 mL
private const float MaxThrottleArea = 1e-4f; // ~1 cm² (fully open)
// Pipe lengths and cell counts
private const float IntakeLenBefore = 0.15f; // 15 cm before throttle
private const float RunnerLen = 0.25f; // 25 cm runner
private const float ExhaustLen = 0.60f; // 60 cm exhaust
private const int CellsBefore = 6;
private const int CellsRunner = 10;
private const int CellsExhaust = 24;
public override void Initialize(int sampleRate) public override void Initialize(int sampleRate)
{ {
dt = 1.0 / sampleRate; dt = 1.0 / sampleRate;
// ---- Crankshaft ---- // Maximum throttle area independent of base class
crankshaft = new Crankshaft(600); _maxThrottleArea = (float)Units.AreaFromDiameter(3 * Units.cm); // 1 cm²
crankshaft.Inertia = 0.05f;
crankshaft.FrictionConstant = 2f;
crankshaft.FrictionViscous = 0.01f;
// ---------- Cylinder ---------- // ---- Crankshaft ----
cylinder = new Cylinder(Bore, Stroke, ConRod, CompressionRatio, crankshaft = new Crankshaft(2000);
IVO, IVC, EVO, EVC, crankshaft) crankshaft.Inertia = 0.01f;
crankshaft.FrictionConstant = 2f;
crankshaft.FrictionViscous = 0.0f;
// ---- Cylinder ----
float bore = 0.056f, stroke = 0.057f, conRod = 0.110f, compRatio = 11f;
float ivo = 350f, ivc = 580f, evo = 120f, evc = 370f;
cylinder = new Cylinder(bore, stroke, conRod, compRatio,
ivo, ivc, evo, evc, crankshaft)
{ {
IntakeValveDiameter = IntakeValveDiam, IntakeValveDiameter = 0.03f,
ExhaustValveDiameter = ExhaustValveDiam, IntakeValveLift = 0.005f,
IntakeValveLift = ValveLift, ExhaustValveDiameter = 0.028f,
ExhaustValveLift = ValveLift, ExhaustValveLift = 0.005f
SparkAdvance = SparkAdv,
EnergyVariationFraction = 0.03f, // small cycletocycle variation
MisfireProbability = 0.0f
}; };
// ---------- Pipe system ---------- // ---- Pipe system ----
int totalCells = CellsBefore + CellsRunner + CellsExhaust; int[] pipeStart = { 0, 10, 20 };
int[] pipeStart = { 0, CellsBefore, CellsBefore + CellsRunner }; int[] pipeEnd = { 10, 20, 70 };
int[] pipeEnd = { CellsBefore, CellsBefore + CellsRunner, totalCells }; int totalCells = pipeEnd[^1]; // automatically 70, stays in sync
float[] area = new float[totalCells];
float[] areas = new float[totalCells]; float[] dx = new float[totalCells];
float[] dxs = new float[totalCells]; float pipeDiameter = 0.02f; // 2 cm
float dxBefore = IntakeLenBefore / CellsBefore; pipeArea = MathF.PI * 0.25f * pipeDiameter * pipeDiameter;
float dxRunner = RunnerLen / CellsRunner; float areaVal = pipeArea;
float dxExh = ExhaustLen / CellsExhaust; float intakeLenBefore = 0.2f, intakeLenRunner = 0.2f, exhaustLen = 0.4f;
for (int i = 0; i < totalCells; i++) for (int i = 0; i < totalCells; i++)
{ {
areas[i] = PipeArea; area[i] = areaVal;
if (i < CellsBefore) if (i < 10) dx[i] = intakeLenBefore / 10f;
dxs[i] = dxBefore; else if (i < 20) dx[i] = intakeLenRunner / 10f;
else if (i < CellsBefore + CellsRunner) else dx[i] = exhaustLen / 50f;
dxs[i] = dxRunner;
else
dxs[i] = dxExh;
} }
pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx, pipeSystem = new PipeSystem(totalCells, pipeStart, pipeEnd, area, dx,
@@ -130,45 +91,49 @@ namespace FluidSim.Tests
intakePlenum = new Volume0D(100e-6f, 101325f, 300f); // 100 mL intakePlenum = new Volume0D(100e-6f, 101325f, 300f); // 100 mL
plenumInlet = intakePlenum.CreatePort(); plenumInlet = intakePlenum.CreatePort();
plenumOutlet = intakePlenum.CreatePort(); plenumOutlet = intakePlenum.CreatePort();
exhaustCollector = new Volume0D(10e-6f, 101325f, 800f); // 10 mL (unused but present)
colIn = exhaustCollector.CreatePort();
colOut = exhaustCollector.CreatePort();
// ---------- Boundary system ---------- // ---- Boundary system ----
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2); boundaries = new BoundarySystem(pipeSystem, maxOrifices: 4, maxOpenEnds: 2);
throttleAreaIdx = 0; throttleAreaIdx = 0;
plenumRunnerIdx = 1; plenumRunnerAreaIdx = 1;
intakeValveIdx = 2; intakeValveIdx = 2;
exhaustValveIdx = 3; exhaustValveIdx = 3;
// Open ends // Intake open end (pipe0 left)
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, PipeArea); boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: true, 101325f, pipeArea);
intakeOpenIdx = 0; intakeOpenIdx = 0;
boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, PipeArea);
// Throttle orifice (plenum inlet to pipe0 right)
boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false, throttleAreaIdx, 0.2f);
// Plenum to runner (plenum outlet to pipe1 left)
boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true, plenumRunnerAreaIdx, 1f);
// Intake valve (cylinder intake to pipe1 right)
boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false, intakeValveIdx, 1f);
// Exhaust valve (cylinder exhaust to pipe2 left)
boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true, exhaustValveIdx, 1f);
// Exhaust open end (pipe2 right)
boundaries.AddOpenEnd(pipeIndex: 2, isLeftEnd: false, 101325f, pipeArea);
exhaustOpenIdx = 1; exhaustOpenIdx = 1;
// Orifices
// throttle variable area, low discharge for restriction
boundaries.AddOrifice(plenumInlet, pipeIndex: 0, isLeftEnd: false,
throttleAreaIdx, dischargeCoeff: 0.8f);
// plenum → runner
boundaries.AddOrifice(plenumOutlet, pipeIndex: 1, isLeftEnd: true,
plenumRunnerIdx, dischargeCoeff: 1.0f);
// intake valve
boundaries.AddOrifice(cylinder.IntakePort, pipeIndex: 1, isLeftEnd: false,
intakeValveIdx, dischargeCoeff: 1.0f);
// exhaust valve
boundaries.AddOrifice(cylinder.ExhaustPort, pipeIndex: 2, isLeftEnd: true,
exhaustValveIdx, dischargeCoeff: 1.0f);
orificeAreas = new float[4]; orificeAreas = new float[4];
orificeAreas[plenumRunnerIdx] = PipeArea; // fixed fullbore orificeAreas[plenumRunnerAreaIdx] = areaVal; // fixed plenum->runner area
// ---------- Solver ---------- // ---- Solver ----
solver = new Solver { SubStepCount = 5, EnableProfiling = false }; solver = new Solver { SubStepCount = 4, EnableProfiling = false };
solver.SetTimeStep(dt); solver.SetTimeStep(dt);
solver.SetPipeSystem(pipeSystem); solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries); solver.SetBoundarySystem(boundaries);
solver.AddComponent(cylinder); solver.AddComponent(cylinder);
solver.AddComponent(intakePlenum); solver.AddComponent(intakePlenum);
solver.AddComponent(exhaustCollector);
// ---- Sound ---- // ---- Sound ----
exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 20f }; exhaustSound = new SoundProcessor(sampleRate, 1f) { Gain = 20f };
@@ -176,37 +141,35 @@ namespace FluidSim.Tests
reverb = new OutdoorExhaustReverb(sampleRate); reverb = new OutdoorExhaustReverb(sampleRate);
stepCount = 0; stepCount = 0;
Console.WriteLine("Singlecylinder engine (YX140) ready."); Console.WriteLine("TestScenario ready.");
} }
public override float Process() public override float Process()
{ {
// ---- Crank and cylinder prestep ----
crankshaft.Step((float)dt); crankshaft.Step((float)dt);
cylinder.PreStep((float)dt); cylinder.PreStep((float)dt);
// ---- Update variable areas ---- // Update variable orifice areas use the private _maxThrottleArea
float throttledArea = MaxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1.0f); float throttledArea = _maxThrottleArea * Math.Clamp(Throttle, 0.0001f, 1f);
orificeAreas[throttleAreaIdx] = throttledArea; orificeAreas[throttleAreaIdx] = throttledArea;
orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea; orificeAreas[intakeValveIdx] = cylinder.IntakeValveArea;
orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea; orificeAreas[exhaustValveIdx] = cylinder.ExhaustValveArea;
boundaries.SetOrificeAreas(orificeAreas); boundaries.SetOrificeAreas(orificeAreas);
// ---- Fluids step ----
solver.Step(); solver.Step();
stepCount++; stepCount++;
// ---- Sound ---- // Retrieve openend mass flows for sound synthesis
float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx); float exhaustFlow = boundaries.GetOpenEndMassFlow(exhaustOpenIdx);
float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx); float intakeFlow = boundaries.GetOpenEndMassFlow(intakeOpenIdx);
float exhaustDry = exhaustSound.Process(exhaustFlow); float exhaustDry = exhaustSound.Process(exhaustFlow);
float intakeDry = intakeSound.Process(intakeFlow); float intakeDry = intakeSound.Process(intakeFlow);
if (stepCount % 2000 == 0) if (stepCount % 1000 == 0)
{ {
float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI); float rpm = crankshaft.AngularVelocity * 60f / (2f * MathF.PI);
float crankDeg = crankshaft.CrankAngle; // public property on Cylinder float crankDeg = crankshaft.CrankAngle; // degrees (0720)
Console.WriteLine($"Step {stepCount}, CA={crankDeg:F1} deg, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar"); Console.WriteLine($"Step {stepCount}, CA={crankDeg:F1} deg, RPM={rpm:F0}, CylP={cylinder.Pressure / 1e5f:F2} bar");
Console.WriteLine($" intake flow: {intakeFlow:F6}, exhaust flow: {exhaustFlow:F6}"); Console.WriteLine($" intake flow: {intakeFlow:F6}, exhaust flow: {exhaustFlow:F6}");
@@ -240,53 +203,44 @@ namespace FluidSim.Tests
float intakeY = winH / 2f - 40f; float intakeY = winH / 2f - 40f;
float exhaustY = winH / 2f + 80f; float exhaustY = winH / 2f + 80f;
float leftX = 40f; float openEndX = 40f;
// Intake open end marker // Intake pipe before throttle (pipe 0)
var om = new CircleShape(5f) { FillColor = Color.Cyan }; float pipe1StartX = openEndX;
om.Position = new Vector2f(leftX - 5f, intakeY - 5f); float pipe1EndX = pipe1StartX + 120f;
target.Draw(om); DrawPipe(target, pipeSystem, 0, intakeY, pipe1StartX, pipe1EndX);
// Pipe 0 before throttle
float p0EndX = leftX + 80f;
DrawPipe(target, pipeSystem, 0, intakeY, leftX, p0EndX);
// Throttle symbol // Throttle symbol
float thrX = p0EndX + 5f; float throttleX = pipe1EndX + 5f;
var thr = new RectangleShape(new Vector2f(8f, 30f)) var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{ {
FillColor = Color.Yellow, FillColor = Color.Yellow,
Position = new Vector2f(thrX, intakeY - 15f) Position = new Vector2f(throttleX, intakeY - 15f)
}; };
target.Draw(thr); target.Draw(throttleRect);
// Plenum volume // Plenum
float plenW = 60f, plenH = 50f; float plenW = 60f, plenH = 80f;
float plenLeftX = thrX + 12f; float plenLeftX = throttleX + 10f;
float plenCenterX = plenLeftX + plenW / 2f; float plenCenterX = plenLeftX + plenW / 2f;
float plenTopY = intakeY - plenH / 2f; float plenTopY = intakeY - plenH / 2f;
DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH); DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH);
// Pipe 1 runner // Runner pipe (pipe 1)
float rStartX = plenLeftX + plenW + 10f; float runnerStartX = plenLeftX + plenW + 5f;
float rEndX = rStartX + 100f; float runnerEndX = runnerStartX + 100f;
DrawPipe(target, pipeSystem, 1, intakeY, rStartX, rEndX); DrawPipe(target, pipeSystem, 1, intakeY, runnerStartX, runnerEndX);
// Cylinder // Cylinder
float cylCX = rEndX + 50f; float cylCX = runnerEndX + 50f;
float cylTopY = intakeY - 120f; float cylTopY = intakeY - 120f;
float cylW = 80f, cylMaxH = 240f; float cylW = 80f, cylMaxH = 240f;
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH); DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
// Pipe 2 exhaust // Exhaust pipe (pipe 2)
float exhStartX = cylCX + cylW / 2f + 20f; float exhStartX = cylCX + cylW / 2f + 20f;
float exhEndX = winW - 60f; float exhEndX = winW - 60f;
DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, exhEndX); DrawPipe(target, pipeSystem, 2, exhaustY, exhStartX, exhEndX);
// Exhaust open end
var em = new CircleShape(5f) { FillColor = Color.Magenta };
em.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f);
target.Draw(em);
} }
} }
} }