Comprehensive guide to BELFEM's file input/output system.
Date: 2026-01-16 Purpose: Usage documentation for src/io module Revision History:
- 2026-01-16: Initial documentation
- 2026-01-16: Critical corrections (FileMode::NEW behavior, resource management, parallel I/O contracts)
Table of Contents
- Overview
- Common Pitfalls
- Core Classes
- File Utilities
- Common Patterns
- Performance Tips
- MPI Considerations
- Thread Safety
- Debugging Tips
- Enumeration Reference
Overview
BELFEM's I/O module provides unified interfaces for reading and writing data in multiple formats:
- HDF5: Hierarchical data format for large-scale scientific data (requires BELFEM_HDF5)
- ASCII: Line-based text file interface with buffering
- CSV: Comma/delimiter-separated value files (numeric data → Matrix<real>)
- InputFile: Hierarchical configuration file parser (key-value pairs, nested sections)
- XML: XML document interface (requires BELFEM_XML)
Build Configuration
# Enable HDF5 support (default ON; highly recommended for large datasets)
cmake -DUSE_HDF5=ON ..
# Enable XML support via tinyxml2 (default OFF)
cmake -DUSE_TINYXML2=ON ..
(BELFEM_HDF5 is the compile definition those options generate, not a CMake cache entry — setting it on the command line does nothing.)
Design Philosophy
Following BELFEM's coding philosophy:
- Manual resource management: Files use RAII (constructor opens, destructor closes)
- Explicit control: FileMode explicitly controls read/write intent
- Performance-first: Buffering in Ascii, direct memory copy in HDF5
- Zero-overhead abstractions: Template functions for type-safe I/O
- C-style error handling: a failed check ends the run — abort in release, throw in debug (see Error Handling Philosophy)
Error Handling Philosophy
IMPORTANT: BELFEM uses C libraries (HDF5, BLAS, etc.) with abort-on-error semantics, NOT C++ exceptions.
T* tData = (T*) malloc(tLength * sizeof(T));
std::memcpy(tData, aVector.data(), ...);
aStatus = H5Dwrite(..., tData);
free(tData);
#define BELFEM_ASSERT(aCheck,...)
Definition assert.hpp:244
Why this matters:
- HDF5 functions return error codes (herr_t), they never throw exceptions
- malloc/free are C functions (don't throw in C++)
- BELFEM_ERROR and BELFEM_ASSERT end the run — but how depends on the build: a debug build throws, a release build calls error_abort() (assert.hpp:88-93,189-192)
- So the "no unwinding" simplification holds for release builds only. In a debug build the throw does unwind, and a raw malloc between the allocation and the check is leaked
Resource cleanup guarantees:
- Normal path: Destructors run when objects go out of scope (RAII)
- Error path: Explicit cleanup (e.g., free()) runs before BELFEM_ASSERT aborts
- Abort/crash: Process terminates → OS reclaims all memory, file descriptors, handles
Implication: in a release build, traditional C++ "exception safety" concerns (RAII wrappers, unique_ptr) do not drive the design of these error paths — the process ends after the explicit cleanup rather than unwinding through it. Do not read that as "BELFEM never throws": a debug run does, deliberately and at every rank, so that a failure can be caught under a debugger. The pattern above stays correct in both builds because the free() precedes the check.
Note for contributors: If you add C++ library code that throws exceptions (e.g., std::vector::at()), wrap it in try/catch and convert to BELFEM_ERROR. Do NOT let exceptions propagate through C library boundaries.
Common Pitfalls
0. Modern C++ Tool Warnings (False Positives for BELFEM)
IMPORTANT: Static analyzers and modern C++ code reviewers may flag BELFEM code with warnings like:
- "Use `std::unique_ptr` instead of raw pointers"
- "Potential memory leak: `malloc` without exception-safe cleanup"
- "Use RAII wrapper for file handles"
- "Exception-unsafe resource management"
These warnings DO NOT apply to BELFEM because:
- BELFEM uses C libraries (HDF5, BLAS, etc.) that return error codes, not exceptions
- Error handling ends the run (BELFEM_ERROR/BELFEM_ASSERT: abort in release, throw in debug)
- In release there is no unwinding, so cleanup placed before the check always runs; in debug the throw unwinds, which is why the free() must precede the check
- Manual resource management is intentional for zero-overhead performance in HPC
Example of "false positive":
T* tData = (T*) malloc(tLength * sizeof(T));
aStatus = H5Dwrite(..., tData);
free(tData);
This is safe in BELFEM because:
- H5Dwrite() is a C function (returns error code, never throws)
- free() executes before the assertion
- If assertion fails, process aborts (OS reclaims all resources)
When to ignore modern C++ warnings:
- ✅ malloc/free with C library calls (HDF5, BLAS, etc.)
- ✅ Raw pointers owned by RAII classes (deleted in destructor)
- ✅ Manual cleanup before BELFEM_ERROR/BELFEM_ASSERT
When warnings ARE valid:
- ❌ Calling C++ library functions that throw (e.g., std::vector::at()) between resource allocation and cleanup
- ❌ Resources not cleaned up in EITHER destructor OR before error abort
- ❌ Mixing exception-throwing C++ code with manual memory management
See "Error Handling Philosophy" section above for detailed explanation of BELFEM's C-style error handling model.
1. Forgetting to Enable HDF5/XML at Build Time
HDF5 file("data.h5", FileMode::NEW);
#ifdef BELFEM_HDF5
HDF5 file("data.h5", FileMode::NEW);
#else
BELFEM_ERROR(
false,
"BELFEM_HDF5 not enabled. Reconfigure with -DUSE_HDF5=ON");
#endif
#define BELFEM_ERROR(aCheck,...)
Definition assert.hpp:264
2. Using Wrong FileMode (CRITICAL - Data Loss Hazard!)
HDF5 file("valuable_results.h5", FileMode::NEW);
if (file_exists("results.h5")) {
BELFEM_ERROR(
false,
"File already exists. Use OPEN_RDWR or choose new name.");
}
HDF5 file("results.h5", FileMode::NEW);
HDF5 file("existing_results.h5", FileMode::OPEN_RDWR);
string path = "checkpoint_" + std::to_string(timestep) + ".h5";
HDF5 file(path, FileMode::NEW);
FileMode options (cl_HDF5.cpp:44-97):
- NEW: Create new file (TRUNCATES if exists — H5F_ACC_TRUNC — use with caution!)
- OPEN_RDONLY: Read-only access (H5F_ACC_RDONLY, fails if doesn't exist)
- OPEN_RDONLY_PARALLEL: not handled by HDF5 — the constructor has no case for it and falls through to "unknown filemode passed". Ascii implements it (rank 0 reads, then broadcasts)
- OPEN_RDWR: Read-write access (H5F_ACC_RDWR, fails if doesn't exist)
Warning: FileMode::NEW will destroy existing results if used with automatically generated paths. For restarts, always use OPEN_RDWR. For checkpoints, use explicit versioning.
3. HDF5 Group Management Errors (Resource Leaks!)
HDF5 file("data.h5", FileMode::NEW);
file.create_group("Results");
file.save_data("GlobalMetadata", metadata);
HDF5 file("data.h5", FileMode::NEW);
file.create_group("Results");
file.save_data("field", vector);
file.close_active_group();
file.save_data("GlobalMetadata", metadata);
{
HDF5 file("data.h5", FileMode::NEW);
file.create_group("Results");
file.save_data("Temperature", T);
file.save_data("Pressure", P);
file.close_active_group();
}
HDF5 Group Navigation Invariant (cl_HDF5.cpp:78-239):
Critical Rule: HDF5 maintains exactly one active group at any time. All save_data() and load_data() calls operate relative to the currently active group.
- create_group(name) opens the group (calls H5Gcreate2) and makes it active
- select_group(name) opens an existing group (H5Gopen2) and makes it active
- close_active_group() closes the current group (H5Gclose) and returns to parent
- Destructor automatically calls close() → close_active_group() (RAII cleanup)
Resource Management:
- Each opened group allocates an HDF5 handle (hid_t)
- Handles are not automatically closed when opening a new group
- Failure to close groups before program exit → handle leaks (finite limit!)
- The destructor calls close(), which closes only the active group before H5Fclose; close_tree() exists but is not called automatically. Close every group you opened, or call close_tree() yourself before the object dies.
Recommendation: Always match create_group() / select_group() with close_active_group(). Rely on destructor for final cleanup, but explicitly close groups to maintain clarity.
4. InputFile Section Hierarchy Confusion
const input::Section* sec = input_file.section("subsection");
const input::Section* parent = input_file.section("Section");
const input::Section* child = parent->section("subsection");
real value = child->get_real("key");
5. CsvFile Assumes Numeric Data
CsvFile csv("data.csv");
Ascii file("data.csv", FileMode::OPEN_RDONLY);
for (index_t i = 0; i < file.length(); ++i) {
string line = file.line(i);
}
Core Classes
HDF5 Class
Location: cl_HDF5.hpp
Hierarchical data format interface for saving/loading scalars, vectors, matrices, and structured data.
Constructor
HDF5(const string & aPath,
const enum FileMode aMode,
const bool aParallelMode = false);
Parameters:
- aPath: File path (.h5 or .hdf5 extension recommended)
- aMode: File access mode (NEW, OPEN_RDONLY, OPEN_RDWR; OPEN_RDONLY_PARALLEL is not handled by HDF5 — see the FileMode table)
- aParallelMode: write one file per rank — the path is rewritten through make_path_parallel(). This is not PHDF5 and nothing about it is collective
Group Operations
hid_t create_group(const string & aLabel);
hid_t select_group(const string & aLabel);
void close_active_group();
Example:
HDF5 file("simulation.h5", FileMode::NEW);
file.create_group("TimeStep_001");
file.create_group("Fields");
file.save_data("Temperature", temperature_field);
file.close_active_group();
file.close_active_group();
Save Operations
void save_data(const string & aLabel, const string & aValue);
void save_data(const string & aLabel, const sint & aValue);
void save_data(const string & aLabel, const uint & aValue);
void save_data(const string & aLabel, const luint & aValue);
void save_data(const string & aLabel, const lluint & aValue);
void save_data(const string & aLabel, const real & aValue);
void save_data(const string & aLabel, const bool & aValue);
void save_data(const string & aLabel, const Vector<sint> & aVector);
void save_data(const string & aLabel, const Vector<uint> & aVector);
void save_data(const string & aLabel, const Vector<luint> & aVector);
void save_data(const string & aLabel, const Vector<real> & aVector);
void save_data(const string & aLabel, const Matrix<sint> & aMatrix);
void save_data(const string & aLabel, const Matrix<uint> & aMatrix);
void save_data(const string & aLabel, const Matrix<luint> & aMatrix);
void save_data(const string & aLabel, const Matrix<real> & aMatrix);
void save_data(const string & aLabel, const Cell<string> & aStrings);
Load Operations
void load_data(const string & aLabel, string & aValue);
void load_data(const string & aLabel, sint & aValue);
void load_data(const string & aLabel, uint & aValue);
void load_data(const string & aLabel, luint & aValue);
void load_data(const string & aLabel, lluint & aValue);
void load_data(const string & aLabel, real & aValue);
void load_data(const string & aLabel, bool & aValue);
void load_data(const string & aLabel, Vector<sint> & aVector);
void load_data(const string & aLabel, Vector<uint> & aVector);
void load_data(const string & aLabel, Vector<luint> & aVector);
void load_data(const string & aLabel, Vector<real> & aVector);
void load_data(const string & aLabel, Matrix<sint> & aMatrix);
void load_data(const string & aLabel, Matrix<uint> & aMatrix);
void load_data(const string & aLabel, Matrix<luint> & aMatrix);
void load_data(const string & aLabel, Matrix<real> & aMatrix);
void load_data(const string & aLabel, Cell<string> & aStrings);
Best Practices
{
HDF5 file("results.h5", FileMode::NEW);
file.create_group("Mesh");
file.save_data("Nodes", node_coords);
file.save_data("Elements", element_connectivity);
file.close_active_group();
file.create_group("Solution");
file.save_data("Temperature", T);
file.save_data("Pressure", P);
file.close_active_group();
}
Ascii Class
Location: cl_Ascii.hpp
Line-based ASCII file interface with an in-memory line buffer; changes must be written with save() explicitly.
Constructor
Ascii(const string & aPath, const enum FileMode & aMode);
Modes supported:
- NEW: empty buffer, save() writes the file
- OPEN_RDONLY: read existing file into the buffer
- OPEN_RDONLY_PARALLEL: rank 0 reads, the buffer is broadcast
- OPEN_RDWR: not supported by Ascii (constructor error)
Interface
const string & line(index_t aLineNumber) const;
string & line(index_t aLineNumber);
index_t length() const;
void print(const string & aLine);
bool save();
Usage Patterns
Read text file:
Ascii file("input.txt", FileMode::OPEN_RDONLY);
for (index_t i = 0; i < file.length(); ++i) {
const string & line = file.line(i);
if (line.find("PARAMETER") != string::npos) {
}
}
Write text file (cl_Ascii.cpp:135-140):
Ascii output("results.txt", FileMode::NEW);
output.print("# Simulation results");
output.print("Time, Temperature, Pressure");
for (index_t i = 0; i < n; ++i) {
output.print(std::to_string(time(i)) + ", " +
std::to_string(temp(i)) + ", " +
std::to_string(pres(i)));
}
output.save();
Memory considerations:
- Entire file loaded into memory (Cell<string> buffer)
- Efficient for moderate-sized text files (< 10 MB)
- For large files (> 100 MB), consider streaming with standard library
- Destructor: raises a BELFEM_ERROR (always active) if the buffer changed but was never saved (mChangedSinceLastSave)
CsvFile Class
Location: cl_CsvFile.hpp
CSV reader that loads numeric data into Matrix<real>. Extends Ascii.
Constructor
CsvFile(const string & aPath, const char aDelimiter = ',');
Parameters:
- aPath: Path to CSV file
- aDelimiter: Column separator (default: ,)
Interface
const Matrix<real> & data() const;
Matrix<real> & data();
Usage
CsvFile csv("data.csv");
const Matrix<real> & data = csv.data();
real value = data(0, 2);
CsvFile tsv("data.tsv", '\t');
Assumptions:
- All data is numeric (real type)
- Rectangular data (all rows same length)
- Non-numeric content will cause parsing errors
InputFile and Input_Section Classes
Location: cl_InputFile.hpp, cl_Input_Section.hpp
Hierarchical configuration file parser with key-value pairs and nested sections.
File Format
// comments start with a double slash
// simple key-value pairs: key : value ;
title : my simulation ;
timesteps : 100 ;
dt : 0.001 s ;
// nested sections: the header is the line before its own-line brace
solver
{
type : mumps ;
tolerance : 1e-6 ;
max iterations : 1000 ;
// sub-sections
preconditioner
{
type : ilu ;
fill level : 2 ;
}
}
// named sections ( type : label )
material : steel
{
density : 7850 kg/m^3 ;
youngs modulus : 200e9 Pa ;
}
material : aluminum
{
density : 2700 kg/m^3 ;
youngs modulus : 69e9 Pa ;
}
Every key line ends with ;; a line without one is not a key. Keys, section types and labels are folded to lower case by the parser.
InputFile Class
InputFile(const string & aPath);
const input::Section * section(const string & aSection) const;
const input::Section * section(const index_t aIndex) const;
bool section_exists(const string & aSection) const;
index_t num_sections() const;
void print();
Input_Section Class
const string & type() const;
const string & label() const;
const string & key() const;
const Section * section(const string & aType) const;
const Section * section(const string & aType, const string & aLabel) const;
const Section * section(const index_t aIndex) const;
bool section_exists(const string & aType) const;
bool section_exists(const string & aType, const string & aLabel) const;
index_t num_sections() const;
bool key_exists(const string & aKey) const;
bool key_is_real(const string & aKey) const;
const string & get_string(const string & aKey) const;
bool get_bool(const string & aKey) const;
real get_real(const string & aKey) const;
value get_value(const string & aKey, const string & aUnit) const;
int get_int(const string & aKey) const;
string get_units(const string & aKey) const;
void get_ids(const string & aKey, Vector<id_t> & aIDs) const;
void get_reals(const string & aKey, Vector<real> & aReals) const;
void get_id_groups(const string & aKey, Cell<Cell<id_t>> & aIDs) const;
index_t num_keys() const;
const string & key(const index_t aIndex) const;
int level() const;
const Section * parent() const;
string tree() const;
Usage Example
InputFile config("simulation.input");
if (config.section_exists("Solver")) {
const input::Section* solver = config.section("Solver");
string solver_type = solver->get_string("type");
real tol = solver->get_real("tolerance");
int max_iter = solver->get_int("max_iterations");
if (solver->section_exists("Preconditioner")) {
const input::Section* precond = solver->section("Preconditioner");
string precond_type = precond->get_string("type");
}
}
if (config.section_exists("material:steel")) {
const input::Section* steel = config.section("material:steel");
value rho = steel->get_value("density", "kg/m^3");
value E = steel->get_value("youngs_modulus", "Pa");
}
for (index_t i = 0; i < config.num_sections(); ++i) {
const input::Section* sec = config.section(i);
if (sec->type() == "material") {
string mat_name = sec->label();
}
}
Unit Handling:
The get_value() method automatically converts from file units to BELFEM base units.
Supported temperature conversions (cl_Input_Section.cpp, create_key):
- K, °K — Kelvin (base unit, no conversion)
- C, °C — Celsius → K: T_K = T_C + 273.15
- °F — Fahrenheit → K: T_K = (T_F - 32) / 1.8 + 273.15 (a bare F is farad, not Fahrenheit)
- R, °R — Rankine → K: T_K = T_R / 1.8
General units: Uses unit_to_si() from physics module for automatic SI conversion:
density = 7.85 g/cm^3 → 7850 kg/m³
pressure = 100 MPa → 1e8 Pa
length = 25.4 mm → 0.0254 m
energy = 1 kJ → 1000 J
Usage:
value T = section->get_value("temperature", "C");
real T_kelvin = section->get_real("temperature");
string unit = section->get_units("temperature");
Error Handling Contract:
- Missing keys or sections are logic errors, not recoverable conditions.
- InputFile uses fail-fast behavior via BELFEM_ERROR assertions.
- Always check section_exists() and key_exists() before access in production code.
XML Class
Location: cl_XML.hpp
XML document interface using TinyXML2 backend.
Requires: BELFEM_XML enabled at compile time and TinyXML2 library
Constructor
XML(const string & aPath, const FileMode aMode = FileMode::OPEN_RDONLY);
Interface
const string & path() const;
void select_first_child(const string & aLabel);
void select_parent();
bool next_sibling_of_same_name();
void select_subtree(const string & aTree);
bool child_exists(const string & aLabel);
bool key_exists(const string & aKey);
uint number_of_children();
uint number_of_children(const string & aLabel);
string get_string(const string & aKey);
int get_int(const string & aKey);
real get_real(const string & aKey);
bool get_bool(const string & aKey);
Usage Example
<Configuration>
<Solver>
<type>MUMPS</type>
<threads>8</threads>
<Tolerance>1e-6</Tolerance>
<MaxIterations>1000</MaxIterations>
</Solver>
</Configuration>
#ifdef BELFEM_XML
XML xml("config.xml", FileMode::OPEN_RDONLY);
xml.select_first_child("Configuration");
xml.select_first_child("Solver");
string solver_type = xml.get_string("type");
int threads = xml.get_int("threads");
real tolerance = xml.get_real("Tolerance");
xml.select_parent();
#else
BELFEM_ERROR(
false,
"XML support not enabled. Reconfigure with -DUSE_TINYXML2=ON");
#endif
File Utilities
Location: filetools.hpp, filetools.cpp
FileMode Enum
enum class FileMode
{
NEW,
OPEN_RDONLY,
OPEN_RDONLY_PARALLEL,
OPEN_RDWR
};
Free Functions
bool file_exists(const string & aPath);
string filetype(const string & aPath);
string make_path_parallel(const string & aPath);
Usage:
if (file_exists("restart.h5")) {
HDF5 file("restart.h5", FileMode::OPEN_RDONLY);
}
string ext = filetype("data.exo");
string path = make_path_parallel("output.h5");
HDF5 file(path, FileMode::NEW);
Implementation details:
- file_exists(): std::filesystem::exists()
- filetype(): Returns substring after last '.'
- make_path_parallel(): reads the global communicator gComm for the rank and size. BELFEM is not internally thread safe (doc/coding_philosophy.md); this is safe only in the sense that it does not mutate gComm.
Common Patterns
Pattern 1: Checkpoint/Restart with HDF5
void save_checkpoint(const Vector<real> & aState, real aTime, int aTimestep) {
HDF5 file("checkpoint.h5", FileMode::NEW);
file.save_data("time", aTime);
file.save_data("timestep", aTimestep);
file.save_data("state", aState);
}
void load_checkpoint(Vector<real> & aState, real & aTime, int & aTimestep) {
if (!file_exists("checkpoint.h5")) {
}
HDF5 file("checkpoint.h5", FileMode::OPEN_RDONLY);
file.load_data("time", aTime);
file.load_data("timestep", aTimestep);
file.load_data("state", aState);
}
Pattern 2: Configuration-Driven Simulation
void setup_solver_from_config(const string & aConfigPath) {
InputFile config(aConfigPath);
const input::Section* solver_sec = config.section("Solver");
string solver_type = solver_sec->get_string("type");
real tolerance = solver_sec->get_real("tolerance");
SolverType type = string_to_solver_type(solver_type);
Solver solver(type);
SolverParameters params;
params.set_tolerance(tolerance);
if (solver_sec->key_exists("max_iterations")) {
params.set_max_iterations(solver_sec->get_int("max_iterations"));
}
solver.set_parameters(params);
}
Pattern 3: Time-Series Data Storage
void save_time_series(const Cell<real> & aTimes,
const Cell<Vector<real>> & aFields) {
HDF5 file("timeseries.h5", FileMode::NEW);
Vector<real> times(aTimes.size());
for (index_t i = 0; i < aTimes.size(); ++i) {
times(i) = aTimes(i);
}
file.save_data("times", times);
for (index_t i = 0; i < aFields.size(); ++i) {
string group_name = "TimeStep_" + std::to_string(i);
file.create_group(group_name);
file.save_data("field", aFields(i));
file.close_active_group();
}
}
Pattern 4: Parallel I/O (One File Per Rank)
void save_distributed_data(const Vector<real> & aLocalData) {
HDF5 file("distributed_output.h5", FileMode::NEW, true);
file.save_data("local_data", aLocalData);
file.save_data("rank", comm_rank());
file.save_data("size", comm_size());
}
void load_distributed_data(Vector<real> & aLocalData) {
string path = make_path_parallel("distributed_output.h5");
if (!file_exists(path)) {
BELFEM_ERROR(
false,
"Distributed file for rank %d not found", comm_rank());
}
HDF5 file("distributed_output.h5", FileMode::OPEN_RDONLY, true);
file.load_data("local_data", aLocalData);
}
Pattern 5: Reading Tabulated Data from CSV
void load_material_curve(const string & aPath,
Vector<real> & aStrain,
Vector<real> & aStress) {
CsvFile csv(aPath);
const Matrix<real> & data = csv.data();
index_t n = data.n_rows();
aStrain.set_size(n);
aStress.set_size(n);
for (index_t i = 0; i < n; ++i) {
aStrain(i) = data(i, 0);
aStress(i) = data(i, 1);
}
}
Performance Tips
1. Minimize HDF5 Group Operations
for (index_t i = 0; i < n; ++i) {
HDF5 file("data.h5", FileMode::OPEN_RDWR);
file.save_data("field_" + std::to_string(i), fields(i));
}
HDF5 file("data.h5", FileMode::NEW);
for (index_t i = 0; i < n; ++i) {
file.save_data("field_" + std::to_string(i), fields(i));
}
2. Batch HDF5 Writes
HDF5 file("data.h5", FileMode::NEW);
for (index_t i = 0; i < 1000000; ++i) {
file.save_data("value_" + std::to_string(i), values(i));
}
HDF5 file("data.h5", FileMode::NEW);
file.save_data("values", values);
3. Avoid Unnecessary String Copies in Ascii
Ascii file("large.txt", FileMode::OPEN_RDONLY);
for (index_t i = 0; i < file.length(); ++i) {
string line_copy = file.line(i);
}
Ascii file("large.txt", FileMode::OPEN_RDONLY);
for (index_t i = 0; i < file.length(); ++i) {
const string & line_ref = file.line(i);
}
4. Pre-Allocate for Large CSV Files
Ascii file("huge_data.csv", FileMode::OPEN_RDONLY);
5. One File Per Rank for Large MPI Jobs
BELFEM has no parallel-HDF5 support. BELFEM_PHDF5 is defined nowhere in the tree, and H5Pset_fapl_mpio is never called — every HDF5 file is opened with H5P_DEFAULT (cl_HDF5.cpp:47-51,67-71). The per-rank pattern is not a fallback; it is the only pattern.
HDF5 file("output.h5", FileMode::NEW, true);
Do not hand-roll it with make_path_parallel() alone. The open is gated on ( aParallelMode || gComm.rank() == 0 ) (cl_HDF5.cpp:40), so passing a per-rank path without the flag still leaves every non-root rank with no open file at all.
MPI Considerations
MPI I/O Contract
The only pattern: one file per rank, via the aParallelMode constructor argument.
- Each rank writes to its own file: base_N.X.ext
- No communication overhead during I/O
- Post-processing tools must gather files
There is no single-file pattern. aParallelMode = true does not open a shared file — it rewrites the path per rank and opens that. Nothing in the HDF5 layer is collective: no MPI access property list is set, and close() is a plain H5Fclose. A rank may open, write and close entirely on its own.
Consequences: there is no collective-close hazard to guard against, and post-processing always has to gather the per-rank files. If you genuinely need cooperative single-file output, it does not exist yet and would have to be added to the HDF5 wrapper.
File Naming Conventions
HDF5 file("output.h5", FileMode::NEW);
if (comm_size() > 1) {
HDF5 file("output.h5", FileMode::NEW, true);
}
Master-Rank I/O Pattern
if (comm_rank() == 0) {
InputFile config("solver.input");
}
Per-rank HDF5 output (aParallelMode)
aParallelMode is one file per rank, not cooperative PHDF5. The third constructor argument rewrites the path through make_path_parallel() (cl_HDF5.cpp:31-40) and opens that per-rank file with H5P_DEFAULT — no MPI access property list is ever set (cl_HDF5.cpp:47-51,67-71). Nothing is collective, so there is no cooperative write and no collective close to synchronize.
HDF5 file("global_solution.h5", FileMode::NEW, true);
file.save_data("local_field", local_data);
Because the files are separate, the usual parallel-I/O hazards do not apply: a rank that opens without the others cannot deadlock, and post-processing has to stitch the per-rank files together itself.
Thread Safety
HDF5 Thread Safety
- Not thread-safe by default (HDF5 library limitation)
- HDF5 output here is per-rank and process-level; there is no threaded or collective path
- Solution: Use OpenMP critical sections or one file per thread
#pragma omp parallel
{
int tid = omp_get_thread_num();
string path = "thread_" + std::to_string(tid) + "_output.h5";
HDF5 file(path, FileMode::NEW);
file.save_data("data", thread_local_data);
}
Ascii/CsvFile Thread Safety
- Read-only safe: Multiple threads can read same Ascii object
- Write not safe: Modifications to buffer require synchronization
Ascii file("data.txt", FileMode::OPEN_RDONLY);
#pragma omp parallel for
for (index_t i = 0; i < file.length(); ++i) {
const string & line = file.line(i);
}
InputFile Thread Safety
- Read-only after construction: Safe for concurrent queries
- Typical usage: Parse once in serial, query in parallel
InputFile config("params.input");
const input::Section* solver = config.section("Solver");
#pragma omp parallel
{
real tol = solver->get_real("tolerance");
}
Memory Ownership and Lifetime
All I/O classes use RAII (Resource Acquisition Is Initialization) — resources are automatically released in destructors.
Error Handling Reminder: BELFEM ends the run on error (abort in release, throw in debug). Resources are cleaned up explicitly before abort, or reclaimed by OS on process termination. See "Error Handling Philosophy" section above for details.
| Class | Resource Owned | Ownership Model | Lifetime Rules |
| HDF5 | File handle (hid_t mFile) | RAII | Destructor calls close() → H5Fclose() |
| Group handles (Cell<hid_t> mTree) | RAII | Destructor calls close() → close_active_group() → H5Gclose() on the active group only; close_tree() must be called explicitly to close the rest |
| Ascii | Line buffer (Cell<string> mBuffer) | Owned | Destructor clears buffer; BELFEM_ERROR (always active) if unsaved changes |
| CsvFile | Matrix data (Matrix<real> mData) | Owned | Destructor releases matrix memory (via Matrix RAII) |
| InputFile | Section tree (input::Section* mData) | Owned | Destructor deletes root section (recursive delete) |
| Section pointers returned | Non-owning | Valid while InputFile lives; do not delete |
| input::Section | Child sections (Cell<Section*> mData) | Owned | Destructor deletes all children recursively |
| Map entries (Map<string, Section*>) | Non-owning views | Point to children in mData; no separate cleanup |
| XML | TinyXML2 document (tinyxml2::XMLDocument mFile) | RAII | Managed by tinyxml2 (auto-cleanup) |
| Element pointers returned | Non-owning | Valid while XML object lives; do not delete |
Lifetime Rules
HDF5:
{
HDF5 file("data.h5", FileMode::NEW);
file.create_group("Results");
}
InputFile:
InputFile config("sim.input");
const input::Section* solver = config.section("Solver");
real tol = solver->get_real("tolerance");
Ascii buffer changes:
Ascii file("data.txt", FileMode::NEW);
file.print("Line 1");
file.print("Line 2");
file.save();
Debugging Tips
1. Enable HDF5 Error Reporting
2. Check File Existence Before Opening
if (!file_exists("restart.h5")) {
BELFEM_ERROR(
false,
"Restart file 'restart.h5' not found in %s",
std::filesystem::current_path().c_str());
}
3. Verify HDF5 Group State
HDF5 file("data.h5", FileMode::NEW);
file.create_group("A");
file.create_group("B");
file.close_active_group();
file.close_active_group();
4. Print InputFile Structure
InputFile config("complex.input");
config.print();
5. Validate Input File Sections
const input::Section* solver = config.section("Solver");
if (!solver->key_exists("tolerance")) {
BELFEM_ERROR(
false,
"Required key 'tolerance' not found in [Solver] section");
}
if (!solver->key_is_real("tolerance")) {
}
6. HDF5 File Inspection Tools
Use command-line tools to inspect HDF5 files:
# List contents
h5dump -n data.h5
# View dataset
h5dump -d /Results/Temperature data.h5
# Interactive browser (if available)
hdfview data.h5
7. Check CSV Parsing Errors
try {
CsvFile csv("data.csv");
} catch (...) {
BELFEM_ERROR(
false,
"Failed to parse CSV. Check for non-numeric data or irregular rows.");
}
Enumeration Reference
FileMode
Location: filetools.hpp
enum class FileMode
{
NEW,
OPEN_RDONLY,
OPEN_RDONLY_PARALLEL,
OPEN_RDWR
};
Usage Guidelines:
| Mode | Use Case | File Must Exist | Allows Write | Behavior if Exists |
| NEW | Creating output files | No | Yes | TRUNCATES (data loss!) |
| OPEN_RDONLY | Reading input files | Yes | No | Opens existing |
| OPEN_RDONLY_PARALLEL | Ascii only — rank 0 reads the file and broadcasts the buffer to the others (cl_Ascii.cpp:46-49, load_buffer(true)). HDF5 has no case for this mode and reaches its "unknown filemode passed" error (cl_HDF5.cpp:42-103) | Yes | No | Opens existing |
| OPEN_RDWR | Appending/modifying | Yes | Yes | Opens existing |
Critical: FileMode::NEW uses H5F_ACC_TRUNC — existing files are silently overwritten. Always check file_exists() first or use versioned filenames for production.
See Also