seemingly working, added display text

This commit is contained in:
2026-05-07 16:37:12 +02:00
parent f79cf6b7eb
commit 14f5ba925f
10 changed files with 288 additions and 161 deletions

View File

@@ -51,11 +51,11 @@ namespace FluidSim.Components
/// Set the pressure to a specific value while keeping the current temperature constant. /// Set the pressure to a specific value while keeping the current temperature constant.
/// Updates Mass and InternalEnergy accordingly. /// Updates Mass and InternalEnergy accordingly.
/// </summary> /// </summary>
public void SetPressure(double pressure) public void SetPressure(double pressure, double? temperature = null)
{ {
double V = Math.Max(Volume, 1e-12); double V = Math.Max(Volume, 1e-12);
double currentT = Temperature; // current temperature before changes double T = temperature ?? Temperature;
double rho = pressure / (GasConstant * currentT); double rho = pressure / (GasConstant * T);
Mass = rho * V; Mass = rho * V;
InternalEnergy = pressure * V / (Gamma - 1.0); InternalEnergy = pressure * V / (Gamma - 1.0);
} }

View File

@@ -4,27 +4,18 @@ using FluidSim.Interfaces;
namespace FluidSim.Core namespace FluidSim.Core
{ {
/// <summary>
/// Connects a port (volume or atmosphere) to one end of a pipe via an orifice.
/// Uses the isentropic nozzle model for the steadystate relationship,
/// and includes acoustic inertance for dynamic (Helmholtz) behaviour.
/// </summary>
public class OrificeLink public class OrificeLink
{ {
public Port VolumePort { get; } public Port? VolumePort { get; }
public Pipe1D Pipe { get; } public Pipe1D Pipe { get; }
public bool IsPipeLeftEnd { get; } public bool IsPipeLeftEnd { get; }
public Func<double> AreaProvider { get; set; } public Func<double> AreaProvider { get; set; }
public double DischargeCoefficient { get; set; } = 0.62; public double DischargeCoefficient { get; set; } = 0.62;
// Acoustic length (wall thickness + end correction) controls the resonance frequency public double EffectiveLength { get; set; } = 0.001;
public double EffectiveLength { get; set; } = 0.001; // 1 mm
// Whether to include inertance; if false, uses the steadystate nozzle model directly
public bool UseInertance { get; set; } = true; public bool UseInertance { get; set; } = true;
// Current mass flow (kg/s, positive = volume → pipe) private double _mdot; // positive = volume → pipe
private double _mdot;
public double LastMassFlowRate { get; private set; } public double LastMassFlowRate { get; private set; }
public double LastFaceDensity { get; private set; } public double LastFaceDensity { get; private set; }
@@ -33,7 +24,7 @@ namespace FluidSim.Core
public OrificeLink(Port? volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func<double> areaProvider) public OrificeLink(Port? volumePort, Pipe1D pipe, bool isPipeLeftEnd, Func<double> areaProvider)
{ {
VolumePort = volumePort; // null is allowed VolumePort = volumePort;
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe)); Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
IsPipeLeftEnd = isPipeLeftEnd; IsPipeLeftEnd = isPipeLeftEnd;
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider)); AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
@@ -43,20 +34,18 @@ namespace FluidSim.Core
public void Resolve(double dtSub) public void Resolve(double dtSub)
{ {
double area = AreaProvider(); double area = AreaProvider();
// Closed wall or missing volume port => reflective boundary
if (area < 1e-12 || VolumePort == null) if (area < 1e-12 || VolumePort == null)
{ {
SetClosedWall(); SetClosedWall();
return; return;
} }
// Gather volume state // Gather states
double volP = VolumePort.Pressure; double volP = VolumePort.Pressure;
double volRho = VolumePort.Density; double volRho = VolumePort.Density;
double volT = VolumePort.Temperature; double volT = VolumePort.Temperature;
double volH = VolumePort.SpecificEnthalpy; double volH = VolumePort.SpecificEnthalpy;
// Gather pipe interior state at the connected end
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd (double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
? Pipe.GetInteriorStateLeft() ? Pipe.GetInteriorStateLeft()
: Pipe.GetInteriorStateRight(); : Pipe.GetInteriorStateRight();
@@ -65,24 +54,23 @@ namespace FluidSim.Core
double gamma = 1.4; double gamma = 1.4;
double R = 287.0; double R = 287.0;
// ---- Steadystate mass flow from isentropic nozzle ---- // ---- 1. Steadystate nozzle solution (gives correct exit pressure) ----
double mdotSS; // positive = volume → pipe double mdotSS;
double rhoFace, uFace, pFace; double rhoFace0, uFace0, pFace0;
if (volP >= pipeP) if (volP >= pipeP)
{ {
IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient, IsentropicOrifice.Compute(volP, volRho, volT, pipeP, gamma, R, area, DischargeCoefficient,
out double mdotUpToDown, out rhoFace, out uFace, out pFace); out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
mdotSS = mdotUpToDown; // volume → pipe mdotSS = mdotUpToDown; // volume → pipe
} }
else else
{ {
IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient, IsentropicOrifice.Compute(pipeP, pipeRho, pipeT, volP, gamma, R, area, DischargeCoefficient,
out double mdotUpToDown, out rhoFace, out uFace, out pFace); out double mdotUpToDown, out rhoFace0, out uFace0, out pFace0);
mdotSS = -mdotUpToDown; // pipe → volume → negative for volume→pipe convention mdotSS = -mdotUpToDown; // pipe → volume → negative for volume→pipe convention
} }
// ---- Inertance ODE (optional) ---- // ---- 2. Inertance dynamics ----
if (UseInertance) if (UseInertance)
{ {
double rhoUp = _mdot >= 0 ? volRho : pipeRho; double rhoUp = _mdot >= 0 ? volRho : pipeRho;
@@ -97,35 +85,31 @@ namespace FluidSim.Core
_mdot = mdotSS; _mdot = mdotSS;
} }
// Clamp outflow to available mass (if finite volume) // Clamp outflow to available mass
if (VolumePort.Owner is Volume0D vol) if (VolumePort.Owner is Volume0D vol)
{ {
double maxOut = vol.Mass / dtSub; double maxOut = vol.Mass / dtSub;
if (_mdot > maxOut) _mdot = maxOut; if (_mdot > maxOut) _mdot = maxOut;
} }
// ---- Ghost state ---- // ---- 3. Ghost state (use nozzleexit pressure!) ----
// Density = upstream density (consistent with current flow direction) double rhoFace = _mdot >= 0 ? volRho : pipeRho; // upstream density
rhoFace = _mdot >= 0 ? volRho : pipeRho; double pFace = pFace0; // correct exit pressure (choked/subsonic)
// Pressure = downstream pressure (consistent with nozzle exit)
pFace = _mdot >= 0 ? pipeP : volP;
// Velocity magnitude derived from actual mass flow
double mdotMag = Math.Abs(_mdot); double mdotMag = Math.Abs(_mdot);
uFace = mdotMag / (rhoFace * area); double uFace = mdotMag / (rhoFace * area);
if (IsPipeLeftEnd) if (IsPipeLeftEnd)
uFace = _mdot >= 0 ? uFace : -uFace; // left end: positive u = into pipe uFace = _mdot >= 0 ? uFace : -uFace; // left: +u into pipe
else else
uFace = _mdot >= 0 ? -uFace : uFace; // right end: positive u = out of pipe uFace = _mdot >= 0 ? -uFace : uFace; // right: +u out of pipe
// Apply ghost to pipe
if (IsPipeLeftEnd) if (IsPipeLeftEnd)
Pipe.SetGhostLeft(rhoFace, uFace, pFace); Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else else
Pipe.SetGhostRight(rhoFace, uFace, pFace); Pipe.SetGhostRight(rhoFace, uFace, pFace);
// ---- Store results ---- // Store for monitoring
double mdotIntoVolume = -_mdot; // positive = into volume double mdotIntoVolume = -_mdot;
LastMassFlowRate = mdotIntoVolume; LastMassFlowRate = mdotIntoVolume;
LastFaceDensity = rhoFace; LastFaceDensity = rhoFace;
LastFaceVelocity = uFace; LastFaceVelocity = uFace;
@@ -133,13 +117,12 @@ namespace FluidSim.Core
VolumePort.MassFlowRate = mdotIntoVolume; VolumePort.MassFlowRate = mdotIntoVolume;
// Enthalpy for volume integration if (mdotIntoVolume >= 0)
if (mdotIntoVolume >= 0) // inflow → pipe enthalpy
{ {
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12); double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
VolumePort.SpecificEnthalpy = hPipe; VolumePort.SpecificEnthalpy = hPipe;
} }
else // outflow → volume's own enthalpy else
{ {
VolumePort.SpecificEnthalpy = volH; VolumePort.SpecificEnthalpy = volH;
} }
@@ -160,7 +143,6 @@ namespace FluidSim.Core
LastFaceDensity = rInt; LastFaceDensity = rInt;
LastFaceVelocity = 0.0; LastFaceVelocity = 0.0;
LastFacePressure = pInt; LastFacePressure = pInt;
// Don't touch VolumePort if it's null
if (VolumePort != null) if (VolumePort != null)
VolumePort.MassFlowRate = 0.0; VolumePort.MassFlowRate = 0.0;
} }

View File

@@ -10,7 +10,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SFML.Net" Version="3.0.0" /> <PackageReference Include="SFML.Net" Version="3.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="fonts\LiberationMono-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -13,9 +13,8 @@ public class Program
private const double DrawFrequency = 60.0; private const double DrawFrequency = 60.0;
private static Scenario scenario; private static Scenario scenario;
// Speed control (existing + new throttle) // Speed control
private static double desiredSpeed = 0.01; private static double desiredSpeed = 0.01;
//private static double desiredSpeed = 1.0;
private static double currentSpeed = desiredSpeed; private static double currentSpeed = desiredSpeed;
private const double MinSpeed = 0.0001; private const double MinSpeed = 0.0001;
private const double MaxSpeed = 1.0; private const double MaxSpeed = 1.0;
@@ -24,22 +23,47 @@ public class Program
private static double lastDesiredSpeed = 0.1; private static double lastDesiredSpeed = 0.1;
private static bool isRealTime = false; private static bool isRealTime = false;
// Throttle smoothing // Throttle smoothing (unused but kept)
private static double targetThrottle = 0.0; // 1.0 when W is pressed, 0.0 otherwise private static double targetThrottle = 0.0;
private static double currentThrottle = 0.0; private static double currentThrottle = 0.0;
private const double ThrottleSmoothing = 20.0; // rate of change private const double ThrottleSmoothing = 20.0;
private static volatile bool running = true; private static volatile bool running = true;
// ---- Overlay text ----
private static Font? overlayFont;
private static Text? overlayText;
public static void Main() public static void Main()
{ {
var mode = new VideoMode(new Vector2u(1280, 720)); var mode = new VideoMode(new Vector2u(1280, 720));
var window = new RenderWindow(mode, "FluidSim - Engine (W = throttle)"); var window = new RenderWindow(mode, "FluidSim");
window.SetVerticalSyncEnabled(true); window.SetVerticalSyncEnabled(true);
window.Closed += (_, _) => { running = false; window.Close(); }; window.Closed += (_, _) => { running = false; window.Close(); };
window.MouseWheelScrolled += OnMouseWheel; window.MouseWheelScrolled += OnMouseWheel;
window.KeyPressed += OnKeyPressed; window.KeyPressed += OnKeyPressed;
// ---- Load font ----
try
{
overlayFont = new Font("fonts/FiraCodeNerdFont-Medium.ttf");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load font 'fonts/LiberationMono-Regular.ttf': {ex.Message}");
overlayFont = null; // will skip text drawing
}
if (overlayFont != null)
{
// SFML 3 Text(font, character size in pixels)
overlayText = new Text(overlayFont)
{
FillColor = Color.White,
Position = new Vector2f(10, 10)
};
}
var soundEngine = new SoundEngine(bufferCapacity: 16384); var soundEngine = new SoundEngine(bufferCapacity: 16384);
soundEngine.Volume = 100; soundEngine.Volume = 100;
soundEngine.Start(); soundEngine.Start();
@@ -74,7 +98,6 @@ public class Program
double speedSmoothing = 8.0; double speedSmoothing = 8.0;
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock)); currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
// Generate audio // Generate audio
double targetAudioClock = currentRealTime + 0.05; double targetAudioClock = currentRealTime + 0.05;
while (totalOutputSamples < targetAudioClock * SampleRate && running) while (totalOutputSamples < targetAudioClock * SampleRate && running)
@@ -116,21 +139,30 @@ public class Program
break; break;
} }
// Drawing & title // Drawing
if (currentRealTime - lastDrawTime >= drawInterval) if (currentRealTime - lastDrawTime >= drawInterval)
{ {
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate); double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
double simTime = totalSimSteps / (double)SampleRate; double realtimePercent = totalOutputSamples / (currentRealTime * SampleRate) * 100.0;
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released"; // Update overlay text
window.SetTitle( if (overlayText != null)
$"{toggleHint} {throttleHint} " + {
$"Thr: {currentThrottle:F2} " + string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
$"Speed: {currentSpeed:F3}x → {desiredSpeed:F3}x " + string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
$"Act: {actualSpeed:F2}x" overlayText.DisplayedString =
); $"{toggleHint} {throttleHint} " +
$"Speed: {currentSpeed:F3}x " +
$"RT: {realtimePercent:F1}%";
}
window.Clear(Color.Black); window.Clear(Color.Black);
scenario.Draw(window); scenario.Draw(window);
// Draw the overlay on top
if (overlayText != null)
window.Draw(overlayText);
window.Display(); window.Display();
lastDrawTime = currentRealTime; lastDrawTime = currentRealTime;
} }
@@ -140,7 +172,6 @@ public class Program
window.Dispose(); window.Dispose();
} }
// (Mouse wheel, space toggle unchanged)
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e) private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
{ {
bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6; bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;

View File

@@ -19,34 +19,37 @@ namespace FluidSim.Tests
// ---------- Shared drawing helpers ---------- // ---------- Shared drawing helpers ----------
protected const double AmbientPressure = 101325.0; protected const double AmbientPressure = 101325.0;
protected const double AmbientTemperature = 300.0; // K
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary> /// <summary>Map temperature [0K … 2000K] to a color: blue (0K) → green (300K) → red (2000K).</summary>
protected Color PressureColor(double pressure) protected Color TemperatureColor(double temperature)
{ {
double range = AmbientPressure * 0.05; // ±5% gives full colour swing // Clamp to the range we want to display
double t = (pressure - AmbientPressure) / range; double t = Math.Clamp(temperature, 0.0, 2000.0);
t = Math.Clamp(t, -1.0, 1.0);
byte r, g, b; byte r, g, b;
if (t < 0) if (t < AmbientTemperature)
{ {
double factor = -t; // Blue → Green
double factor = t / AmbientTemperature; // 0 at 0K, 1 at 300K
r = 0; r = 0;
g = (byte)(255 * (1 - factor)); g = (byte)(255 * factor);
b = (byte)(255 * factor); b = (byte)(255 * (1.0 - factor));
} }
else else
{ {
double factor = t; // Green → Red
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); // 0 at 300K, 1 at 2000K
r = (byte)(255 * factor); r = (byte)(255 * factor);
g = (byte)(255 * (1 - factor)); g = (byte)(255 * (1.0 - factor));
b = 0; b = 0;
} }
return new Color(r, g, b); return new Color(r, g, b);
} }
/// <summary> /// <summary>
/// Draws the pipe as a smooth trianglestrip whose radius varies with cell pressure. /// Draws the pipe as a smooth trianglestrip whose radius varies with cell pressure (for visibility),
/// but colored by temperature.
/// </summary> /// </summary>
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX) protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
{ {
@@ -57,8 +60,8 @@ namespace FluidSim.Tests
float dx = pipeLengthPx / (n - 1); // spacing between cell centres float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float baseRadius = 25f; float baseRadius = 25f;
float rangeFactor = 1f; float rangeFactor = 2f;
float scaleFactor = 5f; float scaleFactor = 2f;
// ----- smoothstep helper ----- // ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x) static float SmoothStep(float edge0, float edge1, float x)
@@ -67,12 +70,19 @@ namespace FluidSim.Tests
return t * t * (3f - 2f * t); return t * t * (3f - 2f * t);
} }
// ----- Precompute cell positions and radii ----- // ----- Precompute cell positions, radii, and temperatures -----
var centers = new float[n]; var centers = new float[n];
var radii = new float[n]; var radii = new float[n];
var temperatures = new double[n];
double R_gas = 287.0;
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
double p = pipe.GetCellPressure(i); double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / Math.Max(rho * R_gas, 1e-12); // ideal gas
temperatures[i] = T;
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor); float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor); radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f; if (radii[i] < 2f) radii[i] = 2f;
@@ -89,8 +99,7 @@ namespace FluidSim.Tests
{ {
float x = centers[i]; float x = centers[i];
float r = radii[i]; float r = radii[i];
double p = pipe.GetCellPressure(i); Color col = TemperatureColor(temperatures[i]);
Color col = PressureColor(p);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col); stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY - r), col);
stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col); stripVertices[idx++] = new Vertex(new Vector2f(x, pipeCenterY + r), col);
@@ -103,8 +112,8 @@ namespace FluidSim.Tests
float st = SmoothStep(0f, 1f, t); float st = SmoothStep(0f, 1f, t);
float xi = centers[i] + (centers[i + 1] - centers[i]) * t; float xi = centers[i] + (centers[i + 1] - centers[i]) * t;
float ri = radii[i] + (radii[i + 1] - radii[i]) * st; float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
double pi = pipe.GetCellPressure(i) * (1 - t) + pipe.GetCellPressure(i + 1) * t; double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; // linear interpolation
Color coli = PressureColor(pi); Color coli = TemperatureColor(Ti);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli); stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli); stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);

