220 lines
9.2 KiB
C#
220 lines
9.2 KiB
C#
using System;
|
||
using SFML.Graphics;
|
||
using SFML.System;
|
||
using FluidSim.Components;
|
||
using FluidSim.Core;
|
||
|
||
namespace FluidSim.Tests
|
||
{
|
||
public class TestScenario : Scenario
|
||
{
|
||
// Engine
|
||
private Cylinder cylinder;
|
||
|
||
// Intake side
|
||
private Pipe1D intakePipeBeforeThrottle; // pipe from ambient to plenum
|
||
private Volume0D intakePlenum; // plenum (100 mL)
|
||
private Pipe1D intakeRunner; // pipe from plenum to cylinder
|
||
|
||
// Exhaust side
|
||
private Pipe1D exhaustPipe;
|
||
|
||
// Links
|
||
private OpenEndLink intakeOpenEnd; // ambient → left end of first pipe
|
||
private OrificeLink throttleOrifice; // first pipe right end → plenum inlet (variable area)
|
||
private OrificeLink plenumToRunner; // plenum outlet → runner left end (fixed area)
|
||
private OrificeLink intakeValve; // runner right end → cylinder intake port
|
||
|
||
private OrificeLink exhaustValve;
|
||
private OpenEndLink exhaustOpenEnd;
|
||
|
||
private Solver solver;
|
||
private SoundProcessor soundProcessor;
|
||
private double dt;
|
||
private int stepCount;
|
||
|
||
public double ThrottleArea { get; set; } = 0.0; // controlled externally
|
||
|
||
public override void Initialize(int sampleRate)
|
||
{
|
||
dt = 1.0 / sampleRate;
|
||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||
|
||
solver = new Solver();
|
||
solver.SetTimeStep(dt);
|
||
solver.CflTarget = 0.9;
|
||
|
||
// ---- Cylinder (no valve overlap to avoid backflow) ----
|
||
double bore = 0.056, stroke = 0.050, conRod = 0.110, compRatio = 10.0;
|
||
double ivo = 370.0, ivc = 580.0, evo = 120.0, evc = 350.0;
|
||
cylinder = new Cylinder(bore, stroke, conRod, compRatio, ivo, ivc, evo, evc, 1000)
|
||
{
|
||
MaxIntakeArea = 0.00037,
|
||
MaxExhaustArea = 0.00037,
|
||
};
|
||
solver.AddComponent(cylinder);
|
||
|
||
double pipeArea = 0.00037; // 3.7 cm²
|
||
|
||
// ---- Pipes ----
|
||
intakePipeBeforeThrottle = new Pipe1D(0.15, pipeArea, 5); // short pipe before throttle
|
||
intakeRunner = new Pipe1D(0.10, pipeArea, 5); // runner after plenum
|
||
exhaustPipe = new Pipe1D(1.00, pipeArea, 80);
|
||
solver.AddComponent(intakePipeBeforeThrottle);
|
||
solver.AddComponent(intakeRunner);
|
||
solver.AddComponent(exhaustPipe);
|
||
|
||
// ---- Plenum (100 mL) ----
|
||
intakePlenum = new Volume0D(0.0001, 101325.0, 300.0); // 0.0001 m³
|
||
var plenumInlet = intakePlenum.CreatePort(); // from throttle
|
||
var plenumOutlet = intakePlenum.CreatePort(); // to runner
|
||
solver.AddComponent(intakePlenum);
|
||
|
||
// ---- Intake open end (ambient → left end of first pipe) ----
|
||
intakeOpenEnd = new OpenEndLink(intakePipeBeforeThrottle, isLeftEnd: true)
|
||
{
|
||
AmbientPressure = 101325.0,
|
||
Gamma = 1.4
|
||
};
|
||
solver.AddOpenEndLink(intakeOpenEnd);
|
||
|
||
// ---- Throttle orifice (first pipe right end → plenum inlet) ----
|
||
throttleOrifice = new OrificeLink(plenumInlet, intakePipeBeforeThrottle, isPipeLeftEnd: false,
|
||
areaProvider: () => ThrottleArea)
|
||
{
|
||
DischargeCoefficient = 0.1, // realistic throttle Cd
|
||
UseInertance = false
|
||
};
|
||
solver.AddOrificeLink(throttleOrifice);
|
||
|
||
// ---- Plenum → runner (fixed area = pipe area) ----
|
||
plenumToRunner = new OrificeLink(plenumOutlet, intakeRunner, isPipeLeftEnd: true,
|
||
areaProvider: () => pipeArea)
|
||
{
|
||
DischargeCoefficient = 1.0,
|
||
UseInertance = false
|
||
};
|
||
solver.AddOrificeLink(plenumToRunner);
|
||
|
||
// ---- Intake valve (runner right end → cylinder intake port) ----
|
||
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);
|
||
|
||
stepCount = 0;
|
||
Console.WriteLine("4‑Stroke 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()
|
||
{
|
||
// 1. Advance crankshaft & pre‑step
|
||
cylinder.Crankshaft.Step(dt);
|
||
cylinder.PreStep(dt);
|
||
|
||
// 2. Run solver
|
||
solver.Step();
|
||
stepCount++;
|
||
|
||
// 3. Log every 200 steps
|
||
if (stepCount % 200 == 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;
|
||
|
||
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 area = {ThrottleArea * 1e6:F2} mm², Plenum P = {plenumP:F3} bar");
|
||
}
|
||
|
||
return soundProcessor.Process(exhaustOpenEnd);
|
||
}
|
||
|
||
public override void Draw(RenderWindow target)
|
||
{
|
||
float winW = target.GetView().Size.X;
|
||
float winH = target.GetView().Size.Y;
|
||
|
||
// Fixed vertical centres for intake and exhaust
|
||
float intakeY = winH / 2f - 40f;
|
||
float exhaustY = winH / 2f + 80f;
|
||
|
||
// ---- 1. Open end (ambient air source) ----
|
||
float openEndX = 40f;
|
||
var openEndMark = new CircleShape(5f) { FillColor = Color.Cyan };
|
||
openEndMark.Position = new Vector2f(openEndX - 5f, intakeY - 5f);
|
||
target.Draw(openEndMark);
|
||
|
||
// ---- 2. First intake pipe (ambient → throttle) ----
|
||
float pipe1StartX = openEndX;
|
||
float pipe1EndX = pipe1StartX + 120f;
|
||
DrawPipe(target, intakePipeBeforeThrottle, intakeY, pipe1StartX, pipe1EndX);
|
||
|
||
// ---- 3. Throttle (symbolic restriction) ----
|
||
float throttleX = pipe1EndX + 5f;
|
||
var throttleRect = new RectangleShape(new Vector2f(8f, 30f))
|
||
{
|
||
FillColor = Color.Yellow,
|
||
Position = new Vector2f(throttleX, intakeY - 15f)
|
||
};
|
||
target.Draw(throttleRect);
|
||
|
||
// ---- 4. 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);
|
||
|
||
// ---- 5. Runner pipe (plenum → cylinder) ----
|
||
float runnerStartX = plenLeftX + plenW + 5f;
|
||
float runnerEndX = runnerStartX + 100f;
|
||
DrawPipe(target, intakeRunner, intakeY, runnerStartX, runnerEndX);
|
||
|
||
// ---- 6. Cylinder ----
|
||
float cylCX = runnerEndX + 50f; // center X
|
||
float cylTopY = intakeY - 120f; // top of cylinder (so it sits above the pipe)
|
||
float cylW = 80f, cylMaxH = 240f;
|
||
DrawCylinder(target, cylinder, cylCX, cylTopY, cylW, cylMaxH);
|
||
|
||
// ---- 7. Exhaust pipe (cylinder → open end) ----
|
||
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);
|
||
}
|
||
}
|
||
} |