seemingly working, added display text
This commit is contained in:
@@ -51,11 +51,11 @@ namespace FluidSim.Components
|
||||
/// Set the pressure to a specific value while keeping the current temperature constant.
|
||||
/// Updates Mass and InternalEnergy accordingly.
|
||||
/// </summary>
|
||||
public void SetPressure(double pressure)
|
||||
public void SetPressure(double pressure, double? temperature = null)
|
||||
{
|
||||
double V = Math.Max(Volume, 1e-12);
|
||||
double currentT = Temperature; // current temperature before changes
|
||||
double rho = pressure / (GasConstant * currentT);
|
||||
double T = temperature ?? Temperature;
|
||||
double rho = pressure / (GasConstant * T);
|
||||
Mass = rho * V;
|
||||
InternalEnergy = pressure * V / (Gamma - 1.0);
|
||||
}
|
||||
|
||||
@@ -4,27 +4,18 @@ using FluidSim.Interfaces;
|
||||
|
||||
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 steady‑state relationship,
|
||||
/// and includes acoustic inertance for dynamic (Helmholtz) behaviour.
|
||||
/// </summary>
|
||||
public class OrificeLink
|
||||
{
|
||||
public Port VolumePort { get; }
|
||||
public Port? VolumePort { get; }
|
||||
public Pipe1D Pipe { get; }
|
||||
public bool IsPipeLeftEnd { get; }
|
||||
public Func<double> AreaProvider { get; set; }
|
||||
public double DischargeCoefficient { get; set; } = 0.62;
|
||||
|
||||
// Acoustic length (wall thickness + end correction) – controls the resonance frequency
|
||||
public double EffectiveLength { get; set; } = 0.001; // 1 mm
|
||||
|
||||
// Whether to include inertance; if false, uses the steady‑state nozzle model directly
|
||||
public double EffectiveLength { get; set; } = 0.001;
|
||||
public bool UseInertance { get; set; } = true;
|
||||
|
||||
// Current mass flow (kg/s, positive = volume → pipe)
|
||||
private double _mdot;
|
||||
private double _mdot; // positive = volume → pipe
|
||||
|
||||
public double LastMassFlowRate { 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)
|
||||
{
|
||||
VolumePort = volumePort; // null is allowed
|
||||
VolumePort = volumePort;
|
||||
Pipe = pipe ?? throw new ArgumentNullException(nameof(pipe));
|
||||
IsPipeLeftEnd = isPipeLeftEnd;
|
||||
AreaProvider = areaProvider ?? throw new ArgumentNullException(nameof(areaProvider));
|
||||
@@ -43,20 +34,18 @@ namespace FluidSim.Core
|
||||
public void Resolve(double dtSub)
|
||||
{
|
||||
double area = AreaProvider();
|
||||
// Closed wall or missing volume port => reflective boundary
|
||||
if (area < 1e-12 || VolumePort == null)
|
||||
{
|
||||
SetClosedWall();
|
||||
return;
|
||||
}
|
||||
|
||||
// Gather volume state
|
||||
// Gather states
|
||||
double volP = VolumePort.Pressure;
|
||||
double volRho = VolumePort.Density;
|
||||
double volT = VolumePort.Temperature;
|
||||
double volH = VolumePort.SpecificEnthalpy;
|
||||
|
||||
// Gather pipe interior state at the connected end
|
||||
(double pipeRho, double pipeU, double pipeP) = IsPipeLeftEnd
|
||||
? Pipe.GetInteriorStateLeft()
|
||||
: Pipe.GetInteriorStateRight();
|
||||
@@ -65,24 +54,23 @@ namespace FluidSim.Core
|
||||
double gamma = 1.4;
|
||||
double R = 287.0;
|
||||
|
||||
// ---- Steady‑state mass flow from isentropic nozzle ----
|
||||
double mdotSS; // positive = volume → pipe
|
||||
double rhoFace, uFace, pFace;
|
||||
|
||||
// ---- 1. Steady‑state nozzle solution (gives correct exit pressure) ----
|
||||
double mdotSS;
|
||||
double rhoFace0, uFace0, pFace0;
|
||||
if (volP >= pipeP)
|
||||
{
|
||||
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
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
// ---- Inertance ODE (optional) ----
|
||||
// ---- 2. Inertance dynamics ----
|
||||
if (UseInertance)
|
||||
{
|
||||
double rhoUp = _mdot >= 0 ? volRho : pipeRho;
|
||||
@@ -97,35 +85,31 @@ namespace FluidSim.Core
|
||||
_mdot = mdotSS;
|
||||
}
|
||||
|
||||
// Clamp outflow to available mass (if finite volume)
|
||||
// Clamp outflow to available mass
|
||||
if (VolumePort.Owner is Volume0D vol)
|
||||
{
|
||||
double maxOut = vol.Mass / dtSub;
|
||||
if (_mdot > maxOut) _mdot = maxOut;
|
||||
}
|
||||
|
||||
// ---- Ghost state ----
|
||||
// Density = upstream density (consistent with current flow direction)
|
||||
rhoFace = _mdot >= 0 ? volRho : pipeRho;
|
||||
// Pressure = downstream pressure (consistent with nozzle exit)
|
||||
pFace = _mdot >= 0 ? pipeP : volP;
|
||||
// Velocity magnitude derived from actual mass flow
|
||||
// ---- 3. Ghost state (use nozzle‑exit pressure!) ----
|
||||
double rhoFace = _mdot >= 0 ? volRho : pipeRho; // upstream density
|
||||
double pFace = pFace0; // correct exit pressure (choked/subsonic)
|
||||
double mdotMag = Math.Abs(_mdot);
|
||||
uFace = mdotMag / (rhoFace * area);
|
||||
double uFace = mdotMag / (rhoFace * area);
|
||||
|
||||
if (IsPipeLeftEnd)
|
||||
uFace = _mdot >= 0 ? uFace : -uFace; // left end: positive u = into pipe
|
||||
uFace = _mdot >= 0 ? uFace : -uFace; // left: +u into pipe
|
||||
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)
|
||||
Pipe.SetGhostLeft(rhoFace, uFace, pFace);
|
||||
else
|
||||
Pipe.SetGhostRight(rhoFace, uFace, pFace);
|
||||
|
||||
// ---- Store results ----
|
||||
double mdotIntoVolume = -_mdot; // positive = into volume
|
||||
// Store for monitoring
|
||||
double mdotIntoVolume = -_mdot;
|
||||
LastMassFlowRate = mdotIntoVolume;
|
||||
LastFaceDensity = rhoFace;
|
||||
LastFaceVelocity = uFace;
|
||||
@@ -133,13 +117,12 @@ namespace FluidSim.Core
|
||||
|
||||
VolumePort.MassFlowRate = mdotIntoVolume;
|
||||
|
||||
// Enthalpy for volume integration
|
||||
if (mdotIntoVolume >= 0) // inflow → pipe enthalpy
|
||||
if (mdotIntoVolume >= 0)
|
||||
{
|
||||
double hPipe = gamma / (gamma - 1.0) * pipeP / Math.Max(pipeRho, 1e-12);
|
||||
VolumePort.SpecificEnthalpy = hPipe;
|
||||
}
|
||||
else // outflow → volume's own enthalpy
|
||||
else
|
||||
{
|
||||
VolumePort.SpecificEnthalpy = volH;
|
||||
}
|
||||
@@ -160,7 +143,6 @@ namespace FluidSim.Core
|
||||
LastFaceDensity = rInt;
|
||||
LastFaceVelocity = 0.0;
|
||||
LastFacePressure = pInt;
|
||||
// Don't touch VolumePort if it's null
|
||||
if (VolumePort != null)
|
||||
VolumePort.MassFlowRate = 0.0;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
|
||||
67
Program.cs
67
Program.cs
@@ -13,9 +13,8 @@ public class Program
|
||||
private const double DrawFrequency = 60.0;
|
||||
private static Scenario scenario;
|
||||
|
||||
// Speed control (existing + new throttle)
|
||||
// Speed control
|
||||
private static double desiredSpeed = 0.01;
|
||||
//private static double desiredSpeed = 1.0;
|
||||
private static double currentSpeed = desiredSpeed;
|
||||
private const double MinSpeed = 0.0001;
|
||||
private const double MaxSpeed = 1.0;
|
||||
@@ -24,22 +23,47 @@ public class Program
|
||||
private static double lastDesiredSpeed = 0.1;
|
||||
private static bool isRealTime = false;
|
||||
|
||||
// Throttle smoothing
|
||||
private static double targetThrottle = 0.0; // 1.0 when W is pressed, 0.0 otherwise
|
||||
// Throttle smoothing (unused but kept)
|
||||
private static double targetThrottle = 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;
|
||||
|
||||
// ---- Overlay text ----
|
||||
private static Font? overlayFont;
|
||||
private static Text? overlayText;
|
||||
|
||||
public static void Main()
|
||||
{
|
||||
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.Closed += (_, _) => { running = false; window.Close(); };
|
||||
window.MouseWheelScrolled += OnMouseWheel;
|
||||
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);
|
||||
soundEngine.Volume = 100;
|
||||
soundEngine.Start();
|
||||
@@ -74,7 +98,6 @@ public class Program
|
||||
double speedSmoothing = 8.0;
|
||||
currentSpeed += (desiredSpeed - currentSpeed) * (1.0 - Math.Exp(-speedSmoothing * dtClock));
|
||||
|
||||
|
||||
// Generate audio
|
||||
double targetAudioClock = currentRealTime + 0.05;
|
||||
while (totalOutputSamples < targetAudioClock * SampleRate && running)
|
||||
@@ -116,21 +139,30 @@ public class Program
|
||||
break;
|
||||
}
|
||||
|
||||
// Drawing & title
|
||||
// Drawing
|
||||
if (currentRealTime - lastDrawTime >= drawInterval)
|
||||
{
|
||||
double actualSpeed = totalOutputSamples / (currentRealTime * SampleRate);
|
||||
double simTime = totalSimSteps / (double)SampleRate;
|
||||
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
|
||||
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
|
||||
window.SetTitle(
|
||||
$"{toggleHint} {throttleHint} " +
|
||||
$"Thr: {currentThrottle:F2} " +
|
||||
$"Speed: {currentSpeed:F3}x → {desiredSpeed:F3}x " +
|
||||
$"Act: {actualSpeed:F2}x"
|
||||
);
|
||||
double realtimePercent = totalOutputSamples / (currentRealTime * SampleRate) * 100.0;
|
||||
|
||||
// Update overlay text
|
||||
if (overlayText != null)
|
||||
{
|
||||
string toggleHint = isRealTime ? "[Space] slow mo" : "[Space] real time";
|
||||
string throttleHint = Keyboard.IsKeyPressed(Keyboard.Key.W) ? "W held" : "W released";
|
||||
overlayText.DisplayedString =
|
||||
$"{toggleHint} {throttleHint} " +
|
||||
$"Speed: {currentSpeed:F3}x " +
|
||||
$"RT: {realtimePercent:F1}%";
|
||||
}
|
||||
|
||||
window.Clear(Color.Black);
|
||||
scenario.Draw(window);
|
||||
|
||||
// Draw the overlay on top
|
||||
if (overlayText != null)
|
||||
window.Draw(overlayText);
|
||||
|
||||
window.Display();
|
||||
lastDrawTime = currentRealTime;
|
||||
}
|
||||
@@ -140,7 +172,6 @@ public class Program
|
||||
window.Dispose();
|
||||
}
|
||||
|
||||
// (Mouse wheel, space toggle unchanged)
|
||||
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
||||
{
|
||||
bool wasRealTime = Math.Abs(desiredSpeed - 1.0) < 1e-6;
|
||||
|
||||
@@ -19,34 +19,37 @@ namespace FluidSim.Tests
|
||||
// ---------- Shared drawing helpers ----------
|
||||
|
||||
protected const double AmbientPressure = 101325.0;
|
||||
protected const double AmbientTemperature = 300.0; // K
|
||||
|
||||
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
|
||||
protected Color PressureColor(double pressure)
|
||||
/// <summary>Map temperature [0 K … 2000 K] to a color: blue (0 K) → green (300 K) → red (2000 K).</summary>
|
||||
protected Color TemperatureColor(double temperature)
|
||||
{
|
||||
double range = AmbientPressure * 0.05; // ±5% gives full colour swing
|
||||
double t = (pressure - AmbientPressure) / range;
|
||||
t = Math.Clamp(t, -1.0, 1.0);
|
||||
// Clamp to the range we want to display
|
||||
double t = Math.Clamp(temperature, 0.0, 2000.0);
|
||||
|
||||
byte r, g, b;
|
||||
if (t < 0)
|
||||
if (t < AmbientTemperature)
|
||||
{
|
||||
double factor = -t;
|
||||
// Blue → Green
|
||||
double factor = t / AmbientTemperature; // 0 at 0 K, 1 at 300 K
|
||||
r = 0;
|
||||
g = (byte)(255 * (1 - factor));
|
||||
b = (byte)(255 * factor);
|
||||
g = (byte)(255 * factor);
|
||||
b = (byte)(255 * (1.0 - factor));
|
||||
}
|
||||
else
|
||||
{
|
||||
double factor = t;
|
||||
// Green → Red
|
||||
double factor = (t - AmbientTemperature) / (2000.0 - AmbientTemperature); // 0 at 300 K, 1 at 2000 K
|
||||
r = (byte)(255 * factor);
|
||||
g = (byte)(255 * (1 - factor));
|
||||
g = (byte)(255 * (1.0 - factor));
|
||||
b = 0;
|
||||
}
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the pipe as a smooth triangle‑strip whose radius varies with cell pressure.
|
||||
/// Draws the pipe as a smooth triangle‑strip whose radius varies with cell pressure (for visibility),
|
||||
/// but colored by temperature.
|
||||
/// </summary>
|
||||
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 baseRadius = 25f;
|
||||
float rangeFactor = 1f;
|
||||
float scaleFactor = 5f;
|
||||
float rangeFactor = 2f;
|
||||
float scaleFactor = 2f;
|
||||
|
||||
// ----- smoothstep helper -----
|
||||
static float SmoothStep(float edge0, float edge1, float x)
|
||||
@@ -67,12 +70,19 @@ namespace FluidSim.Tests
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
// ----- Pre‑compute cell positions and radii -----
|
||||
// ----- Pre‑compute cell positions, radii, and temperatures -----
|
||||
var centers = 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++)
|
||||
{
|
||||
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);
|
||||
radii[i] = baseRadius * (1f + deviation * scaleFactor);
|
||||
if (radii[i] < 2f) radii[i] = 2f;
|
||||
@@ -89,8 +99,7 @@ namespace FluidSim.Tests
|
||||
{
|
||||
float x = centers[i];
|
||||
float r = radii[i];
|
||||
double p = pipe.GetCellPressure(i);
|
||||
Color col = PressureColor(p);
|
||||
Color col = TemperatureColor(temperatures[i]);
|
||||
|
||||
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 xi = centers[i] + (centers[i + 1] - centers[i]) * t;
|
||||
float ri = radii[i] + (radii[i + 1] - radii[i]) * st;
|
||||
double pi = pipe.GetCellPressure(i) * (1 - t) + pipe.GetCellPressure(i + 1) * t;
|
||||
Color coli = PressureColor(pi);
|
||||
double Ti = temperatures[i] + (temperatures[i + 1] - temperatures[i]) * st; // linear interpolation
|
||||
Color coli = TemperatureColor(Ti);
|
||||
|
||||
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
|
||||
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
|
||||
|
||||
@@ -9,124 +9,222 @@ namespace FluidSim.Tests
|
||||
{
|
||||
public class TestScenario : Scenario
|
||||
{
|
||||
// Simulation core
|
||||
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 resetInterval = 0.2; // seconds between resets
|
||||
private double nextResetTime;
|
||||
private double targetPressure = 10 * Units.atm;
|
||||
private double rampDuration = 0.002; // 2 ms ramp
|
||||
private double rampStartTime;
|
||||
private double rampStartPressure; // pressure when ramp started
|
||||
private bool ramping;
|
||||
|
||||
// Engine components
|
||||
private Volume0D cylinder;
|
||||
private Pipe1D exhaustPipe;
|
||||
private OrificeLink exhaustPort;
|
||||
private OpenEndLink pipeOpenEnd;
|
||||
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)
|
||||
{
|
||||
dt = 1.0 / sampleRate;
|
||||
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1);
|
||||
soundProcessor.Gain = 2.0f; // lower gain to avoid clipping
|
||||
// Audio
|
||||
soundProcessor = new SoundProcessor(sampleRate, 1) { Gain = 1f };
|
||||
|
||||
// Solver
|
||||
solver = new Solver();
|
||||
solver.SetTimeStep(dt);
|
||||
solver.CflTarget = 0.4;
|
||||
solver.CflTarget = 0.4; // safe CFL for high‑pressure pulses
|
||||
|
||||
volume = new Volume0D(1e-3, targetPressure, 300.0);
|
||||
solver.AddComponent(volume);
|
||||
// Compute engine volumes
|
||||
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);
|
||||
pipe.EnergyRelaxationRate = 10;
|
||||
solver.AddComponent(pipe);
|
||||
|
||||
var volPort = volume.CreatePort();
|
||||
double orificeArea = 1e-5;
|
||||
orifice = new OrificeLink(volPort, pipe, isPipeLeftEnd: true,
|
||||
areaProvider: () => orificeArea)
|
||||
// Cylinder
|
||||
cylinder = new Volume0D(initialVolume, 101325.0, 300.0)
|
||||
{
|
||||
DischargeCoefficient = 0.62,
|
||||
UseInertance = true,
|
||||
EffectiveLength = 0.001
|
||||
Dvdt = 0.0
|
||||
};
|
||||
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,
|
||||
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;
|
||||
simTime = 0.0;
|
||||
nextResetTime = resetInterval;
|
||||
ramping = false;
|
||||
|
||||
Console.WriteLine("Pressure reset test with smooth ramp");
|
||||
Console.WriteLine($"Volume 1L, reset to {targetPressure} Pa every {resetInterval*1000} ms, ramp {rampDuration*1000} ms");
|
||||
Console.WriteLine("2‑Stroke engine test");
|
||||
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 (slider‑crank) ----
|
||||
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()
|
||||
{
|
||||
// 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();
|
||||
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
|
||||
if (stepCount % 500 == 0)
|
||||
if (stepCount % 50000 == 0)
|
||||
{
|
||||
double volP = volume.Pressure;
|
||||
double pipeL = pipe.GetCellPressure(0);
|
||||
double pipeR = pipe.GetCellPressure(pipe.CellCount - 1);
|
||||
double mdotOrif = orifice.LastMassFlowRate;
|
||||
double mdotOpen = openEnd.LastMassFlowRate;
|
||||
int midCell = exhaustPipe.CellCount / 2;
|
||||
|
||||
Console.WriteLine($"Step {stepCount}: " +
|
||||
$"VolP={volP:F1} Pa, PipeL={pipeL:F1}, PipeR={pipeR:F1}, " +
|
||||
$"mdot_orif={mdotOrif:E4} kg/s, mdot_open={mdotOpen:E4} kg/s");
|
||||
double cylP_bar = cylinder.Pressure / 1e5;
|
||||
double cylT_K = cylinder.Temperature;
|
||||
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.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return soundProcessor.Process(openEnd);
|
||||
// Audio from open end
|
||||
return soundProcessor.Process(pipeOpenEnd);
|
||||
}
|
||||
|
||||
public override void Draw(RenderWindow target)
|
||||
@@ -137,7 +235,7 @@ namespace FluidSim.Tests
|
||||
float margin = 60f;
|
||||
float pipeStartX = margin;
|
||||
float pipeEndX = winWidth - margin;
|
||||
DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
DrawPipe(target, exhaustPipe, pipeCenterY, pipeStartX, pipeEndX);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
fonts/FiraCodeNerdFont-Medium.ttf
Normal file
BIN
fonts/FiraCodeNerdFont-Medium.ttf
Normal file
Binary file not shown.
BIN
trace.nettrace
Normal file
BIN
trace.nettrace
Normal file
Binary file not shown.
BIN
trace.nettrace.etlx
Normal file
BIN
trace.nettrace.etlx
Normal file
Binary file not shown.
1
trace.speedscope.speedscope.json
Normal file
1
trace.speedscope.speedscope.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user