refactoring (broken right now)

This commit is contained in:
2026-05-06 15:24:39 +02:00
parent bc4e077924
commit bc0df51ddb
25 changed files with 1184 additions and 1983 deletions

View File

@@ -1,325 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using FluidSim.Interfaces;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class EngineScenario : Scenario
{
private Solver solver;
private Crankshaft crankshaft;
private EngineCylinder engineCyl;
private Pipe1D exhaustPipe;
private Pipe1D intakePipe;
private PipeVolumeConnection couplingExhaust;
private PipeVolumeConnection couplingIntake;
private SoundProcessor exhaustSoundProcessor;
private SoundProcessor intakeSoundProcessor;
private OutdoorExhaustReverb reverb;
private Port exhaustPort = new Port();
private Port intakePort = new Port();
private double dt;
private double exhPipeArea, intPipeArea;
private const double AmbientPressure = 101325.0;
private double time;
private int stepCount = 0;
private const int LogInterval = 1000;
public double Throttle { get; set; } = 0.15;
private const double FullLoadPeakPressure = 140.0 * Units.bar;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// Crankshaft
crankshaft = new Crankshaft(initialRPM: 2000.0)
{
Inertia = 0.05,
FrictionConstant = 0.2,
FrictionViscous = 0.025
};
// Exhaust pipe (longer, larger)
double exhLength = 0.5;
double exhRadius = 1.5 * Units.cm;
exhPipeArea = Math.PI * exhRadius * exhRadius;
exhaustPipe = new Pipe1D(exhLength, exhPipeArea, sampleRate, forcedCellCount: 30);
exhaustPipe.SetUniformState(1.225, 0.0, AmbientPressure);
exhaustPipe.DampingMultiplier = 0.0;
exhaustPipe.EnergyRelaxationRate = 100.0f;
// Intake pipe (shorter, narrower)
double intLength = 0.1;
double intRadius = 1 * Units.cm;
intPipeArea = Math.PI * intRadius * intRadius;
intakePipe = new Pipe1D(intLength, intPipeArea, sampleRate, forcedCellCount: 10);
intakePipe.SetUniformState(1.225, 0.0, AmbientPressure);
// Cylinder (starts at BDC, fresh charge)
engineCyl = new EngineCylinder(crankshaft,
bore: 56 * Units.mm, stroke: 57 * Units.mm, compressionRatio: 9.5,
exhPipeArea: exhPipeArea, intPipeArea: intPipeArea, sampleRate: sampleRate);
engineCyl.ignition = true;
// Set crank to BDC (180°) and sync
crankshaft.CrankAngle = Math.PI;
crankshaft.PreviousAngle = Math.PI; // make sure this property is settable (public setter)
// Couplings
couplingExhaust = new PipeVolumeConnection(engineCyl.Cylinder, exhaustPipe, true, orificeArea: 0.0);
couplingIntake = new PipeVolumeConnection(engineCyl.Cylinder, intakePipe, false, orificeArea: 0.0);
// Solver
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(engineCyl.Cylinder);
solver.AddPipe(exhaustPipe);
solver.AddPipe(intakePipe);
solver.AddConnection(couplingExhaust);
solver.AddConnection(couplingIntake);
solver.SetPipeBoundary(exhaustPipe, false, BoundaryType.OpenEnd, AmbientPressure);
solver.SetPipeBoundary(intakePipe, true, BoundaryType.GhostCell); // cylinder side left
solver.SetPipeBoundary(intakePipe, false, BoundaryType.OpenEnd, AmbientPressure); // ambient side right
// Sound
exhaustSoundProcessor = new SoundProcessor(sampleRate, exhRadius * 2);
exhaustSoundProcessor.Gain = 0.001f;
intakeSoundProcessor = new SoundProcessor(sampleRate, intRadius * 2);
intakeSoundProcessor.Gain = 0.001f;
// Reverb
reverb = new OutdoorExhaustReverb(sampleRate);
reverb.DryMix = 1.0f;
reverb.EarlyMix = 0.5f;
reverb.TailMix = 0.9f;
reverb.Feedback = 0.9f;
reverb.DampingFreq = 6000f;
Console.WriteLine("=== Engine with intake & cycleaware valves ===");
}
public override float Process()
{
double idleThrottle = 0.1;
if (crankshaft.AngularVelocity < 80) idleThrottle = 0.2;
double throttle = Math.Clamp(Throttle, idleThrottle, 1.0);
double targetPressure = throttle * FullLoadPeakPressure;
engineCyl.TargetPeakPressure = targetPressure;
engineCyl.Step(dt);
crankshaft.Step(dt);
couplingExhaust.OrificeArea = engineCyl.ExhaustOrificeArea;
couplingIntake.OrificeArea = engineCyl.IntakeOrificeArea;
solver.Step();
UpdateExhaustPort();
UpdateIntakePort();
float dryExhaust = exhaustSoundProcessor.Process(exhaustPort);
float dryIntake = intakeSoundProcessor.Process(intakePort);
float dry = dryExhaust + dryIntake;
float wet = reverb.Process(dry);
if (++stepCount % LogInterval == 0) Log();
return wet;
}
private void Log()
{
double rpm = crankshaft.AngularVelocity * 60.0 / (2.0 * Math.PI);
double cycleDeg = (engineCyl.CycleAngle * 180.0 / Math.PI) % 720.0;
string stroke = cycleDeg < 180.0 ? "Power" :
cycleDeg < 360.0 ? "Exhaust" :
cycleDeg < 540.0 ? "Intake" : "Compression";
// Cylinder
double pCyl = engineCyl.Cylinder.Pressure;
double TCyl = engineCyl.Cylinder.Temperature;
double VCyl = engineCyl.Cylinder.Volume;
double mCyl = engineCyl.Cylinder.Mass;
double exhArea = engineCyl.ExhaustOrificeArea * 1e6; // mm²
double intArea = engineCyl.IntakeOrificeArea * 1e6; // mm²
// Exhaust pipe
int exhLast = exhaustPipe.GetCellCount() - 1;
double pExhEnd = exhaustPipe.GetCellPressure(exhLast);
double mdotExhOut = exhaustPipe.GetOpenEndMassFlow(); // positive out
// Intake pipe
double mdotIntIn = couplingIntake.LastMassFlowIntoVolume;
double pIntAmbEnd = intakePort.Pressure;
Console.WriteLine(
$"{stepCount,8} {stroke,-11} {cycleDeg,6:F1}° " +
$"RPM:{rpm,5:F0} " +
$"Cyl: p={pCyl/1e5,6:F3}bar T={TCyl,6:F0}K V={VCyl*1e6,6:F0}cm³ m={mCyl*1e3,6:F6}g " +
$"Valves: Exh={exhArea,5:F0}mm² Int={intArea,5:F0}mm² " +
$"Intake: p_end={pIntAmbEnd/1e5,6:F3}bar mdot_in={mdotIntIn,7:F4}kg/s " +
$"Exhaust: p_end={pExhEnd/1e5,6:F3}bar mdot_out={mdotExhOut,7:F4}kg/s");
}
private void UpdateExhaustPort()
{
int last = exhaustPipe.GetCellCount() - 1;
double p = exhaustPipe.GetCellPressure(last);
double rho = exhaustPipe.GetCellDensity(last);
double vel = exhaustPipe.GetCellVelocity(last);
// Safety clamps
rho = Math.Clamp(rho, 0.01, 50.0);
vel = Math.Clamp(vel, -500.0, 500.0);
p = Math.Clamp(p, 1e4, 2e6);
double outflowMassFlow = rho * vel * exhPipeArea;
exhaustPort.Pressure = p;
exhaustPort.Density = rho;
exhaustPort.Temperature = p / (rho * 287.05);
exhaustPort.MassFlowRate = -outflowMassFlow;
exhaustPort.SpecificEnthalpy = 0.0;
}
private void UpdateIntakePort()
{
// Use the actual valve mass flow (positive = into cylinder)
double mdotIntoEngine = couplingIntake.LastMassFlowIntoVolume;
// Use cylinder pressure/density for the port state (or intake pipe last cell)
double pCyl = engineCyl.Cylinder.Pressure;
double rhoCyl = engineCyl.Cylinder.Density;
intakePort.Pressure = Math.Max(pCyl, 100);
intakePort.Density = Math.Max(rhoCyl, 1e-6);
intakePort.Temperature = engineCyl.Cylinder.Temperature;
intakePort.MassFlowRate = mdotIntoEngine;
intakePort.SpecificEnthalpy = 0.0;
}
// ==================== Drawing ====================
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
const float T_ambient = 293.15f;
const float T_hot = 1500f;
const float T_cold = 0f;
const float R = 287.05f;
float deltaHot = T_hot - T_ambient;
float deltaCold = T_ambient - T_cold;
float NormaliseTemperature(double T)
{
double t;
if (T >= T_ambient)
t = (T - T_ambient) / deltaHot;
else
t = (T - T_ambient) / deltaCold;
return (float)Math.Clamp(t, -1.0, 1.0);
}
// ---- Cylinder ----
float cylW = 80f, cylH = 150f;
var cylRect = new RectangleShape(new Vector2f(cylW, cylH));
cylRect.Position = new Vector2f(200f, centerY - cylH / 2f);
double tempCyl = engineCyl.Cylinder.Temperature;
float tnCyl = NormaliseTemperature(tempCyl);
byte rC = (byte)(tnCyl > 0 ? 255 * tnCyl : 0);
byte bC = (byte)(tnCyl < 0 ? -255 * tnCyl : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tnCyl)));
cylRect.FillColor = new Color(rC, gC, bC);
target.Draw(cylRect);
// ---- Piston ----
float pistonWidth = cylW - 12f;
float pistonHeight = 16f;
float pistonFraction = (float)engineCyl.PistonPositionFraction;
float pistonTopY = cylRect.Position.Y + pistonFraction * (cylH - pistonHeight);
var pistonRect = new RectangleShape(new Vector2f(pistonWidth, pistonHeight))
{
Position = new Vector2f(cylRect.Position.X + 6f, pistonTopY),
FillColor = new Color(80, 80, 80)
};
target.Draw(pistonRect);
// ---------- NEW: Valve lift indicators ----------
float barWidth = 30f;
float barHeight = 10f;
float exhLift = (float)engineCyl.ExhaustValveLiftCurrent;
float intLift = (float)engineCyl.IntakeValveLiftCurrent;
// Exhaust valve indicator (right side of cylinder)
var exhBar = new RectangleShape(new Vector2f(barWidth, barHeight))
{
Position = new Vector2f(cylRect.Position.X + cylW - 10,
cylRect.Position.Y - 20 - exhLift * 20),
FillColor = new Color(200, 200, 200)
};
target.Draw(exhBar);
// Intake valve indicator (left side of cylinder)
var intBar = new RectangleShape(new Vector2f(barWidth, barHeight))
{
Position = new Vector2f(cylRect.Position.X - 20,
cylRect.Position.Y - 20 - intLift * 20),
FillColor = new Color(200, 200, 200)
};
target.Draw(intBar);
// ---- Exhaust pipe (rightwards) ----
DrawPipe(target, exhaustPipe, startX: 280f, endX: winW - 60f, centerY + 10 - cylRect.Size.Y / 2,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, true);
// ---- Intake pipe (leftwards) ----
DrawPipe(target, intakePipe, startX: 200f, endX: 20f, centerY + 10 - cylRect.Size.Y / 2,
T_ambient, T_hot, T_cold, R, NormaliseTemperature, false);
}
private void DrawPipe(RenderWindow target, Pipe1D pipe,
float startX, float endX, float centerY,
float T_ambient, float T_hot, float T_cold, float R,
Func<double, float> normaliseTemp, bool leftToRight)
{
int n = pipe.GetCellCount();
float dir = leftToRight ? 1f : -1f;
float pipeLen = Math.Abs(endX - startX);
float dx = pipeLen / (n - 1) * dir;
float baseRadius = leftToRight ? 20f : 14f; // exhaust thicker, intake thinner
var vertices = new Vertex[n * 2];
float ambPress = 101325f;
for (int i = 0; i < n; i++)
{
float x = startX + i * dx;
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / (rho * R);
float r = baseRadius * 0.2f * (float)(1.0 + (p - ambPress) / ambPress);
if (r < 2f) r = 2f;
float tn = normaliseTemp(T);
byte rC = (byte)(tn > 0 ? 255 * tn : 0);
byte bC = (byte)(tn < 0 ? -255 * tn : 0);
byte gC = (byte)(255 * (1 - Math.Abs(tn)));
var col = new Color(rC, gC, bC);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
}
}
}

