In Computational Fluid Dynamics, a case setup file is not a simple configuration artifact. It represents engineering intent: physical modeling assumptions, boundary definitions, solver coupling choices, numerical parameters, and simplifications that are often the result of long design iterations. These case files usually live much longer than the solver version that created them.

Solver internals, on the other hand, evolve continuously. Data structures change, models are extended, algorithms are replaced, and performance-driven refactors happen regularly. This creates a structural mismatch: the solver code changes fast, but case setup data must remain usable.

In practice, this means that strict serialization or schema-driven configuration formats fail very quickly. A single renamed option, a missing field, or a deprecated enum value can prevent an entire case from loading, even though most of the data is still meaningful and correct from an engineering point of view.

To address this problem, a fault-tolerant case setup I/O layer was added as the latest architectural component in the CMPS solver environment. This component is not a generic configuration or serialization library. It is developed explicitly for Computational Fluid Dynamics case setup data and intentionally reflects CFD-specific constraints and usage patterns.

The main objective is very clear: a case file should load as completely as possible, even if parts of it are outdated, incomplete, or invalid.

Why This Is Not a Generic Library

General-purpose serialization libraries aim to be reusable across domains. They assume stable schemas, strict type consistency, or explicit version migration. These assumptions do not hold in Computational Fluid Dynamics.

CFD case files often:

  • come from older solver versions,
  • are manually edited,
  • contain deprecated options,
  • miss fields that did not exist before,
  • mix solver intent with partially obsolete data.

 

Rejecting the entire case because of one invalid entry is not acceptable in real CFD workflows. Engineers need the solver to recover as much information as possible and clearly report what could not be used.

For this reason, the I/O layer is domain-specific by design. It encodes CFD semantics directly into the loading logic instead of relying on external schemas.

Core Design Philosophy

The case file is treated as engineering input, not as a strict contract. Every field is optional. Every failure is local. No global rollback is performed.

Each setup object:

  • owns its default state,
  • validates its own data,
  • never assumes that all input is correct.

 

Missing keys do not overwrite defaults. Invalid values generate warnings but do not abort loading. Indexed data is validated element by element. Structural and computational data is never reconstructed from the case file.

Object-Level Loading with Defaults

Each setup class implements its own load() function and defines safe defaults directly in code. The case file only overrides what it actually contains.

void CPointProbe::load(const boost::property_tree::ptree& pt)
{
    const char* ctx = "CPointProbe";

    CIOJson::Read(pt, "fileName", fileName, ctx);
    CIOJson::Read(pt, "phase", phase, ctx);
    CIOJson::Read(pt, "numOfIteration", numOfIteration, ctx);
    CIOJson::Read(pt, "isActive", isActive, ctx);
} 

If any key is missing, the existing value remains unchanged. If conversion fails, the error is logged and loading continues. The object always remains in a valid state.

This approach allows new parameters to be added in future solver versions without breaking older case files.

Structured Data and Fixed-Size Arrays

CFD setups often contain fixed-size vectors such as positions, directions, or reference values. These are handled without assuming full correctness.

std::array<double,3> probePosition = {0.0, 0.0, 0.0};

CIOJson::Read(
    pt,
    "probePosition",
    probePosition,
    "CPointProbe"
); 

If only one or two components exist in the case file, the remaining values keep their defaults. No exception is thrown and no partial overwrite occurs.

Indexed Containers with Fault Isolation

Many CFD entities are indexed collections: probes, monitors, phases, model coefficients, or section-based settings. These are read element by element with full fault isolation.

std::vector<CPointProbe> probes;

CIOJson::Read(
    pt,
    "PointProbes",
    probes,
    "SolutionMonitor"
); 

Internally, each index is parsed and validated independently. Non-numeric indices are skipped. Containers are resized only when needed. A malformed entry does not invalidate other entries.

The following simplified internal logic illustrates this behavior:

template<typename ObjT>
inline void ReadOptVectorChild(
    const boost::property_tree::ptree& pt,
    const char* key,
    std::vector<ObjT>& v,
    const char* ctx
) noexcept
{
    const auto cOpt = pt.get_child_optional(key);
    if (!cOpt) {
        return;
    }

    for (const auto& kv : *cOpt) {
        size_t idx = 0;
        if (!TryParseIndex_(kv.first, idx)) {
            WarnKeyReadFailed(ctx, key, "non-numeric index");
            continue;
        }

        if (v.size() <= idx) {
            v.resize(idx + 1);
        }

        ReadOptChild(*cOpt, kv.first.c_str(), v[idx], ctx);
    }
} 

This design explicitly avoids global rollback and allows partial recovery, which is critical for long-lived Computational Fluid Dynamics cases.

Hierarchical Setup and Nested Objects

CFD configuration is deeply hierarchical. Solver options include linear solvers, turbulence models include sub-models, and boundary conditions include layered definitions. Each level loads independently.

void CSolverOptions::load(const boost::property_tree::ptree& pt)
{
    const char* ctx = "CSolverOptions";

    CIOJson::Read(pt, "timeIntegration", timeIntegration, ctx);
    CIOJson::Read(pt, "linearSolver", linearSolverOptions, ctx);
    CIOJson::Read(pt, "turbulence", turbulenceOptions, ctx);
} 

If the turbulence block is invalid or missing, time integration and linear solver settings still load correctly. There is no cascading failure across unrelated components.

No Pointer or Topology Serialization

A strict rule is enforced: raw pointers are never serialized. This is blocked at compile time, not at runtime.

Instead of storing pointers, the case file stores stable identifiers such as indices or names:

size_t cellIndex = invalidIndex;
CIOJson::Read(pt, "cellIndex", cellIndex, ctx);

// resolved after load
cellPtr = &grid.Cells[cellIndex]; 

This prevents dangling references, alignment issues, and memory corruption, and completely decouples case files from internal memory layout.

Separation from Computational Data

The case setup format is intentionally limited to setup data only. It never stores mesh connectivity, solver matrices, caches, or solution vectors.

After loading is complete, all runtime data is rebuilt explicitly:

grid.BuildConnectivity();
grid.CalculateWallDistances();
solver.Initialize(); 

This guarantees consistency even when internal algorithms or data layouts change between solver versions.

Forward and Backward Compatibility by Construction

There is no strict version gate. Older case files load with defaults applied. New solver versions can add fields freely. Deprecated options can be ignored without breaking the rest of the case.

This behavior is essential for long-term Computational Fluid Dynamics projects where archived cases must remain usable years later.

Closing Remarks

This fault-tolerant case setup I/O layer is a domain-specific infrastructure component, designed explicitly for Computational Fluid Dynamics. It does not try to be generic or reusable outside this domain. Its purpose is robustness, longevity, and engineering continuity.

As the final architectural addition to the solver infrastructure, it completes the foundation required for CFD case files that remain usable across solver evolution, without fragile migration logic or hard failures.