refactoring (broken right now)
This commit is contained in:
@@ -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 & cycle‑aware 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
// 1‑litre cavity, 10% over‑pressure
|
||||
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 volume‑coupled 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ----- Pre‑compute 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 triangle‑strip 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 triangle‑strip 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);
|
||||
}
|
||||
|
||||
// ----- Pre‑compute 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 triangle‑strip 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 temperature‑to‑hue mapping that matches the given Sod‑tube hue values:
|
||||
/// 250 K → 176, 300 K → 122, 350 K → 120?, 450 K → 71.
|
||||
/// Interpolates piecewise linearly, clamping outside [250,450].
|
||||
/// </summary>
|
||||
private Color TemperatureColor(double T)
|
||||
{
|
||||
// 1. Map temperature to hue (0–360)
|
||||
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
96
Scenarios/TestScenario.cs
Normal 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 open‑end)
|
||||
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, Open‑end mdot = {mdotOpen:E4} kg/s");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Audio sample from the open‑end 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user