View File

@@ -1,128 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class HelmholtzResonatorScenario : Scenario
{
private Solver solver;
private Volume0D cavity;
private Pipe1D neck;
private PipeVolumeConnection coupling;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
// 1litre cavity, 10% overpressure
double cavityVolume = 1e-3;
double initialCavityPressure = 1.1 * ambientPressure;
cavity = new Volume0D(cavityVolume, initialCavityPressure, 300.0, sampleRate)
{
Gamma = 1.4,
GasConstant = 287.0
};
// Neck: length 10 cm, radius 1 cm
double neckLength = 0.1;
double neckRadius = 0.01;
double neckArea = Math.PI * neckRadius * neckRadius;
neck = new Pipe1D(neckLength, neckArea, sampleRate, forcedCellCount: 40);
neck.SetUniformState(1.225, 0.0, ambientPressure);
// Create the coupling between cavity and left end of the neck (PortA)
coupling = new PipeVolumeConnection(cavity, neck, isPipeLeftEnd: true, orificeArea: neckArea);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddVolume(cavity);
solver.AddPipe(neck);
solver.AddConnection(coupling);
// Left boundary (PortA) is volumecoupled via ghost cell, right boundary (PortB) is open end
solver.SetPipeBoundary(neck, isA: true, BoundaryType.GhostCell);
solver.SetPipeBoundary(neck, isA: false, BoundaryType.OpenEnd, ambientPressure);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pOpen = neck.GetCellPressure(neck.GetCellCount() - 1);
float audio = (float)((pOpen - ambientPressure) / ambientPressure);
if (stepCount % 20 == 0)
{
double pCav = cavity.Pressure;
// Mass flow rate is not directly available we can compute from pressure difference or skip
Console.WriteLine(
$"t={time * 1e3:F2} ms step={stepCount} " +
$"P_cav={pCav:F1} Pa, P_open={pOpen:F1} Pa, " +
$"audio={audio:F4}");
}
return audio;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
// Cavity rectangle
float cavityWidth = 120f;
float cavityHeight = 180f;
var cavityRect = new RectangleShape(new Vector2f(cavityWidth, cavityHeight));
cavityRect.Position = new Vector2f(40f, centerY - cavityHeight / 2f);
cavityRect.FillColor = PressureColor(cavity.Pressure);
target.Draw(cavityRect);
// Neck drawn as tapered pipe
int n = neck.GetCellCount();
float neckStartX = 40f + cavityWidth + 10f;
float neckEndX = winW - 60f;
float neckLenPx = neckEndX - neckStartX;
float dx = neckLenPx / (n - 1);
float baseRadius = 20f;
var vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++)
{
float x = neckStartX + i * dx;
double p = neck.GetCellPressure(i);
float r = baseRadius * (float)(0.5 + 0.5 * Math.Tanh((p - ambientPressure) / (ambientPressure * 0.2)));
if (r < 4f) r = 4f;
Color col = PressureColor(p);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
// Open end indicator
var arrow = new CircleShape(8f);
arrow.Position = new Vector2f(neckEndX - 4f, centerY - 4f);
arrow.FillColor = Color.White;
target.Draw(arrow);
}
private Color PressureColor(double pressure)
{
double range = ambientPressure * 0.1;
double t = Math.Clamp((pressure - ambientPressure) / range, -1.0, 1.0);
byte r = (byte)(t > 0 ? 255 * t : 0);
byte b = (byte)(t < 0 ? -255 * t : 0);
byte g = (byte)(255 * (1 - Math.Abs(t)));
return new Color(r, g, b);
}
}
}

