Skip to main content

Multiphysics Engine

A fluent simulation engine for student-friendly PDE simulations on simple geometries, designed for visualization in Unity or web platforms. Built on existing finite difference infrastructure (Grid2D, GridOperators, ITimeStepper) with engineering materials, binary/JSON export, and optional ML/Monte Carlo integration.

Namespace: CSharpNumerics.Engines.Multiphysics


โš™๏ธ Pipeline Overviewโ€‹

Material โ†’ Geometry โ†’ Boundary โ†’ Source โ†’ Solve โ†’ Result โ†’ Export / ML

The engine builds on existing library capabilities:

StepUses
MaterialsEngineeringMaterial, EngineeringLibrary (Physics)
FD SpatialGrid2D, GridOperators.Laplacian2D, Advection2D, Divergence2D, SolvePoisson2D (Numerics)
Time IntegrationForward Euler (internal), ITimeStepper compatible
Beam TheorySolidExtensions - Euler-Bernoulli, Hooke's law (Physics)
Monte CarloIMonteCarloModel, MonteCarloSimulator (Statistics)
ML ClusteringClusteringExperiment, KMeans, evaluators (ML)
ML SurrogateIModel, Ridge, MLPRegressor (ML)

๐Ÿงช Simulation Typesโ€‹

TypePDEMethodDimension
HeatPlateโˆ‚T/โˆ‚t=ฮฑโˆ‡2T+S\partial T/\partial t = \alpha \nabla^2 T + SFD (Grid2D + Laplacian2D + forward Euler)2D
PipeFlowHagen-Poiseuille (cylindrical Laplacian)FD (1D transient)1D
ElectricFieldโˆ‡2ฯ†=โˆ’ฯ/ฮต\nabla^2 \varphi = -\rho/\varepsilonPoisson solver (Gauss-Seidel)2D
BeamStressEIuโ€ฒโ€ฒโ€ฒโ€ฒ=qEI u'''' = qAnalytical (SolidExtensions)1D
CylinderFlowIncompressible Navier-StokesChorin projection (FD + Poisson)2D
FluidFlow2DIncompressible Navier-Stokes (rectangular)Chorin projection (FD + Poisson)2D
MagneticFieldโˆ‡2A=โˆ’ฮผJ\nabla^2 A = -\mu J (magnetostatics)Poisson solver (Gauss-Seidel)2D
PlaneStressNavier-Cauchy (plane stress elasticity)FD (SOR iterative)2D

๐Ÿ”— Fluent API - SimulationType -> SimulationBuilder -> SimulationResultโ€‹

All simulations use the same fluent entry point:

using CSharpNumerics.Engines.Multiphysics;
using CSharpNumerics.Engines.Multiphysics.Enums;
using CSharpNumerics.Physics.Materials.Engineering;

var result = SimulationType.Create(MultiphysicsType.HeatPlate)
.WithMaterial(EngineeringLibrary.Aluminum)
.WithGeometry(width: 0.1, height: 0.1, nx: 20, ny: 20)
.WithBoundary(top: 100.0, bottom: 0.0, left: 0.0, right: 0.0)
.WithInitialCondition(0.0)
.AddSource(10, 10, 1e6)
.Solve(dt: 0.0001, steps: 500);

double[,] finalField = result.Field; // final temperature field
List<double[,]> timeline = result.Timeline; // per-step snapshots
double peak = result.MaxValue; // 100.0

โ™จ๏ธ HeatPlate - 2D Transient Heat Equationโ€‹

โˆ‚Tโˆ‚t=ฮฑโˆ‡2T+Sฯcp\frac{\partial T}{\partial t} = \alpha \nabla^2 T + \frac{S}{\rho c_p}

Thermal diffusivity ฮฑ=k/(ฯcp)\alpha = k / (\rho c_p) is automatically derived from the EngineeringMaterial.

var result = SimulationType.Create(MultiphysicsType.HeatPlate)
.WithMaterial(EngineeringLibrary.Copper) // ฮฑ = k/(ฯยทcp) โ‰ˆ 1.17e-4
.WithGeometry(width: 0.1, height: 0.1, nx: 50, ny: 50)
.WithBoundary(top: 200.0, bottom: 0.0, left: 0.0, right: 0.0)
.WithInitialCondition((x, y) => 20.0) // function-based IC
.AddSource(25, 25, 5e5) // point heat source
.Solve(dt: 0.00005, steps: 1000);

// Access timeline for animation
for (int step = 0; step < result.Timeline.Count; step++)
double[,] frame = result.Timeline[step];

๐Ÿšฐ PipeFlow - 1D Hagen-Poiseuilleโ€‹

Transient cylindrical channel flow converging to the parabolic velocity profile:

v(r)=R2โˆ’r24ฮผ(โˆ’dPdx)v(r) = \frac{R^2 - r^2}{4\mu} \left(-\frac{dP}{dx}\right)

var result = SimulationType.Create(MultiphysicsType.PipeFlow)
.WithMaterial(EngineeringLibrary.Water) // uses KinematicViscosity
.WithGeometry(length: 1.0, radius: 0.005, nodes: 21)
.WithBoundary(pressureGradient: -100.0)
.Solve(dt: 0.02, steps: 3000);

double[] velocity = result.Values; // radial velocity profile
double[] positions = result.Positions; // radial positions [0..R]
double vCenter = result.Values[0]; // peak centreline velocity

โšก ElectricField - 2D Poisson/Laplaceโ€‹

Solves โˆ‡2ฯ†=โˆ’ฯ/ฮต\nabla^2 \varphi = -\rho/\varepsilon with Dirichlet BCs, then computes E=โˆ’โˆ‡ฯ†\mathbf{E} = -\nabla \varphi:

var result = SimulationType.Create(MultiphysicsType.ElectricField)
.WithMaterial(EngineeringLibrary.Air) // uses ElectricPermittivity
.WithGeometry(width: 1.0, height: 1.0, nx: 40, ny: 40)
.WithBoundary(top: 0.0, bottom: 0.0, left: 0.0, right: 100.0)
.AddSource(20, 20, 1e-6) // point charge
.Solve(maxIterations: 20000, tolerance: 1e-8);

double[,] potential = result.Field; // ฯ†(x, y)
double[,] Ex = result.Ex; // -โˆ‚ฯ†/โˆ‚x
double[,] Ey = result.Ey; // -โˆ‚ฯ†/โˆ‚y

๐Ÿ—๏ธ BeamStress - 1D Euler-Bernoulliโ€‹

Analytical beam solutions with multiple support types and load combinations:

var result = SimulationType.Create(MultiphysicsType.BeamStress)
.WithMaterial(EngineeringLibrary.Steel) // uses YoungsModulus
.WithGeometry(length: 2.0, nodes: 201)
.WithCrossSection(width: 0.05, height: 0.1) // rectangular
// .WithCrossSection(radius: 0.02) // circular
// .WithSecondMoment(1.5e-6) // custom I
.WithBoundary(BeamSupport.Cantilever) // or SimplySupported, FixedFixed
.AddSource(position: 2.0, value: 1000) // point load at tip
.WithSource(500) // uniform distributed load
.Solve();

double[] deflection = result.Values; // ฮด(x)
double[] moment = result.BendingMoment; // M(x)
double[] shear = result.ShearForce; // V(x)
double[] stress = result.Stress; // ฯƒ(x) = Mยทy_max/I
double[] x = result.Positions; // node positions

Analytical reference

SupportPoint Load P at tipUniform Load q
Cantileverฮดmax=PL33EI\delta_{max} = \frac{PL^3}{3EI}ฮดmax=qL48EI\delta_{max} = \frac{qL^4}{8EI}
Simply Supported-ฮดmid=5qL4384EI\delta_{mid} = \frac{5qL^4}{384EI}
Fixed-Fixed-ฮดmid=qL4384EI\delta_{mid} = \frac{qL^4}{384EI}

๐Ÿ”ต CylinderFlow โ€” 2D Incompressible Navier-Stokesโ€‹

Simulates viscous flow around a circular cylinder using Chorin's projection (fractional-step) method. Classic benchmark for observing the Kรกrmรกn vortex street at moderate Reynolds numbers.

โˆ‚vโˆ‚t+(vโ‹…โˆ‡)v=โˆ’1ฯโˆ‡p+ฮฝโˆ‡2v,โˆ‡โ‹…v=0\frac{\partial \mathbf{v}}{\partial t} + (\mathbf{v} \cdot \nabla)\mathbf{v} = -\frac{1}{\rho}\nabla p + \nu \nabla^2 \mathbf{v}, \quad \nabla \cdot \mathbf{v} = 0

var result = SimulationType.Create(MultiphysicsType.CylinderFlow)
.WithMaterial(EngineeringLibrary.Water) // uses KinematicViscosity
.WithGeometry(width: 2.0, height: 0.8, nx: 200, ny: 80)
.WithCylinder(centerX: 0.4, centerY: 0.4, radius: 0.05)
.WithInletVelocity(0.1) // Uโˆž = 0.1 m/s
.Solve(dt: 0.001, steps: 5000);

double[,] vx = result.Vx; // x-velocity field
double[,] vy = result.Vy; // y-velocity field
double[,] p = result.Pressure; // pressure field
double[,] w = result.Vorticity; // vorticity ฯ‰ = โˆ‚vy/โˆ‚x โˆ’ โˆ‚vx/โˆ‚y
bool[,] mask = result.CylinderMask; // obstacle cells
double cd = result.DragCoefficient; // โ‰ˆ 1.3โ€“1.5 at Re โ‰ˆ 100
double cl = result.LiftCoefficient; // oscillates for vortex shedding
double st = result.StrouhalNumber; // โ‰ˆ 0.2 at Re โ‰ˆ 100โ€“200

Algorithm

Each time step applies Chorin's fractional-step method:

  1. Predict โ€” advection (Advection2D, upwind) + diffusion (Laplacian2D)
  2. Pressure Poisson โ€” solve โˆ‡2p=โˆ‡โ‹…vโˆ—/ฮ”t\nabla^2 p = \nabla \cdot \mathbf{v}^* / \Delta t (SolvePoisson2D)
  3. Correct โ€” vn+1=vโˆ—โˆ’ฮ”tโ€‰โˆ‡p\mathbf{v}^{n+1} = \mathbf{v}^* - \Delta t \, \nabla p (Gradient2D)
  4. Enforce BCs โ€” inlet (Dirichlet), outlet (Neumann), walls (free-slip), cylinder (no-slip mask)

CFL-based adaptive time-step clamping ensures stability: ฮ”tโ‰ค0.4โ‹…minโก(ฮ”x/โˆฃuโˆฃmax,ฮ”x2/4ฮฝ)\Delta t \leq 0.4 \cdot \min(\Delta x / |u|_{max}, \Delta x^2 / 4\nu).

Boundary Conditions

BoundaryType
Inlet (left)Uniform velocity UโˆžU_\infty (Dirichlet)
Outlet (right)Zero-gradient โˆ‚v/โˆ‚x=0\partial v/\partial x = 0 (Neumann)
Top / BottomFree-slip (vy=0v_y = 0)
CylinderNo-slip (v=0\mathbf{v} = 0, immersed boundary mask)

Post-Processing

OutputDescription
Vorticityฯ‰=โˆ‚vy/โˆ‚xโˆ’โˆ‚vx/โˆ‚y\omega = \partial v_y/\partial x - \partial v_x/\partial y โ€” ideal for visualization
DragCoefficientPressure-based CdC_d integrated around cylinder boundary
LiftCoefficientPressure-based ClC_l โ€” oscillates during vortex shedding
StrouhalNumberSt=fD/UโˆžSt = fD/U_\infty estimated from lift time series zero-crossings
TimelineVorticity snapshots every 10 steps for animation

๐Ÿ’จ FluidFlow2D โ€” 2D Transient Navier-Stokesโ€‹

Solves incompressible Navier-Stokes on a rectangular domain using the Chorin projection method. Unlike CylinderFlow, this type operates on an open domain without an embedded obstacle โ€” ideal for lid-driven cavity, channel flow, or general 2D flow simulations.

โˆ‚vโˆ‚t+(vโ‹…โˆ‡)v=โˆ’1ฯโˆ‡p+ฮฝโˆ‡2v,โˆ‡โ‹…v=0\frac{\partial \mathbf{v}}{\partial t} + (\mathbf{v} \cdot \nabla)\mathbf{v} = -\frac{1}{\rho}\nabla p + \nu \nabla^2 \mathbf{v}, \quad \nabla \cdot \mathbf{v} = 0

var result = SimulationType.Create(MultiphysicsType.FluidFlow2D)
.WithMaterial(EngineeringLibrary.Water)
.WithGeometry(width: 0.1, height: 0.1, nx: 40, ny: 40)
.WithBoundary(top: 0.1, bottom: 0, left: 0, right: 0) // lid-driven cavity
.WithInletVelocity(0.01)
.Solve(dt: 0.001, steps: 500);

double[,] vx = result.Vx; // x-velocity field
double[,] vy = result.Vy; // y-velocity field
double[,] p = result.Pressure; // pressure field
double[,] mag = result.Field; // velocity magnitude |v|
List<double[,]> timeline = result.Timeline; // per-step snapshots

Boundary Conditions

BoundaryType
Inlet (left)Uniform velocity (Dirichlet)
Outlet (right)Zero-gradient (Neumann)
Top / BottomNo-slip (v=0\mathbf{v} = 0), or driven (set via WithBoundary)

Algorithm

Same Chorin fractional-step as CylinderFlow:

  1. Predict โ€” advection (upwind) + diffusion (Laplacian2D) โ†’ vโˆ—\mathbf{v}^*
  2. Pressure Poisson โ€” โˆ‡2p=(ฯ/ฮ”t)โˆ‡โ‹…vโˆ—\nabla^2 p = (\rho/\Delta t) \nabla \cdot \mathbf{v}^*
  3. Correct โ€” vn+1=vโˆ—โˆ’(ฮ”t/ฯ)โˆ‡p\mathbf{v}^{n+1} = \mathbf{v}^* - (\Delta t/\rho) \nabla p

CFL-limited adaptive time-stepping for stability.


๐Ÿงฒ MagneticField โ€” 2D Magnetostaticsโ€‹

Solves the magnetic vector potential Poisson equation with material permeability support:

โˆ‡2A=โˆ’ฮผ0ฮผrโ‹…J\nabla^2 A = -\mu_0 \mu_r \cdot J

where AA is the z-component of the vector potential and JJ is the current density. After solving, computes B=โˆ‡ร—A\mathbf{B} = \nabla \times A:

Bx=โˆ‚Aโˆ‚y,By=โˆ’โˆ‚Aโˆ‚xB_x = \frac{\partial A}{\partial y}, \quad B_y = -\frac{\partial A}{\partial x}

var result = SimulationType.Create(MultiphysicsType.MagneticField)
.WithMaterial(EngineeringLibrary.Steel) // ฮผ_r = 100 (ferromagnetic)
.WithGeometry(width: 0.2, height: 0.2, nx: 50, ny: 50)
.WithBoundary(top: 0, bottom: 0, left: 0, right: 0) // A = 0 at infinity
.AddSource(25, 25, 1e6) // J = 1 MA/mยฒ
.Solve(maxIterations: 10000, tolerance: 1e-7);

double[,] A = result.VectorPotential; // magnetic vector potential A(x,y)
double[,] bx = result.Bx; // B_x = โˆ‚A/โˆ‚y
double[,] by = result.By; // B_y = โˆ’โˆ‚A/โˆ‚x
double[,] bMag = result.Field; // |B| = โˆš(Bxยฒ + Byยฒ)

The solver uses the material's MagneticPermeability (ฮผr\mu_r) โ€” ferromagnetic materials like Steel (ฮผr=100\mu_r = 100) produce significantly stronger fields than non-magnetic materials.


๐Ÿ”ง PlaneStress โ€” 2D Plane Stress Elasticityโ€‹

Solves the 2D Navier-Cauchy equilibrium equations for plane stress using iterative SOR (Successive Over-Relaxation). Produces displacement and stress tensor fields.

c11โˆ‚2uxโˆ‚x2+c33โˆ‚2uxโˆ‚y2+(c12+c33)โˆ‚2uyโˆ‚xโˆ‚y+fx=0c_{11} \frac{\partial^2 u_x}{\partial x^2} + c_{33} \frac{\partial^2 u_x}{\partial y^2} + (c_{12}+c_{33}) \frac{\partial^2 u_y}{\partial x \partial y} + f_x = 0

with plane-stress constitutive law: c11=E/(1โˆ’ฮฝ2)c_{11} = E/(1-\nu^2), c12=ฮฝโ‹…c11c_{12} = \nu \cdot c_{11}, c33=G=E/(2(1+ฮฝ))c_{33} = G = E/(2(1+\nu)).

var result = SimulationType.Create(MultiphysicsType.PlaneStress)
.WithMaterial(EngineeringLibrary.Steel)
.WithGeometry(width: 1.0, height: 0.1, nx: 50, ny: 10)
.AddSource(40, 5, 1e6) // point force Fx at interior node
.WithSource(500) // uniform distributed load (Fy)
.Solve(maxIterations: 10000, tolerance: 1e-9);

double[,] ux = result.Ux; // x-displacement
double[,] uy = result.Uy; // y-displacement
double[,] sxx = result.StressXX; // normal stress ฯƒxx
double[,] syy = result.StressYY; // normal stress ฯƒyy
double[,] sxy = result.StressXY; // shear stress ฯ„xy
double[,] vonMises = result.Field; // von Mises stress field

Boundary Conditions

BoundaryType
LeftFixed (Dirichlet: ux=uy=0u_x = u_y = 0)
RightFree (Neumann)
BottomRoller (uy=0u_y = 0, free in xx)
TopFree (Neumann)

Output Fields

FieldDescription
Ux, UyDisplacement components (m)
StressXX, StressYYNormal stress (Pa)
StressXYShear stress (Pa)
FieldVon Mises equivalent stress ฯƒvM=ฯƒxx2โˆ’ฯƒxxฯƒyy+ฯƒyy2+3ฯ„xy2\sigma_{vM} = \sqrt{\sigma_{xx}^2 - \sigma_{xx}\sigma_{yy} + \sigma_{yy}^2 + 3\tau_{xy}^2}

๐ŸงŠ HeatBlock3D โ€” 3D Transient Heat Equationโ€‹

Forward Euler diffusion in a solid block with six-face Dirichlet boundary conditions:

var result = SimulationType.Create(MultiphysicsType.HeatBlock3D)
.WithMaterial(EngineeringLibrary.Steel)
.WithGeometry3D(width: 1.0, height: 1.0, depth: 0.5, nx: 20, ny: 20, nz: 10)
.WithBoundary3D(top: 100, bottom: 0, left: 0, right: 0, front: 0, back: 0)
.Solve(dt: 0.0001, steps: 5000);

double[,,] T = result.Field3D; // temperature at final step
double[,] sliceXY = result.SliceXY(5); // horizontal cross-section at iz=5

๐ŸŒซ๏ธ FluidDiffusion3D โ€” 3D Advection-Diffusionโ€‹

Scalar transport in a prescribed 3D velocity field:

var result = SimulationType.Create(MultiphysicsType.FluidDiffusion3D)
.WithGeometry3D(1.0, 1.0, 1.0, 20, 20, 20)
.WithDiffusionCoefficient(0.01)
.WithVelocityField3D((x, y, z) => (0.1, 0, 0))
.AddSource3D(10, 10, 10, 100.0)
.WithBoundary3D(0, 0, 0, 0, 0, 0)
.Solve(dt: 0.001, steps: 2000);

double[,,] c = result.Field3D;

๐Ÿ”ต CylinderFlow3D โ€” 3D Incompressible Navier-Stokesโ€‹

Extension of CylinderFlow with axial (z) variation. Uses Chorin projection on Grid3D:

var result = SimulationType.Create(MultiphysicsType.CylinderFlow3D)
.WithMaterial(EngineeringLibrary.Water)
.WithGeometry3D(2.0, 0.8, 0.4, 40, 16, 8)
.WithCylinder(0.4, 0.4, 0.05)
.WithInletVelocity(0.1)
.Solve(dt: 0.001, steps: 3000);

double[,,] vx = result.Vx3D;
double[,,] p = result.Pressure3D;
double cd = result.DragCoefficient;

๐ŸŽž๏ธ Snapshots & Timelineโ€‹

Time-stamped field data for animation and export:

using CSharpNumerics.Engines.Multiphysics.Snapshots;

// Build timeline from simulation result
var timeline = SimulationTimeline.FromResult(result, dt: 0.0001, dx: 0.005, dy: 0.005);

int frames = timeline.Count; // initial + N steps
double t0 = timeline.StartTime;
double tEnd = timeline.EndTime;
FieldSnapshot first = timeline[0]; // access by index

// Interpolate between frames (linear blend)
double[,] interp = timeline.InterpolateAt(0.00015);

// Individual snapshots
FieldSnapshot snap = timeline[5];
double val = snap[10, 15]; // indexing
double max = snap.Max();
double min = snap.Min();

// Beam snapshot
var beamSnap = BeamSnapshot.FromResult(result, BeamSupport.Cantilever, length: 2.0);
double maxDef = beamSnap.MaxDeflection;
double maxStress = beamSnap.MaxStress;

๐Ÿ’พ Binary Export (MPHY Format)โ€‹

Unity-compatible binary format with magic bytes, header, and float layers:

using CSharpNumerics.Engines.Multiphysics.Export;

// Save 2D timeline
MultiphysicsBinaryExporter.Save(timeline, "heat_sim.mphy");

// Save beam snapshot
MultiphysicsBinaryExporter.Save(beamSnap, "beam.mphy");

// Read back
var header = MultiphysicsBinaryExporter.ReadHeader("heat_sim.mphy");
// header.Type, header.Nx, header.Ny, header.TimeStepCount, header.LayerCount

var data = MultiphysicsBinaryExporter.Read("heat_sim.mphy");
float[] frame0 = data.Layers[0]; // first time step, flattened

๐ŸŒ JSON Exportโ€‹

Zero-dependency JSON for web visualization:

// Direct result export
string json = MultiphysicsJsonExporter.ToJson(result);
MultiphysicsJsonExporter.Save(result, "simulation.json");

// Timeline export (all frames)
string timelineJson = MultiphysicsJsonExporter.ToJson(timeline);

// Single snapshot
string snapJson = MultiphysicsJsonExporter.ToJson(snapshot);

// Beam snapshot with metadata
string beamJson = MultiphysicsJsonExporter.ToJson(beamSnap);

๐ŸŽฒ Monte Carlo Integrationโ€‹

Stochastic parameter variation for uncertainty quantification:

using CSharpNumerics.Engines.Multiphysics.MonteCarlo;

// Define parameter variation ranges
var variation = new ParameterVariation()
.ThermalConductivity(40, 60) // k โˆˆ [40, 60] W/(mยทK)
.BoundaryTemperature(90, 110) // T_top โˆˆ [90, 110] ยฐC
.SourceIntensity(8e5, 1.2e6); // S โˆˆ [8e5, 1.2e6] W/mยณ

// Build MC model from a baseline simulation
var baseline = SimulationType.Create(MultiphysicsType.HeatPlate)
.WithMaterial(EngineeringLibrary.Steel)
.WithGeometry(width: 0.1, height: 0.1, nx: 10, ny: 10)
.WithBoundary(top: 100.0, bottom: 0, left: 0, right: 0)
.WithInitialCondition(0.0);

var mc = new MultiphysicsMonteCarloModel(baseline, variation);

// Full batch: scenario matrix + per-iteration results
MultiphysicsScenarioResult mcResult = mc.RunBatch(iterations: 100, seed: 42);

Matrix scenarioMatrix = mcResult.ScenarioMatrix; // 100 ร— (NxยทNy)
int features = mcResult.FeatureCount; // NxยทNy

// Per-cell distribution across scenarios
double[] dist = mcResult.GetFeatureDistribution(featureIndex: 50);

// Percentile maps
double[] p95 = mcResult.ComputePercentile(95);
double[] p50 = mcResult.ComputePercentile(50);

Compatible with MonteCarloSimulator for scalar summary:

using CSharpNumerics.Statistics.MonteCarlo;

var sim = new MonteCarloSimulator(seed: 42);
MonteCarloResult stats = sim.Run(mc, iterations: 1000);
double meanPeak = stats.Mean;
double p95scalar = stats.Percentile(95);

๐Ÿง  Surrogate Modelโ€‹

Train a regression model on MC data for fast prediction without re-running the solver:

var surrogate = new SurrogateTrainer(mcResult, new Ridge());
surrogate.Train(r => r.MaxValue); // target = peak temperature

// Fast prediction without running the solver
VectorN predictions = surrogate.Predict(mcResult.ScenarioMatrix);
double single = surrogate.PredictOne(mcResult.GetScenarioVector(0));

๐Ÿงฉ Cluster Analysisโ€‹

Identify representative scenario groups from Monte Carlo ensembles:

using CSharpNumerics.ML.Clustering.Algorithms;
using CSharpNumerics.ML.Clustering.Evaluators;

var analysis = MultiphysicsClusterAnalyzer
.For(mcResult)
.WithAlgorithm(new KMeans { K = 3 })
.TryClusterCounts(2, 5)
.WithEvaluator(new SilhouetteEvaluator())
.Run();

int bestK = analysis.BestClusterCount;
int dominant = analysis.DominantCluster; // most frequent group
int[] iterations = analysis.GetClusterIterations(dominant);
double[] meanOutput = analysis.GetClusterMeanOutput(dominant);