Helmholtz testing (no decay bug)

This commit is contained in:
max
2026-05-09 01:44:35 +02:00
parent 9c9e23147a
commit 77ef4753a3
23 changed files with 1811 additions and 2118 deletions

View File

@@ -1,176 +1,91 @@
using System;
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
using FluidSim.Utils;
namespace FluidSim.Tests
{
public class TestScenario : Scenario
{
// Engine
private Cylinder cylinder;
private Crankshaft crankshaft;
// Intake side
private Pipe1D intakePipeBeforeThrottle;
private Volume0D intakePlenum; // 5 mL
private Pipe1D intakeRunner;
// Exhaust side
private Pipe1D exhaustPipe;
// Links
private OpenEndLink intakeOpenEnd;
private OrificeLink throttleOrifice;
private OrificeLink plenumToRunner;
private OrificeLink intakeValve;
private OrificeLink exhaustValve;
private OpenEndLink exhaustOpenEnd;
private PipeSystem pipeSystem;
private BoundarySystem boundaries;
private Solver solver;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private int[] pipeStart = { 0 };
private int[] pipeEnd;
private double dt;
private int stepCount;
// ---------- Throttle control ----------
public double MaxThrottleArea { get; set; } = 1 * Units.cm2; // 2 cm²
// Sound output: use pressure at open end
private SoundProcessor openEndSound;
private int openEndIdx = 0; // index of the open end in BoundarySystem (we added only one)
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
solver = new Solver();
const int cellCount = 200;
float length = 2f;
float dia = 0.02f;
float area = MathF.PI * 0.25f * dia * dia;
float[] areas = new float[cellCount];
float[] dxs = new float[cellCount];
float dx = length / cellCount;
for (int i = 0; i < cellCount; i++)
{
areas[i] = area;
dxs[i] = dx;
}
pipeEnd = new[] { cellCount };
float rho0 = 101325f / (287f * 300f);
pipeSystem = new PipeSystem(cellCount, pipeStart, pipeEnd, areas, dxs,
rho0, 0f, 101325f);
pipeSystem.DampingMultiplier = 0f;
pipeSystem.EnergyRelaxationRate = 0f;
pipeSystem.AmbientPressure = 101325f;
// Pressure bubble near right end
float pBubble = 10f * 101325f;
float TBubble = 2000f;
float rhoBubble = pBubble / (287f * TBubble);
for (int i = 0; i <= 10; i++)
pipeSystem.SetCellState(i, rhoBubble, 0f, pBubble);
// Boundaries: left closed, right open
boundaries = new BoundarySystem(pipeSystem, maxOrifices: 1, maxOpenEnds: 1);
boundaries.AddOrifice(null, pipeIndex: 0, isLeftEnd: true, areaIndex: 0, 1f);
boundaries.AddOpenEnd(pipeIndex: 0, isLeftEnd: false, 101325f, area);
float[] orificeAreas = new float[1] { 0f };
boundaries.SetOrificeAreas(orificeAreas);
solver = new Solver { SubStepCount = 3};
solver.SetTimeStep(dt);
solver.CflTarget = 0.9;
solver.SetPipeSystem(pipeSystem);
solver.SetBoundarySystem(boundaries);
// ---- Crankshaft (external, passed to cylinder) ----
crankshaft = new Crankshaft(600);
crankshaft.Inertia = 0.2;
crankshaft.FrictionConstant = 2;
crankshaft.FrictionViscous = 0.04;
solver.EnableProfiling = true;
pipeSystem.EnableProfiling = true;
// ---- Cylinder ----
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;
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, crankshaft)
{
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.1f };
intakeSoundProcessor = new SoundProcessor(sampleRate, 1, pipeDiameter) { Gain = 0.1f };
reverb = new OutdoorExhaustReverb(sampleRate);
// ---- Pipes ----
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);
intakePlenum = new Volume0D(5 * Units.mL, 101325.0, 300.0);
var plenumInlet = intakePlenum.CreatePort();
var plenumOutlet = intakePlenum.CreatePort();
solver.AddComponent(intakePlenum);
// ---- Intake open end ----
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(intakeOpenEnd);
// ---- Throttle orifice (variable area) ----
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
areaProvider: () => MaxThrottleArea * Math.Clamp(Throttle, 0.0001, 1))
{
DischargeCoefficient = 0.2,
UseInertance = false
};
solver.AddOrificeLink(throttleOrifice);
// ---- Plenum to runner (fixed area) ----
plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true,
areaProvider: () => pipeArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(plenumToRunner);
// ---- Intake valve ----
intakeValve = new OrificeLink(cylinder.IntakePort, intakeRunner, isPipeLeftEnd: false,
areaProvider: () => cylinder.IntakeValveArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(intakeValve);
// ---- Exhaust valve ----
exhaustValve = new OrificeLink(cylinder.ExhaustPort, exhaustPipe, isPipeLeftEnd: true,
areaProvider: () => cylinder.ExhaustValveArea)
{
DischargeCoefficient = 1.0,
UseInertance = false
};
solver.AddOrificeLink(exhaustValve);
// ---- Exhaust open end ----
exhaustOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(exhaustOpenEnd);
// Simple sound processor: convert mass flow rate to audio
openEndSound = new SoundProcessor(sampleRate, 1f) { Gain = 2f };
Console.WriteLine("Pulse test ready.");
stepCount = 0;
Console.WriteLine("4Stroke engine test (plenum + two pipes)");
Console.WriteLine($"Bore {bore * 1000:F0}mm, Stroke {stroke * 1000:F0}mm, CR {compRatio}");
Console.WriteLine($"IVO {ivo}°, IVC {ivc}°, EVO {evo}°, EVC {evc}° (no overlap)");
}
public override float Process()
{
cylinder.Crankshaft.Step(dt);
cylinder.PreStep(dt);
solver.Step();
stepCount++;
if (stepCount % 10000 == 0)
{
double crankDeg = cylinder.Crankshaft.CrankAngle * 180.0 / Math.PI % 720.0;
double cylP = cylinder.Pressure / 1e5;
double cylT = cylinder.Temperature;
double cylMass = cylinder.Mass * 1e6;
double mdotI = intakeValve.LastMassFlowRate;
double mdotE = exhaustValve.LastMassFlowRate;
double pipeR = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
double plenumP = intakePlenum.Pressure / 1e5;
double actualArea = MaxThrottleArea * Throttle;
float flow = boundaries.GetOpenEndMassFlow(openEndIdx);
float sample = openEndSound.Process(flow);
Console.WriteLine($"Step {stepCount}: Angle={crankDeg:F1}°, " +
$"CylP={cylP:F2} bar, T={cylT:F0} K, mass={cylMass:F1} mg, " +
$"mdotI={mdotI:E4} kg/s, mdotE={mdotE:E4} kg/s, PipeR={pipeR:F2} bar");
Console.WriteLine($"Throttle = {Throttle * 100:F0}% area = {actualArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
}
float exhaustDry = exhaustSoundProcessor.Process(exhaustOpenEnd);
float intakeDry = intakeSoundProcessor.Process(intakeOpenEnd);
return reverb.Process(exhaustDry + intakeDry);
return sample;
}
public override void Draw(RenderWindow target)
@@ -178,56 +93,10 @@ namespace FluidSim.Tests
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float intakeY = winH / 2f - 40f;
float exhaustY = winH / 2f + 80f;
// Open end marker
float openEndX = 40f;
var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan };
openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f);
target.Draw(openEndMark);
// First intake pipe
float pipe1StartX = openEndX;
float pipe1EndX = pipe1StartX + 120f;
DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX);
// Throttle symbol
float throttleX = pipe1EndX + 5f;
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
{
FillColor = Color.Yellow,
Position = new Vector2f(throttleX, intakeY - 15f)
};
target.Draw(throttleRect);
// Plenum
float plenW = 60f, plenH = 80f;
float plenLeftX = throttleX + 10f;
float plenCenterX = plenLeftX + plenW / 2f;
float plenTopY = intakeY - plenH / 2f;
DrawVolume(target, intakePlenum, plenCenterX, plenTopY, plenW, plenH);
// Runner pipe
float runnerStartX = plenLeftX + plenW + 5f;
float runnerEndX = runnerStartX + 100f;
DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX);
// Cylinder
float cylCX = runnerEndX + 50f;
float cylTopY = intakeY - 120f;
float cylW = 80f, cylMaxH = 240f;
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
// Exhaust pipe
float exhStartX = cylCX + cylW / 2f + 20f;
float exhEndX = winW - 60f;
DrawPipe(target, exhaustPipe, exhaustY, exhStartX, exhEndX);
// Exhaust open end marker
var exhOpenEndMark = new CircleShape(5f) { FillColor = Color.Magenta };
exhOpenEndMark.Position = new Vector2f(exhEndX - 5f, exhaustY - 5f);
target.Draw(exhOpenEndMark);
float startX = 50f;
float endX = winW - 50f;
float y = winH / 2f;
DrawPipe(target, pipeSystem, 0, y, startX, endX);
}
}
}