engine almost working, backup before adding gas types.

This commit is contained in:
max
2026-05-07 20:07:15 +02:00
parent 14f5ba925f
commit 92d84eacfe
18 changed files with 1236 additions and 587 deletions

View File

@@ -10,8 +10,8 @@ namespace FluidSim.Components
public double PreviousAngle { get; set; } // ← now has public setter
public double Inertia { get; set; } = 0.2;
public double FrictionConstant { get; set; } = 2.0; // N·m
public double FrictionViscous { get; set; } = 0.005; // N·m per rad/s
public double FrictionConstant { get; set; } = 0.0; // N·m
public double FrictionViscous { get; set; } = 0.000; // N·m per rad/s
private double externalTorque;

274
Components/Cylinder.cs Normal file
View File

@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using FluidSim.Interfaces;
namespace FluidSim.Components
{
public class Cylinder : IComponent
{
// Public ports
public Port IntakePort { get; }
public Port ExhaustPort { get; }
public Crankshaft Crankshaft { get; }
private readonly Port[] _ports;
IReadOnlyList<Port> IComponent.Ports => _ports;
// Geometry
public double Bore { get; }
public double Stroke { get; }
public double ConRodLength { get; }
public double CompressionRatio { get; }
// Valve timings (degrees, 0 = TDC compression, 720° full cycle)
public double IVO { get; }
public double IVC { get; }
public double EVO { get; }
public double EVC { get; }
// Valve areas
public double MaxIntakeArea { get; set; } = 0.0005;
public double MaxExhaustArea { get; set; } = 0.0005;
// Ignition and combustion
public double SparkAdvance { get; set; } = 20.0; // °BTDC
public double WiebeA { get; set; } = 5.0;
public double WiebeM { get; set; } = 2.0;
public double WiebeDuration { get; set; } = 60.0; // degrees
public double WiebeStart { get; set; } = 5.0; // degrees after spark
// Fuel
public double StoichiometricAFR { get; set; } = 14.7;
public double FuelLowerHeatingValue { get; set; } = 44e6; // J/kg
// Heat loss
public double CylinderWallArea { get; set; } = 0.02; // m²
public double HeatTransferCoefficient { get; set; } = 100.0; // W/(m²·K)
public double AmbientTemperature { get; set; } = 300.0; // K
// State (public for drawing)
public double Volume => cylinderVolume;
public double Pressure => (Gamma - 1.0) * cylinderEnergy / Math.Max(cylinderVolume, 1e-12);
public double Temperature => Pressure / Math.Max(Density * GasConstant, 1e-12);
public double Density => cylinderMass / Math.Max(cylinderVolume, 1e-12);
public double Mass => cylinderMass;
public double PistonFraction => (cylinderVolume - clearanceVolume) / SweptVolume;
private double cylinderVolume;
private double cylinderMass;
private double cylinderEnergy;
private double trappedAirMass;
private double fuelMass;
private double burnFraction; // 01
private bool combustionActive;
private bool fuelInjected;
// --- Debounce flag: allows combustion only below a certain temperature ---
private bool _canCombust = true;
private const double CombustionEnableTemperature = 800.0; // K must cool below this to rearm
private const double Gamma = 1.4;
private const double GasConstant = 287.0;
// Absolute safety limits
private const double MaxPressurePa = 200e5; // 200 bar
private const double MaxTemperatureK = 3500.0; // 3500 K
public Cylinder(double bore, double stroke, double conRodLength, double compressionRatio,
double ivo, double ivc, double evo, double evc, double initialRPM = 1000)
{
Bore = bore;
Stroke = stroke;
ConRodLength = conRodLength;
CompressionRatio = compressionRatio;
IVO = ivo;
IVC = ivc;
EVO = evo;
EVC = evc;
Crankshaft = new Crankshaft(initialRPM);
cylinderVolume = clearanceVolume;
cylinderMass = 1.225 * clearanceVolume;
cylinderEnergy = 101325.0 * clearanceVolume / (Gamma - 1.0);
IntakePort = new Port { Owner = this };
ExhaustPort = new Port { Owner = this };
_ports = new[] { IntakePort, ExhaustPort };
}
// Derived volumes
private double SweptVolume => Math.PI * 0.25 * Bore * Bore * Stroke;
private double clearanceVolume => SweptVolume / (CompressionRatio - 1.0);
private double CrankRadius => Stroke / 2.0;
private double Obliquity => CrankRadius / ConRodLength;
// Crank angle in degrees (0720)
private double CrankDeg => (Crankshaft.CrankAngle % (4.0 * Math.PI)) * 180.0 / Math.PI % 720.0;
public double ComputeVolume(double thetaRad)
{
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;
return clearanceVolume + area * x;
}
public double IntakeValveArea => ValveArea(CrankDeg, IVO, IVC, MaxIntakeArea);
public double ExhaustValveArea => ValveArea(CrankDeg, EVO, EVC, MaxExhaustArea);
private double ValveArea(double thetaDeg, double opens, double closes, double maxArea)
{
double deg = thetaDeg % 720.0;
if (deg < 0) deg += 720.0;
if (deg >= opens && deg <= closes)
{
double half = (closes - opens) * 0.5;
double mid = opens + half;
double frac = 1.0 - Math.Abs(deg - mid) / half;
frac = Math.Clamp(frac, 0.0, 1.0);
return maxArea * frac;
}
return 0.0;
}
private double Wiebe(double angleSinceSpark)
{
if (angleSinceSpark < WiebeStart) return 0.0;
double phi = (angleSinceSpark - WiebeStart) / WiebeDuration;
if (phi <= 0) return 0.0;
return 1.0 - Math.Exp(-WiebeA * Math.Pow(phi, WiebeM + 1));
}
public void PreStep(double dt)
{
double prevVolume = cylinderVolume;
double crankAngleRad = Crankshaft.CrankAngle;
cylinderVolume = ComputeVolume(crankAngleRad);
// Volume work (done BY gas, positive when expanding)
double dV = cylinderVolume - prevVolume;
cylinderEnergy -= Pressure * dV;
double prevDeg = Crankshaft.PreviousAngle * 180.0 / Math.PI % 720.0;
double currDeg = crankAngleRad * 180.0 / Math.PI % 720.0;
// ----- Intake closing: capture trapped air mass and compute fuel -----
if (prevDeg >= IVO && prevDeg < IVC && currDeg >= IVC)
{
trappedAirMass = cylinderMass;
fuelMass = trappedAirMass / StoichiometricAFR;
fuelInjected = true;
}
// ----- Spark ignition (once per cycle, only if canCombust) -----
double sparkAngle = 0.0 - SparkAdvance;
if (sparkAngle < 0) sparkAngle += 720.0;
bool crossedSpark = (prevDeg < sparkAngle && currDeg >= sparkAngle) ||
(prevDeg > sparkAngle + 360.0 && currDeg < sparkAngle);
if (crossedSpark && !combustionActive && fuelInjected && _canCombust)
{
combustionActive = true;
burnFraction = 0.0;
}
// ----- Combustion progress -----
if (combustionActive)
{
double angleSinceSpark = currDeg - sparkAngle;
if (angleSinceSpark < 0) angleSinceSpark += 720.0;
double newFraction = Wiebe(angleSinceSpark);
if (newFraction >= 1.0 || angleSinceSpark > (WiebeDuration + WiebeStart + SparkAdvance))
{
newFraction = 1.0;
combustionActive = false;
_canCombust = false; // require cooldown before next ignition
}
double dFraction = newFraction - burnFraction;
if (dFraction > 0)
{
double dQ = fuelMass * FuelLowerHeatingValue * dFraction;
cylinderEnergy += dQ;
cylinderMass += fuelMass * dFraction;
burnFraction = newFraction;
}
}
// ----- Rearm combustion if temperature has dropped low enough -----
if (!combustionActive && !_canCombust && Temperature < CombustionEnableTemperature)
{
_canCombust = true;
}
// ----- Heat loss to cylinder walls -----
double dQ_loss = HeatTransferCoefficient * CylinderWallArea *
(Temperature - AmbientTemperature) * dt;
cylinderEnergy -= dQ_loss;
// Update port states
double p = Pressure, rho = Density, T = Temperature;
double h = Gamma / (Gamma - 1.0) * p / Math.Max(rho, 1e-12);
IntakePort.Pressure = p;
IntakePort.Density = rho;
IntakePort.Temperature = T;
IntakePort.SpecificEnthalpy = h;
ExhaustPort.Pressure = p;
ExhaustPort.Density = rho;
ExhaustPort.Temperature = T;
ExhaustPort.SpecificEnthalpy = h;
}
public void UpdateState(double dt)
{
double dm = 0.0;
double dE = 0.0;
foreach (var port in _ports)
{
dm += port.MassFlowRate * dt;
dE += port.MassFlowRate * port.SpecificEnthalpy * dt;
}
cylinderMass += dm;
cylinderEnergy += dE;
double V = Math.Max(cylinderVolume, 1e-12);
// --- Absolute pressure & temperature clamps ---
double currentP = (Gamma - 1.0) * cylinderEnergy / V;
if (currentP > MaxPressurePa)
cylinderEnergy = MaxPressurePa * V / (Gamma - 1.0);
double currentRho = cylinderMass / V;
double currentT = currentP / Math.Max(currentRho * GasConstant, 1e-12);
if (currentT > MaxTemperatureK)
{
double pAtTlimit = currentRho * GasConstant * MaxTemperatureK;
cylinderEnergy = pAtTlimit * V / (Gamma - 1.0);
}
// Existing safeguards
if (cylinderMass < 1e-9)
{
cylinderMass = 1e-9;
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
}
else if (cylinderEnergy < 0.0)
{
cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
}
if (cylinderMass < 0.0) cylinderMass = 1e-9;
if (cylinderEnergy < 0.0) cylinderEnergy = 101325.0 * V / (Gamma - 1.0);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using FluidSim.Interfaces;
namespace FluidSim.Components
@@ -9,6 +10,9 @@ namespace FluidSim.Components
/// </summary>
public class Pipe1D : IComponent
{
// ---------- Compiletime profiling flag ----------
public const bool EnableDetailedProfiling = false; // set to false in release builds
public Port PortA { get; }
public Port PortB { get; }
public double Area { get; }
@@ -32,7 +36,7 @@ namespace FluidSim.Components
private readonly double _gamma = 1.4;
private double[] _rho, _rhou, _E;
private double[] _fluxM, _fluxP, _fluxE; // flux at cell faces (0.._n)
private double[] _fluxM, _fluxP, _fluxE; // flux at cell faces (0.._n) kept for possible external use, not used internally anymore
private double _rhoGhostL, _uGhostL, _pGhostL;
private double _rhoGhostR, _uGhostR, _pGhostR;
@@ -41,6 +45,14 @@ namespace FluidSim.Components
private double _laminarCoeff;
private double _ambientEnergyReference;
// ---------- Profiling accumulators ----------
private long _profPrecomputeTicks;
private long _profLeftFluxTicks;
private long _profInteriorLoopTicks;
private long _profRightFluxTicks;
private long _profPortUpdateTicks;
private long _profCallCount;
public Pipe1D(double length, double area, int cellCount)
{
if (cellCount < 4) throw new ArgumentException("cellCount must be at least 4");
@@ -128,84 +140,142 @@ namespace FluidSim.Components
double dt = dtSub;
int n = _n;
// ---- Compute fluxes at all faces using LaxFriedrichs ----
// Left face (0): between ghostL and cell 0
double rL = Math.Max(_rhoGhostL, 1e-12);
double pL = _pGhostL;
double uL = _uGhostL;
double eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
double rR = Math.Max(_rho[0], 1e-12);
double pR = PressureScalar(0);
double uR = _rhou[0] / rR;
double eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
out _fluxM[0], out _fluxP[0], out _fluxE[0]);
// Internal faces (1 .. n-1)
for (int f = 1; f < n; f++)
{
int iL = f - 1;
int iR = f;
rL = Math.Max(_rho[iL], 1e-12);
pL = PressureScalar(iL);
uL = _rhou[iL] / rL;
eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
rR = Math.Max(_rho[iR], 1e-12);
pR = PressureScalar(iR);
uR = _rhou[iR] / rR;
eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
out _fluxM[f], out _fluxP[f], out _fluxE[f]);
}
// Right face (n): between cell n-1 and ghostR
rL = Math.Max(_rho[n - 1], 1e-12);
pL = PressureScalar(n - 1);
uL = _rhou[n - 1] / rL;
eL = pL / ((_gamma - 1.0) * rL) + 0.5 * uL * uL;
rR = Math.Max(_rhoGhostR, 1e-12);
pR = _pGhostR;
uR = _uGhostR;
eR = pR / ((_gamma - 1.0) * rR) + 0.5 * uR * uR;
LaxFriedrichsFlux(rL, uL, pL, eL, rR, uR, pR, eR,
out _fluxM[n], out _fluxP[n], out _fluxE[n]);
// ---- Cell update ----
double dt_dx = dt / _dx;
double coeff = _laminarCoeff * DampingMultiplier;
double relaxRate = EnergyRelaxationRate;
double gamma = _gamma;
double gm1 = gamma - 1.0;
// ---------- Profiling start ----------
long t0 = 0, t1 = 0;
if (EnableDetailedProfiling)
{
t0 = Stopwatch.GetTimestamp();
_profCallCount++;
}
// ---------- Phase 1: Precompute pressure and speed of sound ----------
double[] p = new double[n];
double[] c = new double[n];
for (int i = 0; i < n; i++)
{
double rho = Math.Max(_rho[i], 1e-12);
double u = _rhou[i] / rho;
p[i] = gm1 * (_E[i] - 0.5 * _rhou[i] * _rhou[i] / rho);
c[i] = Math.Sqrt(gamma * p[i] / rho);
}
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profPrecomputeTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 2: Left face flux (ghostL cell 0) ----------
double rL_ghost = Math.Max(_rhoGhostL, 1e-12);
double pL_ghost = _pGhostL;
double uL_ghost = _uGhostL;
double cL_ghost = Math.Sqrt(gamma * pL_ghost / rL_ghost);
LaxFlux(rL_ghost, uL_ghost, pL_ghost, cL_ghost,
_rho[0], _rhou[0] / Math.Max(_rho[0], 1e-12), p[0], c[0],
out double fluxM_left, out double fluxP_left, out double fluxE_left);
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profLeftFluxTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 3: Interior loop (fluxes + cell updates) ----------
double fluxM_prev = fluxM_left;
double fluxP_prev = fluxP_left;
double fluxE_prev = fluxE_left;
for (int i = 0; i < n - 1; i++)
{
int iL = i;
int iR = i + 1;
double rL = Math.Max(_rho[iL], 1e-12);
double uL = _rhou[iL] / rL;
double pL = p[iL];
double cL = c[iL];
double rR = Math.Max(_rho[iR], 1e-12);
double uR = _rhou[iR] / rR;
double pR = p[iR];
double cR = c[iR];
LaxFlux(rL, uL, pL, cL, rR, uR, pR, cR,
out double fluxM_right, out double fluxP_right, out double fluxE_right);
// Update cell i
double r = _rho[i];
double ru = _rhou[i];
double E = _E[i];
double dM = _fluxM[i + 1] - _fluxM[i];
double dP = _fluxP[i + 1] - _fluxP[i];
double dE_flux = _fluxE[i + 1] - _fluxE[i];
double newR = r - dt_dx * dM;
double newRu = ru - dt_dx * dP;
double newE = E - dt_dx * dE_flux;
double newR = r - dt_dx * (fluxM_right - fluxM_prev);
double newRu = ru - dt_dx * (fluxP_right - fluxP_prev);
double newE = E - dt_dx * (fluxE_right - fluxE_prev);
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
newRu *= dampingFactor;
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
newR = Math.Max(newR, 1e-12);
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
double eMin = 100.0 / (_gamma - 1.0) + kin;
double eMin = 100.0 / gm1 + kin;
newE = Math.Max(newE, eMin);
_rho[i] = newR;
_rhou[i] = newRu;
_E[i] = newE;
fluxM_prev = fluxM_right;
fluxP_prev = fluxP_right;
fluxE_prev = fluxE_right;
}
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profInteriorLoopTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 4: Right face flux (cell n1 ghostR) ----------
double rR_ghost = Math.Max(_rhoGhostR, 1e-12);
double pR_ghost = _pGhostR;
double uR_ghost = _uGhostR;
double cR_ghost = Math.Sqrt(gamma * pR_ghost / rR_ghost);
LaxFlux(_rho[n - 1], _rhou[n - 1] / Math.Max(_rho[n - 1], 1e-12), p[n - 1], c[n - 1],
rR_ghost, uR_ghost, pR_ghost, cR_ghost,
out double fluxM_right_final, out double fluxP_right_final, out double fluxE_right_final);
// Update last cell (identical to interior, but with final fluxes)
{
int i = n - 1;
double r = _rho[i];
double ru = _rhou[i];
double E = _E[i];
double newR = r - dt_dx * (fluxM_right_final - fluxM_prev);
double newRu = ru - dt_dx * (fluxP_right_final - fluxP_prev);
double newE = E - dt_dx * (fluxE_right_final - fluxE_prev);
double dampingFactor = Math.Exp(-coeff / Math.Max(r, 1e-12) * dt);
newRu *= dampingFactor;
double relaxFactor = Math.Exp(-relaxRate * dt);
newE = _ambientEnergyReference + (newE - _ambientEnergyReference) * relaxFactor;
newR = Math.Max(newR, 1e-12);
double kin = 0.5 * newRu * newRu / Math.Max(newR, 1e-12);
double eMin = 100.0 / gm1 + kin;
newE = Math.Max(newE, eMin);
_rho[i] = newR;
@@ -213,43 +283,68 @@ namespace FluidSim.Components
_E[i] = newE;
}
// Update port states
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profRightFluxTicks += (t1 - t0);
t0 = t1;
}
// ---------- Phase 5: Update port states ----------
(double rhoA, double uA, double pA) = GetInteriorStateLeft();
PortA.Pressure = pA; PortA.Density = rhoA;
PortA.Temperature = pA / (rhoA * 287.0);
PortA.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pA / rhoA;
PortA.SpecificEnthalpy = gm1 / (gamma - 1.0) * pA / rhoA;
(double rhoB, double uB, double pB) = GetInteriorStateRight();
PortB.Pressure = pB; PortB.Density = rhoB;
PortB.Temperature = pB / (rhoB * 287.0);
PortB.SpecificEnthalpy = _gamma / (_gamma - 1.0) * pB / rhoB;
PortB.SpecificEnthalpy = gm1 / (gamma - 1.0) * pB / rhoB;
if (EnableDetailedProfiling)
{
t1 = Stopwatch.GetTimestamp();
_profPortUpdateTicks += (t1 - t0);
}
}
// ---------- LaxFriedrichs flux ----------
// ---------- Local LaxFriedrichs flux function ----------
private void LaxFlux(double rL, double uL, double pL, double cL,
double rR, double uR, double pR, double cR,
out double fm, out double fp, out double fe)
{
double gm1 = _gamma - 1.0;
double EL = pL / (gm1 * rL) + 0.5 * uL * uL;
double ER = pR / (gm1 * rR) + 0.5 * uR * uR;
double Fm_L = rL * uL;
double Fp_L = rL * uL * uL + pL;
double Fe_L = (rL * EL + pL) * uL;
double Fm_R = rR * uR;
double Fp_R = rR * uR * uR + pR;
double Fe_R = (rR * ER + pR) * uR;
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rR - rL);
fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rR * uR - rL * uL);
fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (rR * ER - rL * EL);
}
// Original LaxFriedrichsFlux (kept for compatibility, can be removed if unused)
private void LaxFriedrichsFlux(double rL, double uL, double pL, double eL,
double rR, double uR, double pR, double eR,
out double fm, out double fp, out double fe)
{
// Primitive states
double rhoL = rL, rhoR = rR;
double EL = rhoL * eL; // total energy per volume = rho * (specific total energy)
double EL = rhoL * eL;
double ER = rhoR * eR;
// Conserved vectors U = (ρ, ρu, E)
// Flux F = (ρu, ρu²+p, (E+p)u)
double Fm_L = rhoL * uL;
double Fp_L = rhoL * uL * uL + pL;
double Fe_L = (EL + pL) * uL;
double Fm_R = rhoR * uR;
double Fp_R = rhoR * uR * uR + pR;
double Fe_R = (ER + pR) * uR;
// LaxFriedrichs dissipation coefficient α = max(|u|+c) over whole domain, but here we use local max to be simple:
double cL = Math.Sqrt(_gamma * pL / rL);
double cR = Math.Sqrt(_gamma * pR / rR);
double alpha = Math.Max(Math.Abs(uL) + cL, Math.Abs(uR) + cR);
fm = 0.5 * (Fm_L + Fm_R) - 0.5 * alpha * (rhoR - rhoL);
fp = 0.5 * (Fp_L + Fp_R) - 0.5 * alpha * (rhoR * uR - rhoL * uL);
fe = 0.5 * (Fe_L + Fe_R) - 0.5 * alpha * (ER - EL);
@@ -291,5 +386,42 @@ namespace FluidSim.Components
double e = p / ((_gamma - 1.0) * rho);
_E[i] = rho * e + 0.5 * rho * u * u;
}
// ---------- Public profiling interface ----------
public void ResetDetailCounters()
{
_profPrecomputeTicks = 0;
_profLeftFluxTicks = 0;
_profInteriorLoopTicks = 0;
_profRightFluxTicks = 0;
_profPortUpdateTicks = 0;
_profCallCount = 0;
}
public string GetDetailProfileReport()
{
if (!EnableDetailedProfiling)
return "Detailed profiling disabled.";
double freq = Stopwatch.Frequency;
long totalTicks = _profPrecomputeTicks + _profLeftFluxTicks +
_profInteriorLoopTicks + _profRightFluxTicks +
_profPortUpdateTicks;
if (totalTicks == 0) return "No profiling data.";
double totalSec = totalTicks / freq;
double avgCallSec = totalSec / _profCallCount;
double avgCallUs = avgCallSec * 1e6;
string report = $" Pipe detailed (over {_profCallCount} calls, total {totalSec * 1000:F2} ms):\n";
report += $" Avg per call: {avgCallUs:F2} µs\n";
report += $" Precompute p,c: {_profPrecomputeTicks * 100.0 / totalTicks:F1} % ({_profPrecomputeTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Left face flux: {_profLeftFluxTicks * 100.0 / totalTicks:F1} % ({_profLeftFluxTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Interior loop: {_profInteriorLoopTicks * 100.0 / totalTicks:F1} % ({_profInteriorLoopTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Right face flux: {_profRightFluxTicks * 100.0 / totalTicks:F1} % ({_profRightFluxTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
report += $" Port update: {_profPortUpdateTicks * 100.0 / totalTicks:F1} % ({_profPortUpdateTicks / freq * 1e6 / _profCallCount:F2} µs/call)\n";
return report;
}
}
}