View File

@@ -9,124 +9,222 @@ namespace FluidSim.Tests
{ {
public class TestScenario : Scenario public class TestScenario : Scenario
{ {
// Simulation core
private Solver solver; private Solver solver;
private Volume0D volume;
private Pipe1D pipe;
private OrificeLink orifice; // volume → pipe left
private OpenEndLink openEnd; // pipe right → atmosphere
private SoundProcessor soundProcessor;
private int stepCount;
// Pressure reset scheduling
private double simTime;
private double dt; private double dt;
private double resetInterval = 0.2; // seconds between resets
private double nextResetTime; // Engine components
private double targetPressure = 10 * Units.atm; private Volume0D cylinder;
private double rampDuration = 0.002; // 2 ms ramp private Pipe1D exhaustPipe;
private double rampStartTime; private OrificeLink exhaustPort;
private double rampStartPressure; // pressure when ramp started private OpenEndLink pipeOpenEnd;
private bool ramping; private Crankshaft crankshaft;
// Audio
private SoundProcessor soundProcessor;
// Engine geometry (Suzuki TS125 Jones Appendix 1)
private const double Bore = 0.056; // m
private const double Stroke = 0.050; // m
private const double ConRodLength = 0.110; // m (typical)
private const double CrankRadius = Stroke / 2.0;
private const double Obliquity = CrankRadius / ConRodLength;
private const double CompressionRatio = 6.7; // from Jones
// Derived volumes
private double sweptVolume;
private double clearanceVolume;
// Port timing (degrees from TDC)
private const double ExhaustPortOpens = 98.0; // °ATDC
private const double ExhaustPortCloses = 262.0; // °ATDC
private const double PortWidth = 0.025; // m (estimated)
private const double MaxPortArea = 0.001; // m² (fully open)
// Engine state
private double crankAngle; // rad
private double engineSpeed; // rad/s
private bool combustionPending; // true when ready to fire at TDC
// Logging
private int stepCount;
public override void Initialize(int sampleRate) public override void Initialize(int sampleRate)
{ {
dt = 1.0 / sampleRate; dt = 1.0 / sampleRate;
soundProcessor = new SoundProcessor(sampleRate, 1); // Audio
soundProcessor.Gain = 2.0f; // lower gain to avoid clipping soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
// Solver
solver = new Solver(); solver = new Solver();
solver.SetTimeStep(dt); solver.SetTimeStep(dt);
solver.CflTarget = 0.4; solver.CflTarget = 0.4; // safe CFL for highpressure pulses
volume = new Volume0D(1e-3, targetPressure, 300.0); // Compute engine volumes
solver.AddComponent(volume); double boreArea = Math.PI * 0.25 * Bore * Bore;
sweptVolume = boreArea * Stroke;
clearanceVolume = sweptVolume / (CompressionRatio - 1.0);
double initialVolume = clearanceVolume; // at TDC
pipe = new Pipe1D(1.0, 1e-4, 200); // Cylinder
pipe.EnergyRelaxationRate = 10; cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
solver.AddComponent(pipe);
var volPort = volume.CreatePort();
double orificeArea = 1e-5;
orifice = new OrificeLink(volPort, pipe, isPipeLeftEnd: true,
areaProvider: () => orificeArea)
{ {
DischargeCoefficient = 0.62, Dvdt = 0.0
UseInertance = true,
EffectiveLength = 0.001
}; };
solver.AddOrificeLink(orifice); solver.AddComponent(cylinder);
openEnd = new OpenEndLink(pipe, isLeftEnd: false) // Exhaust pipe (1 m, 1 cm², 100 cells)
exhaustPipe = new Pipe1D(0.5, 10e-4, 20);
solver.AddComponent(exhaustPipe);
// Exhaust port orifice with variable area
var cylPort = cylinder.CreatePort();
exhaustPort = new OrificeLink(cylPort, exhaustPipe, isPipeLeftEnd: true,
areaProvider: () => ComputeExhaustPortArea(crankAngle))
{
DischargeCoefficient = 0.8,
UseInertance = false
};
solver.AddOrificeLink(exhaustPort);
// Pipe open end
pipeOpenEnd = new OpenEndLink(exhaustPipe, isLeftEnd: false)
{ {
AmbientPressure = 101325.0, AmbientPressure = 101325.0,
Gamma = 1.4 Gamma = 1.4
}; };
solver.AddOpenEndLink(openEnd); solver.AddOpenEndLink(pipeOpenEnd);
// Crankshaft (3000 rpm)
crankshaft = new Crankshaft(initialRPM: 10000.0);
crankAngle = crankshaft.CrankAngle;
engineSpeed = crankshaft.AngularVelocity;
combustionPending = false; // first combustion will occur at next TDC
stepCount = 0; stepCount = 0;
simTime = 0.0;
nextResetTime = resetInterval;
ramping = false;
Console.WriteLine("Pressure reset test with smooth ramp"); Console.WriteLine("2Stroke engine test");
Console.WriteLine($"Volume 1L, reset to {targetPressure} Pa every {resetInterval*1000} ms, ramp {rampDuration*1000} ms"); Console.WriteLine($"Engine: {Bore*1000:F0} mm x {Stroke*1000:F0} mm, {sweptVolume*1e6:F0} cc");
Console.WriteLine($"Compression ratio: {CompressionRatio:F1}, clearance volume: {clearanceVolume*1e6:F2} cc");
Console.WriteLine($"Exhaust port opens at {ExhaustPortOpens}° ATDC, closes at {ExhaustPortCloses}° ATDC");
}
// ---- Port area vs crank angle (linear ramp, symmetric) ----
private double ComputeExhaustPortArea(double thetaRad)
{
double thetaDeg = thetaRad * 180.0 / Math.PI;
// Wrap to [0,360) for easier logic
thetaDeg %= 360.0;
// Exhaust open period
if (thetaDeg >= ExhaustPortOpens && thetaDeg <= ExhaustPortCloses)
{
// Ramp up from 0 to Max, then back down
double halfPeriod = (ExhaustPortCloses - ExhaustPortOpens) / 2.0;
double midPoint = ExhaustPortOpens + halfPeriod;
double distFromMid = Math.Abs(thetaDeg - midPoint) / halfPeriod;
double fraction = 1.0 - distFromMid;
fraction = Math.Clamp(fraction, 0.0, 1.0);
return MaxPortArea * fraction;
}
return 0.0;
}
// ---- Cylinder volume vs crank angle (slidercrank) ----
private double ComputeCylinderVolume(double thetaRad)
{
// thetaRad = crank angle from TDC (0 at TDC)
double r = CrankRadius;
double l = ConRodLength;
double cosTh = Math.Cos(thetaRad);
double sinTh = Math.Sin(thetaRad);
double term = Math.Sqrt(1.0 - Obliquity * Obliquity * sinTh * sinTh);
double x = r * (1.0 - cosTh) + l * (1.0 - term);
double area = Math.PI * 0.25 * Bore * Bore;
double deltaV = area * x;
return clearanceVolume + deltaV;
}
// ---- Combustion: set cylinder pressure AND temperature ----
private void Combustion()
{
double peakPressure = 20.0 * Units.atm; // 30 bar
double peakTemperature = 2000.0; // K
cylinder.SetPressure(peakPressure, peakTemperature);
} }
public override float Process() public override float Process()
{ {
// Previous crank angle for detecting TDC crossing
double prevAngle = crankshaft.CrankAngle;
// Advance crankshaft
crankshaft.Step(dt);
crankAngle = crankshaft.CrankAngle;
engineSpeed = crankshaft.AngularVelocity;
// Update cylinder volume to match current crank angle
double newVolume = ComputeCylinderVolume(crankAngle);
cylinder.Dvdt = (newVolume - cylinder.Volume) / dt;
cylinder.Volume = newVolume;
// ----- Ignition (once per revolution at TDC) -----
const double TwoPi = 2.0 * Math.PI;
double prevMod = prevAngle % TwoPi;
double currMod = crankAngle % TwoPi;
// Detect crossing of 0 mod 2π (TDC) going from near 2π to near 0
if (prevMod > Math.PI * 1.8 && currMod < Math.PI * 0.2)
{
if (!combustionPending)
{
Combustion();
combustionPending = true; // prevent multiple firings during the crossing
}
}
else if (currMod > Math.PI * 0.2 && currMod < Math.PI * 1.8)
{
combustionPending = false; // reset flag once clear of TDC
}
// Run solver
solver.Step(); solver.Step();
stepCount++; stepCount++;
simTime += dt;
// ---- Smooth pressure ramp ----
if (ramping)
{
double elapsed = simTime - rampStartTime;
if (elapsed >= rampDuration)
{
// Ramp finished, set exactly to target
volume.SetPressure(targetPressure);
ramping = false;
}
else
{
double frac = elapsed / rampDuration;
double currentTarget = rampStartPressure + (targetPressure - rampStartPressure) * frac;
volume.SetPressure(currentTarget);
}
}
// ---- Trigger a new reset ----
if (!ramping && simTime >= nextResetTime)
{
rampStartPressure = volume.Pressure; // current pressure before reset
rampStartTime = simTime;
ramping = true;
nextResetTime += resetInterval;
}
// Log every 500 steps // Log every 500 steps
if (stepCount % 500 == 0) if (stepCount % 50000 == 0)
{ {
double volP = volume.Pressure; int midCell = exhaustPipe.CellCount / 2;
double pipeL = pipe.GetCellPressure(0);
double pipeR = pipe.GetCellPressure(pipe.CellCount - 1);
double mdotOrif = orifice.LastMassFlowRate;
double mdotOpen = openEnd.LastMassFlowRate;
Console.WriteLine($"Step {stepCount}: " + double cylP_bar = cylinder.Pressure / 1e5;
$"VolP={volP:F1} Pa, PipeL={pipeL:F1}, PipeR={pipeR:F1}, " + double cylT_K = cylinder.Temperature;
$"mdot_orif={mdotOrif:E4} kg/s, mdot_open={mdotOpen:E4} kg/s"); double cylVol_cc = cylinder.Volume * 1e6;
double pipeL_bar = exhaustPipe.GetCellPressure(0) / 1e5;
double pipeM_bar = exhaustPipe.GetCellPressure(midCell) / 1e5;
double pipeR_bar = exhaustPipe.GetCellPressure(exhaustPipe.CellCount - 1) / 1e5;
double mdotExh = exhaustPort.LastMassFlowRate; // kg/s, positive into cylinder
double mdotOpen = pipeOpenEnd.LastMassFlowRate; // kg/s, positive out
Console.WriteLine(
$"Step {stepCount}: Angle={crankAngle*180.0/Math.PI % 360.0:F1}°, " +
$"CylP={cylP_bar:F2} bar, CylT={cylT_K:F0} K, Vol={cylVol_cc:F1} cc, " +
$"PipeL={pipeL_bar:F2} bar, PipeM={pipeM_bar:F2} bar, PipeR={pipeR_bar:F2} bar, " +
$"mdot_exh={mdotExh:E4} kg/s, mdot_open={mdotOpen:E4} kg/s"
);
} }
if (double.IsNaN(pipe.GetCellPressure(0))) if (double.IsNaN(exhaustPipe.GetCellPressure(0)))
{ {
Console.WriteLine("NaN detected stopping."); Console.WriteLine("NaN detected stopping.");
return 0f; return 0f;
} }
return soundProcessor.Process(openEnd); // Audio from open end
return soundProcessor.Process(pipeOpenEnd);
} }
public override void Draw(RenderWindow target) public override void Draw(RenderWindow target)
@@ -137,7 +235,7 @@ namespace FluidSim.Tests
float margin = 60f; float margin = 60f;
float pipeStartX = margin; float pipeStartX = margin;
float pipeEndX = winWidth - margin; float pipeEndX = winWidth - margin;
DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX); DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
} }
} }
} }

Binary file not shown.

BIN
trace.nettrace Normal file

Binary file not shown.

BIN
trace.nettrace.etlx Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long