View File

@@ -1,184 +0,0 @@
using FluidSim.Components;
using FluidSim.Interfaces;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
using System;
namespace FluidSim.Core
{
public class PipeResonatorScenario : Scenario
{
private Solver solver;
private Pipe1D pipe;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
private bool enableLogging = true;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
double length = 2;
double radius = 50 * Units.mm;
double area = Units.AreaFromDiameter(radius);
pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: 80);
pipe.SetUniformState(1.225, 0.0, ambientPressure);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddPipe(pipe);
// Open end at port A (left), closed end at port B (right)
solver.SetPipeBoundary(pipe, isA: true, BoundaryType.OpenEnd, ambientPressure);
solver.SetPipeBoundary(pipe, isA: false, BoundaryType.ClosedEnd);
// Initial pressure pulse
int pulseCells = 5;
double pulsePressure = 2 * ambientPressure;
for (int i = 0; i < pulseCells; i++)
pipe.SetCellState(i, 1.225, 0.0, pulsePressure);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pMid = pipe.GetPressureAtFraction(0.5);
sample = (float)((pMid - ambientPressure) / ambientPressure);
Log(sample);
return sample;
}
private void Log(float sample)
{
if (!enableLogging) return;
if (stepCount % 10 == 0 && stepCount < 1000)
{
double pMid = pipe.GetPressureAtFraction(0.5);
double pOpen = pipe.GetCellPressure(0);
double pClosed = pipe.GetCellPressure(pipe.GetCellCount() - 1);
Console.WriteLine(
$"t = {time * 1e3:F3} ms Step {stepCount:D4}: " +
$"sample = {sample:F3}, " +
$"P_mid = {pMid:F2} Pa ({pMid / ambientPressure:F4} atm), " +
$"P_open = {pOpen:F2} Pa, P_closed = {pClosed:F2} Pa");
}
}
public override void Draw(RenderWindow target)
{
float winWidth = target.GetView().Size.X;
float winHeight = target.GetView().Size.Y;
float pipeCenterY = winHeight / 2f;
float margin = 60f;
float pipeStartX = margin;
float pipeEndX = winWidth - margin;
float pipeLengthPx = pipeEndX - pipeStartX;
int n = pipe.GetCellCount();
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float baseRadius = 25f;
float rangeFactor = 1f;
float scaleFactor = 5f;
// ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
// ----- Precompute cell positions and radii -----
var centers = new float[n];
var radii = new float[n];
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
float deviation = (float)Math.Tanh((p - ambientPressure) / ambientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx;
}
// ----- Build trianglestrip vertices -----
int segmentsPerCell = 8; // smoothness
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2]; // top + bottom for each point
int idx = 0;
for (int i = 0; i < n; i++)
{
// ---- Cell centre ----
float x = centers[i];
float r = radii[i];
double p = pipe.GetCellPressure(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);
// ---- Intermediate segments after this cell (if not last) ----
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
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);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
}
}
}
// Draw the pipe as a triangle strip
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
// ----- Closed end indicator (right) -----
float wallThickness = 8f;
var wall = new RectangleShape(new Vector2f(wallThickness, winHeight * 0.6f));
wall.Position = new Vector2f(pipeEndX, pipeCenterY - winHeight * 0.6f / 2f);
wall.FillColor = new Color(180, 180, 180);
target.Draw(wall);
}
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
private Color PressureColor(double pressure)
{
double range = ambientPressure * 0.05; // ±5% gives full colour swing
double t = (pressure - ambientPressure) / range;
t = Math.Clamp(t, -1.0, 1.0);
byte r, g, b;
if (t < 0)
{
double factor = -t;
r = 0;
g = (byte)(255 * (1 - factor));
b = (byte)(255 * factor);
}
else
{
double factor = t;
r = (byte)(255 * factor);
g = (byte)(255 * (1 - factor));
b = 0;
}
return new Color(r, g, b);
}
}
}

