General testing

This commit is contained in:
2026-05-05 10:32:30 +02:00
parent ff4c4aef23
commit d963032e74
11 changed files with 794 additions and 448 deletions

46
Core/NozzleFlow.cs Normal file
View File

@@ -0,0 +1,46 @@
using System;
using FluidSim.Components;
namespace FluidSim.Core
{
public static class NozzleFlow
{
public static void Compute(Volume0D vol, double area, double downstreamPressure,
out double massFlow, out double rhoFace, out double uFace, out double pFace,
double gamma = 1.4)
{
// Default fallback (no flow)
massFlow = 0.0;
rhoFace = 0.0;
uFace = 0.0;
pFace = 0.0;
if (vol == null || vol.Mass <= 0 || vol.Volume <= 0)
return;
double p0 = vol.Pressure;
double T0 = vol.Temperature;
double R = vol.GasConstant;
double rho0 = vol.Density;
if (double.IsNaN(p0) || double.IsNaN(T0) || double.IsNaN(rho0) ||
p0 <= 0 || T0 <= 0 || rho0 <= 0)
return;
double pr = downstreamPressure / p0;
double choked = Math.Pow(2.0 / (gamma + 1.0), gamma / (gamma - 1.0));
if (pr < choked) pr = choked;
double M = Math.Sqrt((2.0 / (gamma - 1.0)) * (Math.Pow(pr, -(gamma - 1.0) / gamma) - 1.0));
if (double.IsNaN(M)) return;
uFace = M * Math.Sqrt(gamma * R * T0);
rhoFace = rho0 * Math.Pow(pr, 1.0 / gamma);
pFace = p0 * pr;
massFlow = rhoFace * uFace * area;
if (double.IsNaN(massFlow) || double.IsInfinity(massFlow))
massFlow = 0.0;
}
}
}

View File

