Introduction
This tutorial demonstrates data‑driven turbulence modelling in a compressible steady Navier–Stokes problem using a neural network. The problem geometry is an open domain with a flow field parameterized by Mach number and other parameters. The network learns to map from reduced velocity coefficients (and parameters) to eddy‑viscosity coefficients, enabling a ROM with inexpensive predictions.
The solver class CompressibleSteadyNN (in 03compSteadyNS.C) follows the same design as NN‑02: offline snapshot generation, POD mode extraction, coefficient computation and projection, neural‑network training, and online evaluation.
The class CompressibleSteadyNN extends CompressibleSteadyNS by adding the neural-network interface.
{
public:
CompressibleSteadyNN(int argc, char *argv[])
Constructor.
Implementation of a parametrized full order steady NS problem and preparation of the the reduced ma...
It stores the numbers of projected velocity and nut modes, the normalization matrices used for scaling network inputs and outputs, and the loaded TorchScript model.
{
NUmodes = para->ITHACAdict->lookupOrDefault<label>("NmodesUproj", 10);
NNutModes = para->ITHACAdict->lookupOrDefault<label>("NmodesNutProj", 10);
};
label NUmodes;
label NNutModes;
Eigen::MatrixXd bias_inp;
Eigen::MatrixXd scale_inp;
Eigen::MatrixXd bias_out;
Eigen::MatrixXd scale_out;
Eigen::MatrixXd coeffL2Nut;
Eigen::MatrixXd coeffL2U;
torch::Tensor coeffL2U_tensor;
torch::Tensor coeffL2Nut_tensor;
torch::nn::Sequential Net;
torch::optim::Optimizer* optimizer;
torch::jit::script::Module netTorchscript;
Class for the definition of some general parameters, the parameters must be defined from the file ITH...
static ITHACAparameters * getInstance()
Gets an instance of ITHACAparameters, to be used if the instance is already existing.
The method loadNet reads the trained network and its normalization data from disk:
void loadNet(word filename)
{
std::string Msg = filename +
" is not existing, please run the training stage of the net with the correct number of modes for U and Nut";
netTorchscript = torch::jit::load(filename);
cnpy::load(bias_inp, "ITHACAoutput/NN/minAnglesInp_" + name(
NUmodes) + "_" + name(NNutModes) + ".npy");
cnpy::load(scale_inp, "ITHACAoutput/NN/scaleAnglesInp_" + name(
NUmodes) + "_" + name(NNutModes) + ".npy");
cnpy::load(bias_out, "ITHACAoutput/NN/minOut_" + name(NUmodes) + "_" + name(
NNutModes) + ".npy");
cnpy::load(scale_out, "ITHACAoutput/NN/scaleOut_" + name(NUmodes) + "_" + name(
NNutModes) + ".npy");
netTorchscript.eval();
}
bool check_file(std::string fileName)
Function that returns true if a file exists.
getTurbNN builds the reduced training and test datasets by projecting offline and validation snapshots onto the POD bases and exporting the resulting coefficients as NumPy arrays.
void getTurbNN()
{
{
mkDir("ITHACAoutput/NN/coeffs");
PtrList<volVectorField> UfieldTrain;
PtrList<volScalarField> PfieldTrain;
PtrList<volScalarField> nutFieldsTrain;
"./ITHACAoutput/Offline/");
"./ITHACAoutput/Offline/");
auto nut_train = _mesh().lookupObject<volScalarField>("nut");
"./ITHACAoutput/Offline/");
Info << "Computing the coefficients for U train" << endl;
Umodes,
0, true);
Info << "Computing the coefficients for p train" << endl;
Pmodes,
0, true);
Info << "Computing the coefficients for nuT train" << endl;
nutModes,
0, true);
coeffL2U_train.transposeInPlace();
coeffL2P_train.transposeInPlace();
coeffL2Nut_train.transposeInPlace();
cnpy::save(coeffL2U_train, "ITHACAoutput/NN/coeffs/coeffL2UTrain.npy");
cnpy::save(coeffL2P_train, "ITHACAoutput/NN/coeffs/coeffL2PTrain.npy");
cnpy::save(coeffL2Nut_train, "ITHACAoutput/NN/coeffs/coeffL2NutTrain.npy");
PtrList<volVectorField> UfieldTest;
PtrList<volScalarField> PfieldTest;
PtrList<volScalarField> nutFieldsTest;
"./ITHACAoutput/checkOff/");
"./ITHACAoutput/checkOff/");
auto nut_test = _mesh().lookupObject<volScalarField>("nut");
"./ITHACAoutput/checkOff/");
Umodes,
0, true);
Pmodes,
0, true);
nutModes,
0, true);
coeffL2U_test.transposeInPlace();
coeffL2P_test.transposeInPlace();
coeffL2Nut_test.transposeInPlace();
cnpy::save(coeffL2U_test, "ITHACAoutput/NN/coeffs/coeffL2UTest.npy");
cnpy::save(coeffL2P_test, "ITHACAoutput/NN/coeffs/coeffL2PTest.npy");
cnpy::save(coeffL2Nut_test, "ITHACAoutput/NN/coeffs/coeffL2NutTest.npy");
}
}
void readMiddleFields(PtrList< GeometricField< Type, PatchField, GeoMesh > > &Lfield, GeometricField< Type, PatchField, GeoMesh > &field, fileName casename)
Funtion to read a list of volVectorField from name of the field including all the intermediate snapsh...
Eigen::VectorXd getCoeffs(GeometricField< Type, PatchField, GeoMesh > &snapshot, PtrList< GeometricField< Type, PatchField, GeoMesh > > &modes, label Nmodes, bool consider_volumes)
Projects a snapshot on a basis function and gets the coefficients of the projection.
bool check_folder(word folder)
Checks if a folder exists.
The method evalNet assembles the parameter vector and reduced velocity coefficients into a single input, normalizes it, performs inference, and returns the predicted reduced nut coefficients.
Eigen::MatrixXd evalNet(Eigen::MatrixXd a, Eigen::MatrixXd mu_now)
{
Eigen::MatrixXd xpred(a.rows() + mu_now.rows(), 1);
if (xpred.cols() != 1)
{
throw std::runtime_error("Predictions for more than one sample not supported yet.");
}
xpred.bottomRows(a.rows()) = a;
xpred.topRows(mu_now.rows()) = mu_now;
xpred = xpred.array() * scale_inp.array() + bias_inp.array() ;
xpred.transposeInPlace();
torch::Tensor xTensor = eigenMatrix2torchTensor(xpred);
torch::Tensor out;
std::vector<torch::jit::IValue> XTensorInp;
XTensorInp.push_back(xTensor);
out = netTorchscript.forward(XTensorInp).toTensor();
Eigen::MatrixXd g = torchTensor2eigenMatrix<double>(out);
g.transposeInPlace();
g = (g.array() - bias_out.array()) / scale_out.array();
return g;
}
The class ReducedCompressibleSteadyNN extends ReducedCompressibleSteadyNS and implements the online reduced solver.
{
public:
:
{}
CompressibleSteadyNN * problem
Full problem.
ReducedCompressibleSteadyNN(CompressibleSteadyNN &FOMproblem)
Constructor.
Class where it is implemented a reduced problem for the steady Navier-stokes problem.
The method projectReducedOperators precomputes the reduced pressure-gradient contribution:
void projectReducedOperators(int NmodesUproj, int NmodesPproj, int NmodesEproj)
{
PtrList<volVectorField> gradModP;
for (label i = 0; i < NmodesPproj; i++)
{
gradModP.append(fvc::grad(problem->
Pmodes[i]));
}
NmodesUproj);
}
List< Eigen::MatrixXd > project(fvMatrix< Type > &Af, label numberOfModes=0, word projType="G")
A function that project an FvMatrix (OpenFoam linear System) on the modes.
volVectorModes Umodes
List of pointers used to form the velocity modes.
volScalarModes Pmodes
List of pointers used to form the pressure modes.
The main routine, solveOnlineCompressible, initializes the reduced coefficients for velocity, pressure, and energy from the current fields:
void solveOnlineCompressible(int NmodesUproj, int NmodesPproj, int NmodesEproj,
int NmodesNutProj, Eigen::MatrixXd mu_now,
word Folder = "./ITHACAoutput/Online/")
{
counter++;
scalar residualNorm(1);
scalar residualJump(1);
Eigen::MatrixXd uResidualOld = Eigen::MatrixXd::Zero(1, NmodesUproj);
Eigen::MatrixXd eResidualOld = Eigen::MatrixXd::Zero(1, NmodesEproj);
Eigen::MatrixXd pResidualOld = Eigen::MatrixXd::Zero(1, NmodesPproj);
Eigen::VectorXd uResidual(Eigen::Map<Eigen::VectorXd>(uResidualOld.data(),
NmodesUproj));
Eigen::VectorXd eResidual(Eigen::Map<Eigen::VectorXd>(eResidualOld.data(),
NmodesEproj));
Eigen::VectorXd pResidual(Eigen::Map<Eigen::VectorXd>(pResidualOld.data(),
NmodesPproj));
...
true);
true);
true);
volScalarModes Emodes
List of pointers used to form the energy modes.
It predicts the turbulent-viscosity coefficients with the neural network, reconstructs the nut field:
Eigen::MatrixXd nutCoeff = problem->evalNet(u, mu_now);
GeometricField< Type, PatchField, GeoMesh > reconstruct(GeometricField< Type, PatchField, GeoMesh > &inputField, Eigen::MatrixXd Coeff, word Name)
Function to reconstruct the solution starting from the coefficients, in this case the field is passed...
volScalarModes nutModes
List of POD modes for eddy viscosity.
It then enters a SIMPLE-like reduced iteration loop. In each iteration, the momentum, energy, and pressure equations are assembled in full-order form, projected onto the reduced bases, solved in reduced coordinates, and reconstructed back into the full fields:
while ((residualJump > residualJumpLim
|| residualNorm > normalizedResidualLim) && csolve < maxIter)
{
csolve++;
Info << "csolve:" << csolve << endl;
#if OFVER == 6
#else
#endif
uResidualOld = uResidual;
eResidualOld = eResidual;
pResidualOld = pResidual;
List<Eigen::MatrixXd> RedLinSysU;
nueff = nut + problem->turbulence->nu();
fvVectorMatrix UEqnR
(
fvm::div(phi, U)
- fvc::div((rho * nueff) * dev2(T(fvc::grad(U))))
- fvm::laplacian(rho * nueff, U)
==
fvOptions(rho, U)
);
UEqnR.relax();
fvOptions.constrain(UEqnR);
NmodesUproj);
Eigen::MatrixXd projGradP = projGradModP * p;
RedLinSysU[1] = RedLinSysU[1] - projGradP;
uResidual);
fvOptions.correct(U);
fvScalarMatrix EEqnR
(
fvm::div(phi, E)
+ fvc::div(phi, volScalarField("Ekp", 0.5 * magSqr(U) + P / rho))
- fvm::laplacian(problem->turbulence->alphaEff(), E)
==
fvOptions(rho, E)
);
EEqnR.relax();
fvOptions.constrain(EEqnR);
List<Eigen::MatrixXd> RedLinSysE = problem->
Emodes.
project(EEqnR, NmodesEproj);
fvOptions.correct(E);
thermo.correct();
constrainPressure(P, rho, U, problem->getPhiHbyA(UEqnR, U, P),
problem->getRhorAUf(
UEqnR));
surfaceScalarField phiHbyACalculated = problem->getPhiHbyA(UEqnR, U, P);
closedVolume = adjustPhi(phiHbyACalculated, U, P);
List<Eigen::MatrixXd> RedLinSysP;
while (problem->
_simple().correctNonOrthogonal())
{
volScalarField rAU(1.0 /
UEqnR.A());
volVectorField HbyA(constrainHbyA(rAU * UEqnR.H(), U,
P));
surfaceScalarField phiHbyA("phiHbyA", fvc::interpolate(rho)*fvc::flux(HbyA));
surfaceScalarField rhorAUf("rhorAUf", fvc::interpolate(rho * rAU));
fvScalarMatrix PEqnR
(
fvc::div(phiHbyA)
- fvm::laplacian(rhorAUf, P)
==
fvOptions(psi, P, rho.name())
);
PEqnR.setReference
(
problem->_pressureControl().refCell(),
problem->_pressureControl().refValue()
);
if (problem->
_simple().finalNonOrthogonalIter())
{
phi = problem->getPhiHbyA(UEqnR, U, P) + PEqnR.flux();
}
}
#include "incompressible/continuityErrs.H"
P.relax();
U = problem->HbyA() - (1.0 / UEqnR.A()) * problem->getGradP(P);
U.correctBoundaryConditions();
fvOptions.correct(U);
bool pLimited = problem->_pressureControl().limit(P);
if (closedVolume)
{
P += (problem->_initialMass() - fvc::domainIntegrate(psi * P))
/ fvc::domainIntegrate(psi);
}
if (pLimited || closedVolume)
{
P.correctBoundaryConditions();
}
rho = thermo.rho();
rho.relax();
static Eigen::MatrixXd solveLinearSys(List< Eigen::MatrixXd > LinSys, Eigen::MatrixXd x, Eigen::VectorXd &residual, const Eigen::MatrixXd &bc=Eigen::MatrixXd::Zero(0, 0), const std::string solverType="fullPivLu")
Linear system solver for the online problem.
autoPtr< simpleControl > _simple
simpleControl
void assignBC(GeometricField< scalar, fvPatchField, volMesh > &s, label BC_ind, double value)
Assign uniform Boundary Condition to a volScalarField.
Residuals are monitored through both normalized magnitude and residual jump, and the neural-network closure for nut is updated during the iterations. At convergence, the final fields are exported.
residualNorm = max(max((uResidual.cwiseAbs()).sum() /
(RedLinSysU[1].cwiseAbs()).sum(),
(pResidual.cwiseAbs()).sum() / (RedLinSysP[1].cwiseAbs()).sum()),
(eResidual.cwiseAbs()).sum() / (RedLinSysE[1].cwiseAbs()).sum());
residualJump = max(max(((uResidual - uResidualOld).cwiseAbs()).sum() /
(RedLinSysU[1].cwiseAbs()).sum(),
((pResidual - pResidualOld).cwiseAbs()).sum() /
(RedLinSysP[1].cwiseAbs()).sum()),
((eResidual - eResidualOld).cwiseAbs()).sum() /
(RedLinSysE[1].cwiseAbs()).sum());
nutCoeff = problem->evalNet(u, mu_now);
}
void exportSolution(GeometricField< Type, PatchField, GeoMesh > &s, fileName subfolder, fileName folder, word fieldName)
Export a field to file in a certain folder and subfolder.
The class tutorial03 provides the specific tutorial setup.
{
public:
:
{
}
bool middleExport
Export also intermediate fields.
IOdictionary * ITHACAdict
dictionary to store input output infos
tutorial03(int argc, char *argv[])
Constructor.
Its offlineSolve method either reads previously computed snapshots or performs the full-order offline simulations over the parameter samples, where the varying parameter is the viscosity.
void offlineSolve(word folder = "./ITHACAoutput/Offline/")
{
volVectorField& U = _U();
volScalarField& p = _p();
volScalarField& E = _E();
{
auto nut = _mesh().lookupObject<volScalarField>("nut");
}
else if (!offline)
{
double UIFinit = para->ITHACAdict->lookupOrDefault<double>("UIFinit", 250);
Vector<double> Uinl(UIFinit, 0, 0);
...
List< Eigen::MatrixXd > readMatrix(word folder, word mat_name)
Read a three dimensional matrix from a txt file in Eigen format.
For each sample, the code updates the viscosity, assigns the inlet condition, and runs the truth solve, storing velocity, pressure, energy, and turbulent-viscosity snapshots.
for (label i = 0; i < mu.rows(); i++)
{
Info << "Current mu = " << mu(i, 0) << endl;
changeViscosity(mu(i, 0));
assignIF(_U(), Uinl);
truthSolve(folder);
}
The main
In main, the tutorial object is created and the offline and online parameter sets are either loaded from file or generated automatically.
int main(int argc, char* argv[])
{
std::cerr << "debug point 1" << endl;
std::ifstream exFileOff("./parsOff_mat.txt");
if (exFileOff)
{
}
else
{
label OffNum = para->ITHACAdict->lookupOrDefault<label>("OffNum", 25);
example.mu = Eigen::VectorXd::LinSpaced(OffNum, 1.00e-05, 1.00e-02);
}
Eigen::MatrixXd parsOn;
std::ifstream exFileOn("./parsOn_mat.txt");
if (exFileOn)
{
}
else
{
label OnNum = para->ITHACAdict->lookupOrDefault<label>("OnNum", 20);
}
void exportMatrix(Eigen::Matrix< T, -1, dim > &matrix, word Name, word type, word folder)
Export the reduced matrices in numpy (type=python), matlab (type=matlab) and txt (type=eigen) format ...
Eigen::MatrixXd rand(label rows, label cols, double min, double max)
Generates random matrix with random values in an interval.
The code then reads the numbers of POD and projection modes from the dictionary, performs the offline stage:
int NmodesUout = para->ITHACAdict->lookupOrDefault<int>("NmodesUout", 15);
int NmodesPout = para->ITHACAdict->lookupOrDefault<int>("NmodesPout", 15);
int NmodesEout = para->ITHACAdict->lookupOrDefault<int>("NmodesEout", 15);
int NmodesNutOut = para->ITHACAdict->lookupOrDefault<int>("NmodesNutOut", 15);
int NmodesUproj = para->ITHACAdict->lookupOrDefault<int>("NmodesUproj", 10);
int NmodesPproj = para->ITHACAdict->lookupOrDefault<int>("NmodesPproj", 10);
int NmodesEproj = para->ITHACAdict->lookupOrDefault<int>("NmodesEproj", 10);
int NmodesNutProj = para->ITHACAdict->lookupOrDefault<int>("NmodesNutProj", 10);
example.offlineSolve();
It then computes POD modes for velocity, pressure, energy, and nut:
example.podex, 0, 0,
NmodesUout);
example.podex, 0, 0,
NmodesPout);
example.podex, 0, 0,
NmodesEout);
0, 0,
NmodesNutOut);
void getModes(PtrList< GeometricField< Type, PatchField, GeoMesh > > &snapshots, PtrList< GeometricField< Type, PatchField, GeoMesh > > &modes, word fieldName, bool podex, bool supex, bool sup, label nmodes, bool correctBC)
Computes the bases or reads them for a field.
If the validation folder checkOff is missing, a second full-order run is performed on the online parameter set to generate reference solutions.
{
checkOff.offline = false;
checkOff.restart();
checkOff.offlineSolve("./ITHACAoutput/checkOff/");
checkOff.offline = true;
}
Otherwise, the stored validation snapshots are reorganized into a one-sample-per-case structure for direct comparison.
else
{
PtrList<volVectorField> UfieldCheck;
PtrList<volScalarField> PfieldCheck;
PtrList<volScalarField> EfieldCheck;
PtrList<volScalarField> nutFieldsCheck;
"./ITHACAoutput/checkOff/");
"./ITHACAoutput/checkOff/");
"./ITHACAoutput/checkOff/");
auto nutCheck = example._mesh().lookupObject<volScalarField>("nut");
"./ITHACAoutput/checkOff/");
Eigen::MatrixXd snapsCheck =
label fieldNum = 0;
for (label k = 0; k < snapsCheck.rows(); k++)
{
fieldNum = fieldNum + snapsCheck(k, 0);
"./ITHACAoutput/checkOffSingle/");
"./ITHACAoutput/checkOffSingle/");
"./ITHACAoutput/checkOffSingle/");
"./ITHACAoutput/checkOffSingle/");
}
}
The reduced datasets for neural-network training are then generated, the trained TorchScript model is loaded, and the reduced solver is created.
example.getTurbNN();
example.loadNet("ITHACAoutput/NN/Net_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + ".pt");
If the online results are not yet available, the code loops over all online parameters, updates the viscosity, projects the reduced operators, resets the solver state, validates the turbulence model, and performs the online reduced solve while storing CPU times.
NmodesUproj) + "_" + name(NmodesNutProj) + "/" ) )
{
std::ofstream cpuTimes;
word OnlineFolder = "./ITHACAoutput/Online_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/";
cpuTimes.open(OnlineFolder + "/cpuTimes", std::ios_base::app);
for (label k = 0; k < parsOn.rows(); k++)
{
std::clock_t startOn;
startOn = std::clock();
double durationOn;
Eigen::MatrixXd mu_now = parsOn.row(k);
mu_now.transposeInPlace();
example.changeViscosity(mu_now(0, 0));
reduced.projectReducedOperators(NmodesUproj, NmodesPproj, NmodesEproj);
example.restart();
example.turbulence->validate();
reduced.solveOnlineCompressible(NmodesUproj, NmodesPproj, NmodesEproj,
NmodesNutProj, mu_now, "./ITHACAoutput/Online_" + name(NmodesUproj) + "_" +
name(NmodesNutProj) + "/");
durationOn = std::clock() - startOn;
cpuTimes << durationOn << endl;
}
}
If the online folder already exists, the code reads both reduced and full-order fields, computes normalized error fields for velocity, pressure, energy, and turbulent viscosity, exports them, and also computes relative L2 error matrices for all variables.
NmodesUproj) + "_" + name(NmodesNutProj) + "/" ) )
{
PtrList<volVectorField> offlineU, onlineU;
PtrList<volScalarField> offlineP, onlineP;
PtrList<volScalarField> offlineE, onlineE;
PtrList<volScalarField> offlineNut, onlineNut;
"./ITHACAoutput/Online_" + name(NmodesUproj) + "_" + name(NmodesNutProj) + "/");
"./ITHACAoutput/Online_" + name(NmodesUproj) + "_" + name(NmodesNutProj) + "/");
"./ITHACAoutput/Online_" + name(NmodesUproj) + "_" + name(NmodesNutProj) + "/");
auto nut = example._mesh().lookupObject<volScalarField>("nut");
"./ITHACAoutput/Online_" + name(NmodesUproj) + "_" + name(NmodesNutProj) + "/");
"./ITHACAoutput/checkOffSingle/");
"./ITHACAoutput/checkOffSingle/");
"./ITHACAoutput/checkOffSingle/");
for (label j = 0; j < parsOn.rows(); j++)
{
volVectorField Ue = offlineU[j] - onlineU[j];
Ue /= u;
volScalarField Pe = offlineP[j] - onlineP[j];
Pe /= p;
volScalarField Ee = offlineE[j] - onlineE[j];
Ee /= e;
volScalarField Nute = offlineNut[j] - onlineNut[j];
Nute /= n;
Ue.rename("Ue");
Pe.rename("Pe");
Ee.rename("Ee");
Nute.rename("Nute");
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
}
"errorU" + name(NmodesUproj) + "_" + name(NmodesNutProj), "python",
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"errorP" + name(NmodesUproj) + "_" + name(NmodesNutProj), "python",
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"errorE" + name(NmodesUproj) + "_" + name(NmodesNutProj), "python",
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
"errorNut" + name(NmodesUproj) + "_" + name(NmodesNutProj), "python",
"./ITHACAoutput/ErrorFields_" + name(NmodesUproj) + "_" + name(
NmodesNutProj) + "/");
}
void read_fields(PtrList< GeometricField< Type, PatchField, GeoMesh > > &Lfield, word Name, fileName casename, int first_snap, int n_snap)
Function to read a list of fields from the name of the field and casename.
double errorL2Rel(GeometricField< T, fvPatchField, volMesh > &field1, GeometricField< T, fvPatchField, volMesh > &field2, List< label > *labels)
Computes the relative error between two geometric Fields in L2 norm.
double L2Norm(const T v)
Compute the L2 norm of v.
Other files
- 03compSteadyNS.C – main source with CompressibleSteadyNN definition.
- compSteady.py – Python helpers for feature engineering and data loading.
- train.py – standalone training script (PyTorch).
- parsOff_mat.txt, parsOn_mat.txt – sample parameter matrices.
- Make/ – build directory; output binary: 03compSteadyNS.
The plain code
The plain code is available here.