View File

@@ -1,23 +1,121 @@
using SFML.Graphics;
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
namespace FluidSim.Core
namespace FluidSim.Tests
{
public abstract class Scenario
{
/// <summary>
/// Initialize the scenario with a given audio sample rate.
/// </summary>
/// <summary>Initialize the scenario with a given audio sample rate.</summary>
public abstract void Initialize(int sampleRate);
/// <summary>
/// Advance one simulation step and return an audio sample.
/// The step size is 1 / sampleRate seconds.
/// </summary>
/// <summary>Advance one simulation step and return an audio sample.</summary>
public abstract float Process();
/// <summary>
/// Draw the current simulation state onto the given SFML render target.
/// </summary>
/// <summary>Draw the current simulation state onto the given SFML render target.</summary>
public abstract void Draw(RenderWindow target);
// ---------- Shared drawing helpers ----------
protected const double AmbientPressure = 101325.0;
/// <summary>Blue (low) → Green (ambient) → Red (high).</summary>
protected Color PressureColor(double pressure)
{
double range = AmbientPressure * 0.05; // ±5% gives full colour swing
double t = (pressure - AmbientPressure) / range;
t = Math.Clamp(t, -1.0, 1.0);
byte r, g, b;
if (t < 0)
{
double factor = -t;
r = 0;
g = (byte)(255 * (1 - factor));
b = (byte)(255 * factor);
}
else
{
double factor = t;
r = (byte)(255 * factor);
g = (byte)(255 * (1 - factor));
b = 0;
}
return new Color(r, g, b);
}
/// <summary>
/// Draws the pipe as a smooth trianglestrip whose radius varies with cell pressure.
/// </summary>
protected void DrawPipe(RenderWindow target, Pipe1D pipe, float pipeCenterY, float pipeStartX, float pipeEndX)
{
int n = pipe.CellCount;
if (n < 2) return;
float pipeLengthPx = pipeEndX - pipeStartX;
float dx = pipeLengthPx / (n - 1); // spacing between cell centres
float baseRadius = 25f;
float rangeFactor = 1f;
float scaleFactor = 5f;
// ----- smoothstep helper -----
static float SmoothStep(float edge0, float edge1, float x)
{
float t = Math.Clamp((x - edge0) / (edge1 - edge0), 0f, 1f);
return t * t * (3f - 2f * t);
}
// ----- Precompute cell positions and radii -----
var centers = new float[n];
var radii = new float[n];
for (int i = 0; i < n; i++)
{
double p = pipe.GetCellPressure(i);
float deviation = (float)Math.Tanh((p - AmbientPressure) / AmbientPressure / rangeFactor);
radii[i] = baseRadius * (1f + deviation * scaleFactor);
if (radii[i] < 2f) radii[i] = 2f;
centers[i] = pipeStartX + i * dx;
}
// ----- Build trianglestrip vertices -----
int segmentsPerCell = 8;
int totalPoints = n + (n - 1) * segmentsPerCell;
Vertex[] stripVertices = new Vertex[totalPoints * 2];
int idx = 0;
for (int i = 0; i < n; i++)
{
float x = centers[i];
float r = radii[i];
double p = pipe.GetCellPressure(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);
if (i < n - 1)
{
for (int s = 1; s <= segmentsPerCell; s++)
{
float t = s / (float)segmentsPerCell;
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);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY - ri), coli);
stripVertices[idx++] = new Vertex(new Vector2f(xi, pipeCenterY + ri), coli);
}
}
}
var pipeMesh = new VertexArray(PrimitiveType.TriangleStrip, (uint)stripVertices.Length);
for (int i = 0; i < stripVertices.Length; i++)
pipeMesh[(uint)i] = stripVertices[i];
target.Draw(pipeMesh);
}
}
}