@@ -0,0 +1,20 @@
using FluidSim.Components;
namespace FluidSim.Core
{
public class PipeVolumeConnection
{
public Volume0D Volume { get; }
public Pipe1D Pipe { get; }
public bool IsPipeLeftEnd { get; }
public double OrificeArea { get; set; }
public PipeVolumeConnection(Volume0D vol, Pipe1D pipe, bool isPipeLeftEnd, double orificeArea)
{
Volume = vol;
Pipe = pipe;
IsPipeLeftEnd = isPipeLeftEnd;
OrificeArea = orificeArea;
}
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using FluidSim.Components;
using FluidSim.Interfaces;
namespace FluidSim.Core
{
@@ -9,162 +8,80 @@ namespace FluidSim.Core
{
private readonly List<Volume0D> _volumes = new();
private readonly List<Pipe1D> _pipes = new();
private readonly List<Connection> _connections = new();
private readonly List<PipeVolumeConnection> _connections = new();
private double _dt;
private double _ambientPressure = 101325.0;
public void SetAmbientPressure(double p) => _ambientPressure = p;
public void AddVolume(Volume0D v) => _volumes.Add(v);
public void AddPipe(Pipe1D p) => _pipes.Add(p);
public void AddConnection(Connection c) => _connections.Add(c);
public void AddConnection(PipeVolumeConnection c) => _connections.Add(c);
public void SetTimeStep(double dt) => _dt = dt;
/// <summary>
/// Set boundary type for a pipe end. isA = true for port A (left), false for port B (right).
/// </summary>
public void SetPipeBoundary(Pipe1D pipe, bool isA, BoundaryType type, double ambientPressure = 101325.0)
{
if (isA)
{
pipe.SetABoundaryType(type);
if (type == BoundaryType.OpenEnd)
pipe.SetAAmbientPressure(ambientPressure);
if (type == BoundaryType.OpenEnd) pipe.SetAAmbientPressure(ambientPressure);
}
else
{
pipe.SetBBoundaryType(type);
if (type == BoundaryType.OpenEnd)
pipe.SetBAmbientPressure(ambientPressure);
if (type == BoundaryType.OpenEnd) pipe.SetBAmbientPressure(ambientPressure);
}
}
public float Step()
{
// 1. Volumes publish state
foreach (var v in _volumes)
v.PushStateToPort();
// 2. Set volume BCs for volumecoupled ends
// 1. Compute nozzle flows and update volumes (once per audio sample)
foreach (var conn in _connections)
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
{
var pipe = GetPipe(conn.PortA);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortA, conn.PortB);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
{
var pipe = GetPipe(conn.PortB);
bool isA = pipe.PortB == conn.PortB;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortB, conn.PortA);
}
double downstreamPressure = conn.IsPipeLeftEnd
? conn.Pipe.GetCellPressure(0)
: conn.Pipe.GetCellPressure(conn.Pipe.GetCellCount() - 1);
NozzleFlow.Compute(conn.Volume, conn.OrificeArea, downstreamPressure,
out double mdot, out double rhoFace, out double uFace, out double pFace,
gamma: conn.Volume.Gamma);
// Limit mass flow to available mass
double maxMdot = conn.Volume.Mass / _dt;
if (mdot > maxMdot) mdot = maxMdot;
if (mdot < -maxMdot) mdot = -maxMdot;
conn.Volume.MassFlowRateIn = -mdot;
conn.Volume.SpecificEnthalpyIn = (conn.Volume.Gamma / (conn.Volume.Gamma - 1.0)) *
(conn.Volume.Pressure / Math.Max(conn.Volume.Density, 1e-12));
conn.Volume.Integrate(_dt);
if (conn.IsPipeLeftEnd)
conn.Pipe.SetGhostLeft(rhoFace, uFace, pFace);
else
conn.Pipe.SetGhostRight(rhoFace, uFace, pFace);
}
// 3. Substeps
// 2. Determine required substeps
int nSub = 1;
foreach (var p in _pipes)
nSub = Math.Max(nSub, p.GetRequiredSubSteps(_dt));
double dtSub = _dt / nSub;
// 3. Substep loop for pipes
for (int sub = 0; sub < nSub; sub++)
{
foreach (var p in _pipes)
p.SimulateSingleStep(dtSub);
foreach (var conn in _connections)
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
{
var pipe = GetPipe(conn.PortA);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
TransferAndIntegrate(conn.PortA, conn.PortB, dtSub);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
{
var pipe = GetPipe(conn.PortB);
bool isA = pipe.PortB == conn.PortB;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
TransferAndIntegrate(conn.PortB, conn.PortA, dtSub);
}
}
if (sub < nSub - 1)
{
foreach (var v in _volumes)
v.PushStateToPort();
foreach (var conn in _connections)
{
if (IsPipePort(conn.PortA) && IsVolumePort(conn.PortB))
{
var pipe = GetPipe(conn.PortA);
bool isA = pipe.PortA == conn.PortA;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortA, conn.PortB);
}
else if (IsVolumePort(conn.PortA) && IsPipePort(conn.PortB))
{
var pipe = GetPipe(conn.PortB);
bool isA = pipe.PortB == conn.PortB;
if ((isA && pipe.ABCType == BoundaryType.VolumeCoupling) ||
(!isA && pipe.BBCType == BoundaryType.VolumeCoupling))
SetVolumeBC(conn.PortB, conn.PortA);
}
}
}
}
// 5. Audio samples (none for now, but placeholder)
var audioSamples = new List<float>();
foreach (var conn in _connections)
{
if (conn is SoundConnection sc)
audioSamples.Add(sc.GetAudioSample());
}
// 6. Clear BC flags
// 4. Clear ghost flags
foreach (var p in _pipes)
p.ClearBC();
p.ClearGhostFlag();
return SoundProcessor.MixAndClip(audioSamples.ToArray());
}
// 5. Return raw mass flow from the first pipes open end (assumed exhaust tailpipe)
if (_pipes.Count > 0)
return (float)_pipes[0].GetOpenEndMassFlow();
private bool IsVolumePort(Port p) => _volumes.Exists(v => v.Port == p);
private bool IsPipePort(Port p) => _pipes.Exists(pp => pp.PortA == p || pp.PortB == p);
private Pipe1D GetPipe(Port p) => _pipes.Find(pp => pp.PortA == p || pp.PortB == p);
private Volume0D GetVolume(Port p) => _volumes.Find(v => v.Port == p);
private void SetVolumeBC(Port pipePort, Port volPort)
{
var pipe = GetPipe(pipePort);
if (pipe == null) return;
bool isA = pipe.PortA == pipePort;
if (isA)
pipe.SetAVolumeState(volPort.Density, volPort.Pressure);
else
pipe.SetBVolumeState(volPort.Density, volPort.Pressure);
}
private void TransferAndIntegrate(Port pipePort, Port volPort, double dtSub)
{
double mdot = pipePort.MassFlowRate;
volPort.MassFlowRate = -mdot;
if (mdot < 0) // pipe → volume
{
volPort.SpecificEnthalpy = pipePort.SpecificEnthalpy;
}
// else volumes own enthalpy (from PushStateToPort) is used
GetVolume(volPort)?.Integrate(dtSub);
return 0f;
}
}
}

View File