View File

@@ -1,158 +0,0 @@
using System;
using FluidSim.Components;
using FluidSim.Utils;
using SFML.Graphics;
using SFML.System;
namespace FluidSim.Core
{
public class SodShockTubeScenario : Scenario
{
private Solver solver;
private Pipe1D pipe;
private int stepCount;
private double time;
private double dt;
private double ambientPressure = 1.0 * Units.atm;
private const double GasConstant = 287.0;
public override void Initialize(int sampleRate)
{
dt = 1.0 / sampleRate;
double length = 1.0;
double area = 1.0;
int nCells = 200;
pipe = new Pipe1D(length, area, sampleRate, forcedCellCount: nCells);
pipe.SetUniformState(0.125, 0.0, 0.1 * ambientPressure); // right state
// Left half high pressure
for (int i = 0; i < nCells / 2; i++)
pipe.SetCellState(i, 1.0, 0.0, ambientPressure);
solver = new Solver();
solver.SetTimeStep(dt);
solver.AddPipe(pipe);
solver.SetPipeBoundary(pipe, isA: true, BoundaryType.ClosedEnd);
solver.SetPipeBoundary(pipe, isA: false, BoundaryType.ClosedEnd);
}
public override float Process()
{
float sample = solver.Step();
time += dt;
stepCount++;
double pMid = pipe.GetPressureAtFraction(0.5);
float audio = (float)((pMid - ambientPressure) / ambientPressure);
bool log = true;
if (log)
{
int n = pipe.GetCellCount();
Console.WriteLine($"step {stepCount}:");
Console.WriteLine("i rho (kg/m³) p (Pa) T (K) u (m/s)");
for (int i = 0; i < n; i++)
{
if (i % 10 == 0)
{
double rho = pipe.GetCellDensity(i);
double p = pipe.GetCellPressure(i);
double u = pipe.GetCellVelocity(i);
double T = p / (rho * GasConstant); // GasConstant = 287.0
Console.WriteLine($"{i,-4} {rho,10:F4} {p,10:F1} {T,8:F2} {u,10:F4}");
}
}
Console.WriteLine();
}
return audio;
}
public override void Draw(RenderWindow target)
{
float winW = target.GetView().Size.X;
float winH = target.GetView().Size.Y;
float centerY = winH / 2f;
float margin = 40f;
float pipeStartX = margin;
float pipeEndX = winW - margin;
float pipeLenPx = pipeEndX - pipeStartX;
int n = pipe.GetCellCount();
float dx = pipeLenPx / (n - 1);
float baseRadius = 60f;
Vertex[] vertices = new Vertex[n * 2];
for (int i = 0; i < n; i++)
{
float x = pipeStartX + i * dx;
double p = pipe.GetCellPressure(i);
double rho = pipe.GetCellDensity(i);
double T = p / (rho * GasConstant); // temperature in Kelvin
// Radius from pressure (exaggerated deviation)
float r = baseRadius * (float)(p / ambientPressure * 2);
if (r < 4f) r = 4f;
// Colour from temperature
Color col = TemperatureColor(T);
vertices[i * 2] = new Vertex(new Vector2f(x, centerY - r), col);
vertices[i * 2 + 1] = new Vertex(new Vector2f(x, centerY + r), col);
}
target.Draw(vertices, PrimitiveType.TriangleStrip);
// Diaphragm marker (faint white line at initial interface)
float diaphragmX = pipeStartX + (n / 2) * dx;
var line = new RectangleShape(new Vector2f(2f, winH * 0.5f));
line.Position = new Vector2f(diaphragmX - 1f, centerY - winH * 0.25f);
line.FillColor = new Color(255, 255, 255, 80);
target.Draw(line);
}
/// <summary>
/// Custom temperaturetohue mapping that matches the given Sodtube hue values:
/// 250K → 176, 300K → 122, 350K → 120?, 450K → 71.
/// Interpolates piecewise linearly, clamping outside [250,450].
/// </summary>
private Color TemperatureColor(double T)
{
// 1. Map temperature to hue (0360)
double[] Tknots = { 250, 282, 353, 450 };
double[] Hknots = { 176, 179, 122, 71 };
double hue;
if (T <= Tknots[0]) hue = Hknots[0];
else if (T >= Tknots[^1]) hue = Hknots[^1];
else
{
int i = 0;
while (i < Tknots.Length - 1 && T > Tknots[i + 1]) i++;
double frac = (T - Tknots[i]) / (Tknots[i + 1] - Tknots[i]);
hue = Hknots[i] + frac * (Hknots[i + 1] - Hknots[i]);
}
// 2. Convert hue to RGB (S = 1, V = 1)
double h = hue / 60.0;
int sector = (int)Math.Floor(h);
double f = h - sector;
byte p = 0;
byte q = (byte)(255 * (1 - f));
byte tByte = (byte)(255 * f);
byte v = 255;
byte r, g, b;
switch (sector % 6)
{
case 0: r = v; g = tByte; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = tByte; break;
case 3: r = p; g = q; b = v; break;
case 4: r = tByte; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
return new Color(r, g, b);
}
}
}

96
Scenarios/TestScenario.cs Normal file
View File

@@ -0,0 +1,96 @@
using System;
using SFML.Graphics;
using SFML.System;
using FluidSim.Components;
using FluidSim.Core;
namespace FluidSim.Tests
{
public class TestScenario : Scenario
{
private Solver solver;
private Volume0D volume;
private Pipe1D pipe;
private Atmosphere atmosphere;
private OrificeLink orificeLink;
private OpenEndLink openEndLink;
private int stepCount;
public override void Initialize(int sampleRate)
{
double dt = 1.0 / sampleRate;
solver = new Solver();
solver.SetTimeStep(dt);
volume = new Volume0D(1e-3, 150000.0, 300.0);
solver.AddComponent(volume);
pipe = new Pipe1D(2.0, 1e-4, 20);
solver.AddComponent(pipe);
atmosphere = new Atmosphere();
solver.AddComponent(atmosphere);
// Volume → left pipe end (orifice)
var volPort = volume.CreatePort();
orificeLink = new OrificeLink(volPort, pipe, isPipeLeftEnd: true, areaProvider: () => 1e-5)
{
DischargeCoefficient = 0.62,
Gamma = volume.Gamma,
GasConstant = volume.GasConstant
};
solver.AddOrificeLink(orificeLink);
// Right pipe end → atmosphere (characteristic openend)
openEndLink = new OpenEndLink(pipe, isLeftEnd: false)
{
AmbientPressure = 101325.0,
Gamma = 1.4
};
solver.AddOpenEndLink(openEndLink);
stepCount = 0;
Console.WriteLine("TestScenario initialized with sampleRate = " + sampleRate);
}
public override float Process()
{
solver.Step();
stepCount++;
if (stepCount % 100 == 0)
{
double volPressure = volume.Pressure;
double volMass = volume.Mass;
double pipeLeftPressure = pipe.GetCellPressure(0);
double pipeRightPressure = pipe.GetCellPressure(pipe.CellCount - 1);
double mdotOrifice = orificeLink.LastMassFlowRate;
double mdotOpen = openEndLink.LastMassFlowRate;
Console.WriteLine($"Step {stepCount}:");
Console.WriteLine($" Vol Pressure = {volPressure:F1} Pa, Mass = {volMass:E4} kg");
Console.WriteLine($" Pipe left P = {pipeLeftPressure:F1} Pa, right P = {pipeRightPressure:F1} Pa");
Console.WriteLine($" Orifice mdot = {mdotOrifice:E4} kg/s, Openend mdot = {mdotOpen:E4} kg/s");
Console.WriteLine();
}
// Audio sample from the openend mass flow
return (float)openEndLink.LastMassFlowRate;
}
public override void Draw(RenderWindow target)
{
float winWidth = target.GetView().Size.X;
float winHeight = target.GetView().Size.Y;
float pipeCenterY = winHeight / 2f;
float margin = 60f;
float pipeStartX = margin;
float pipeEndX = winWidth - margin;
// Use the shared pipe drawing from the base class
DrawPipe(target, pipe, pipeCenterY, pipeStartX, pipeEndX);
}
}
}