@@ -1,23 +1,155 @@
namespace FluidSim.Core
{
/// <summary>
/// Mixes multiple audio samples and applies a softclipping tanh.
/// </summary>
public static class SoundProcessor
{
/// <summary>Overall gain applied after mixing (before tanh).</summary>
public static float MasterGain { get; set; } = 0.01f;
using System;
/// <summary>
/// Mixes an array of raw audio samples and returns a single sample in [1, 1].
/// </summary>
public static float MixAndClip(params float[] samples)
namespace FluidSim.Core
{
public class SoundProcessor
{
// Monopole state
private double lastMassFlow = 0.0;
private double dt;
// Resonant bandpass filter (secondorder)
private double b0, b1, b2, a1, a2;
private double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
private double pipeLength;
// Reverb (outdoor)
private float[] delayLine;
private int writeIndex;
private float feedback = 0.50f;
private float lowpassCoeff = 0.70f;
private float lastFeedbackSample = 0f;
// Turbulence (pink noise scaled by U³)
private PinkNoiseGenerator pinkNoise;
private float turbulenceGain = 0.05f;
private double pipeArea;
private double ambientPressure = 101325.0;
// Gains
private float masterGain = 0.0005f;
private float pressureGain = 0.12f;
public SoundProcessor(int sampleRate, double pipeLengthMeters, double pipeDiameterMeters = 0.04, float reverbTimeMs = 200.0f)
{
dt = 1.0 / sampleRate;
pipeLength = pipeLengthMeters;
pipeArea = Math.PI * Math.Pow(pipeDiameterMeters / 2.0, 2.0);
// Design resonant filter at pipe fundamental frequency
double c = 340.0;
double f0 = c / (4.0 * pipeLength);
double Q = 15.0;
double omega = 2.0 * Math.PI * f0;
double alpha = Math.Sin(omega * dt) / (2.0 * Q);
double norm = 1.0 / (1.0 + alpha);
b0 = alpha * norm;
b1 = 0.0;
b2 = -alpha * norm;
a1 = -2.0 * Math.Cos(omega * dt) * norm;
a2 = (1.0 - alpha) * norm;
// Reverb delay line
int delaySamples = (int)(sampleRate * reverbTimeMs / 1000.0);
delayLine = new float[delaySamples];
writeIndex = 0;
pinkNoise = new PinkNoiseGenerator();
}
public float MasterGain
{
get => masterGain;
set => masterGain = value;
}
public float PressureGain
{
get => pressureGain;
set => pressureGain = value;
}
public float TurbulenceGain
{
get => turbulenceGain;
set => turbulenceGain = value;
}
public void SetAmbientPressure(double p) => ambientPressure = p;
public void SetPipeDiameter(double diameterMeters) => pipeArea = Math.PI * Math.Pow(diameterMeters / 2.0, 2.0);
public float Process(float massFlow, float pipeEndPressure)
{
// 1. Monopole source: d(mdot)/dt
double derivative = (massFlow - lastMassFlow) / dt;
lastMassFlow = massFlow;
float monopole = (float)(derivative * masterGain);
// 2. Pressure difference (lowfrequency component)
float pressureDiff = (float)((pipeEndPressure - ambientPressure) / ambientPressure) * pressureGain;
float mixed = monopole + pressureDiff;
// DO NOT clamp here let the filter and final clamp handle dynamics
// 3. Resonant bandpass filter
double y = b0 * mixed + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
x2 = x1; x1 = mixed;
y2 = y1; y1 = y;
float resonant = (float)Math.Clamp(y, -1f, 1f);
// 4. Turbulence noise: amplitude ∝ U³ (empirical for low speeds)
double velocity = massFlow / (pipeArea * 1.225);
double Uref = 100.0;
double turbulenceAmp = Math.Pow(Math.Abs(velocity) / Uref, 3.0);
float pink = pinkNoise.Next() * turbulenceGain * (float)turbulenceAmp;
resonant += pink;
resonant = Math.Clamp(resonant, -1f, 1f);
// 5. Outdoor reverb
float delayed = delayLine[writeIndex];
float filteredDelay = delayed * lowpassCoeff + lastFeedbackSample * (1f - lowpassCoeff);
lastFeedbackSample = filteredDelay;
float wet = delayed + filteredDelay * feedback;
delayLine[writeIndex] = resonant + filteredDelay * feedback;
writeIndex = (writeIndex + 1) % delayLine.Length;
// 6. Dry/wet mix
float output = resonant * 0.7f + wet * 0.3f;
output = Math.Clamp(output, -1f, 1f);
return output;
}
}
internal class PinkNoiseGenerator
{
private readonly Random random = new Random();
private readonly float[] whiteNoise = new float[7];
private int currentIndex = 0;
public PinkNoiseGenerator()
{
for (int i = 0; i < 7; i++)
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
}
public float Next()
{
whiteNoise[0] = (float)(random.NextDouble() * 2.0 - 1.0);
currentIndex = (currentIndex + 1) & 0x7F;
int updateMask = 0;
int temp = currentIndex;
for (int i = 0; i < 7; i++)
{
if ((temp & 1) == 0)
updateMask |= (1 << i);
temp >>= 1;
}
for (int i = 1; i < 7; i++)
if ((updateMask & (1 << i)) != 0)
whiteNoise[i] = (float)(random.NextDouble() * 2.0 - 1.0);
float sum = 0f;
foreach (float s in samples)
sum += s;
sum *= MasterGain;
return sum;
for (int i = 0; i < 7; i++) sum += whiteNoise[i];
return sum / 3.5f;
}
}
}