Date: 2026-01-16 Module: src/core Purpose: Comprehensive documentation of BELFEM's core utility module
Revision History:
- 2026-01-16: Initial documentation
- 2026-01-16: Technical corrections (Profiler, Timer, thread safety)
Core Module Contracts
Before diving into details, understand these fundamental design contracts:
- Minimal Dependencies: Core utilities must remain dependency-minimal (no FEM, mesh, numerics modules)
- Debug vs Release: Many checks use BELFEM_ASSERT, which is compiled out in release (NDEBUG); a release build still dies on BELFEM_ERROR
- Controlled Global State: Globals (gLog, gTbulk) are intentional and centrally managed, not accidental
- MPI Awareness: Some utilities behave differently in MPI vs serial builds (random, logging, profiling)
- Type Safety: BELFEM's typedefs (real, index_t, id_t) are the single source of truth—do not shadow or redefine
Table of Contents
- Introduction
- Module Architecture
- Logging and Output
- Timing and Profiling
- Command Line Processing
- Type System
- String Utilities
- Error Handling
- Hashing
- Random Number Generation
- Global Variables
- Thread Safety and MPI
- Common Pitfalls
- Usage Examples
Introduction
The core module provides fundamental utilities and infrastructure used throughout the BELFEM framework. It establishes the type system, provides logging and debugging facilities, and implements common utilities for string manipulation, timing, profiling, and command-line processing.
Key Design Principles:
- Lightweight dependencies: Core depends only on the C++ standard library, the containers module (Cell) and the comm layer (gComm, comm_abort) — never on FEM, mesh or numerics
- Cross-platform compatibility: Works on Linux and Darwin (macOS)
- Compiler agnostic: Supports GCC, Clang, and Intel compilers with appropriate pragmas
- MPI-aware: Some utilities (random, logging) adapt to MPI environments
Location: src/core/ CMake Target: belfem_core
Module Architecture
File Organization
The core module contains 26 source files organized into the following categories:
src/core/
├── Logging & Output
│ ├── cl_Logger.{hpp,cpp}
│ ├── cl_Progressbar.{hpp,cpp}
│ └── banner.{hpp,cpp}
│
├── Timing & Profiling
│ ├── cl_Timer.hpp
│ └── cl_Profiler.{hpp,cpp}
│
├── Command Line
│ └── cl_Arguments.{hpp,cpp}
│
├── Utilities
│ ├── cl_Hash.hpp
│ ├── stringtools.{hpp,cpp}
│ ├── fn_sprint.hpp
│ └── fn_available_memory.{hpp,cpp}
│
├── Type System
│ ├── typedefs.hpp
│ ├── constants.hpp
│ ├── units.hpp
│ ├── fn_check_unit.hpp
│ └── fn_to_enum.hpp
│
├── Error Handling
│ └── assert.{hpp,cpp}
│
├── Random Numbers
│ └── random.hpp
│
└── Globals
└── globals.hpp
Dependencies
The core module is self-contained with minimal external dependencies:
- C++ Standard Library: <chrono>, <string>, <cstdio>, <random>, etc.
- Container library: cl_Cell.hpp (BELFEM's dynamic array)
- MPI (optional): For cl_Communicator in random number generation
Logging and Output
Logger Class
File: cl_Logger.{hpp,cpp}
The Logger class provides hierarchical logging with configurable verbosity levels.
Info Levels
enum class InfoLevel
{
Silent = 0,
Minimal = 1,
Default = 2,
Detailed = 3,
Verbose = 4,
Everything = 5
};
Key Features
- Hierarchical verbosity: Messages only print if message_level <= logger_level
- Stream flexibility: Output to stdout or file
- Printf-style formatting: Uses variadic templates with sprint()
- Global instance: extern Logger gLog for framework-wide access
Implementation Details
Location: cl_Logger.hpp:43-108
class Logger
{
uint mInfoLevel = 0;
std::FILE* mStream;
bool mWriteToAscii = false;
public:
Logger(const InfoLevel aInfoLevel);
Logger(const InfoLevel aInfoLevel, const std::string & aPath);
~Logger();
uint info_level() const;
template <typename ... Args>
void message(
const InfoLevel aInfoLevel,
const std::string & aFormat,
const Args ... aArgs);
};
void message(const belfem::InfoLevel aInfoLevel, const std::string &aFormat, const Args ... aArgs)
Definition cl_Logger.hpp:144
Global convenience function:
template <typename ... Args>
const std::string & aFormat,
const Args ... aArgs)
{
}
void message(const InfoLevel aInfoLevel, const std::string &aFormat, const Args ... aArgs)
Definition cl_Logger.hpp:114
InfoLevel
Definition cl_Logger.hpp:32
Logger Lifecycle
Critical: The global gLog must be constructed once at application start, before any module code executes. Core and all modules assume gLog is already initialized. Reassigning or reinitializing gLog mid-run is undefined behavior.
Hierarchical logging with info levels.
Definition cl_Logger.hpp:50
@ Default
Definition cl_Logger.hpp:35
Framework code can then use the global logger anywhere:
message(InfoLevel::Default,
"Solving system with %d DOFs", num_dofs);
message(InfoLevel::Verbose,
"Matrix assembly time: %.3f ms", time);
Progressbar Class
File: cl_Progressbar.{hpp,cpp}
Visual progress indicator for long-running operations.
Key Features
- Configurable steps: Default 100 steps (1% increments)
- Stream output: Defaults to stdout, can redirect to file
- Step control: Manual step prescription or auto-increment
Implementation Details
Location: cl_Progressbar.hpp
class Progressbar
{
const index_t mWidth = 65;
const index_t mNumSteps;
FILE * mFile;
uint mProgress = 0;
uint mStep = 0;
public:
Progressbar(const uint aNumSteps=100, FILE * aFile = stdout);
void reset();
void step(const uint & aProgress);
void step();
void finish();
private:
void draw(const uint aStep, const uint aProgress);
void flush();
};
The bar only redraws when the drawn width actually grows, so the number of frames is bounded by mWidth no matter how many times step() is called.
Usage Pattern
Progressbar bar(num_iterations);
for (uint i = 0; i < num_iterations; ++i)
{
bar.step();
}
bar.finish();
⚠️ Thread Safety: Progressbar is not thread-safe. Use only from a single thread (typically rank 0 / master in MPI applications).
Output buffering. The bar redraws in place with \r and never writes a newline, so nothing in the C library flushes it on its own. Every frame is therefore followed by an explicit std::fflush. This matters most under mpirun: there stdout is a pipe rather than a terminal, so it is fully buffered in 4 KiB blocks, and a short bar would otherwise reach the user in a single burst when the program exits. Flushing per frame also emits each frame in one write(), which keeps the MPI I/O forwarder from tearing it apart. Should output still arrive in bursts, the remaining buffering is on the launcher side; mpirun --stream-buffering 0 turns it off.
Banner System
File: banner.{hpp,cpp}
Application startup banner with seasonal variants.
Key Features
- System information: OS, CPU info via uname and cpu_info()
- Seasonal banners: Easter, St. Patrick's, USA, Canada, Thanksgiving, Christmas
- Automatic selection: Based on system date
API
Location: banner.hpp:24-127
std::string exec(const std::string & aCommand);
std::string uname();
std::string cpu_info();
std::string version();
bool is_built_from_git();
std::string git_commit_hash();
std::string git_commit_hash_short();
std::string git_branch();
bool git_is_dirty();
void print_banner(const std::string aExecName = "");
namespace banners
{
bool print_default();
bool print_easter();
bool print_stpatrick();
bool print_usa();
bool print_canada();
bool print_thanksgiving();
bool print_christmas();
}
Global Constants
const std::string gLongName = "BELFEM -- The Berkeley Lab Finite Element Framework";
const std::string gURL = "http://belfem.lbl.gov";
Timing and Profiling
Timer Class
File: cl_Timer.hpp
High-resolution wall-clock timer using std::chrono::high_resolution_clock.
Key Features
- Millisecond precision: Returns elapsed time in ms
- Wall-clock time: Measures actual elapsed time, not CPU time (includes MPI waits, I/O)
- Lightweight: Header-only implementation with inline methods
- Three operations: stop(), next(), reset()
Implementation Details
Location: cl_Timer.hpp:21-63
class Timer
{
std::chrono::time_point<std::chrono::high_resolution_clock> mStart;
public:
inline Timer() : mStart(std::chrono::high_resolution_clock::now()) {}
inline uint64_t stop() {
return (unsigned int)(
std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::high_resolution_clock::now() - mStart).count()
);
}
inline uint64_t next() {
unsigned int aTime = this->stop();
mStart = std::chrono::high_resolution_clock::now();
return aTime;
}
inline void reset() {
mStart = std::chrono::high_resolution_clock::now();
}
};
⚠️ Limitation: next() stores the result in unsigned int before returning uint64_t, so it truncates past ~49.7 days (2³² milliseconds). stop() does not fix this — it casts the same way (cl_Timer.hpp:42-47). Neither method is safe for intervals that long; measure them from an external clock.
Usage Pattern
Timer timer;
uint64_t time1 = timer.next();
uint64_t time2 = timer.stop();
message(InfoLevel::Default,
"Phase 1: %lu ms, Phase 2: %lu ms", time1, time2);
Profiler Class
File: cl_Profiler.{hpp,cpp}
CPU profiling using Google's gperftools with Callgrind output format.
Key Features
- gperftools integration: Uses ProfilerStart/ProfilerStop from gperftools
- Callgrind conversion: Automatically converts profiler output to Callgrind format via pprof --callgrind
- Selective profiling: Profile only critical code sections
- MPI-aware: Creates separate profile files per rank in parallel runs
- CMake control: Enable with -DUSE_PROFILER=ON (generates the BELFEM_PROFILER define)
Implementation Details
Location: cl_Profiler.hpp:21-46
class Profiler
{
string mLogFile;
string mCallgrindFile;
public:
Profiler(const string aLogFile="profiler.log");
~Profiler() = default;
void start();
void stop();
};
Implementation (from cl_Profiler.cpp:59-92):
- start(): Calls ProfilerStart(mLogFile.c_str())
- stop(): Calls ProfilerStop(), then executes pprof --callgrind to generate .callgrind file
Build requirement: Requires gperftools library and BELFEM_PROFILER compile flag. Without this flag, start() and stop() are no-ops.
Usage Pattern
Profiler profiler("my_analysis.log");
profiler.start();
profiler.stop();
Note: The profiler generates two files: a gperftools native format log and a converted Callgrind file for compatibility with standard analysis tools.
Command Line Processing
Arguments Class
File: cl_Arguments.{hpp,cpp}
Base class for command-line argument parsing.
Key Features
- Extensible design: Virtual destructor for inheritance
- Cell storage: Arguments stored in Cell<string>
- Index access: Retrieve individual arguments by index
- Built-in verbosity flag: the constructor scans for -v / --verbose (GNU style) and sets the info level of the global logger
Built-In Flags
Every executable that constructs an Arguments object (or a subclass) accepts the shared verbosity flag; no code in the executable is needed beyond the construction itself:
| Form | Effect |
| -v, --verbose | InfoLevel::Everything (5) |
| -v 3, -v3, --verbose 3, --verbose=3 | info level 3 |
Levels follow InfoLevel in cl_Logger.hpp (0 = silent … 5 = everything, including third-party library output). Subclasses must not reuse -v or --verbose; a version flag follows the GNU convention -V / --version (see gastables::Arguments).
Implementation Details
Location: cl_Arguments.hpp:20-57
class Arguments
{
protected:
Cell<string> mArguments;
public:
Arguments(int & argc, char * argv[]);
virtual ~Arguments() = default;
const Cell<string> & data() const;
const string & data(const index_t aIndex) const;
};
Design Pattern
The Arguments class is designed as a base class for application-specific argument parsers. Applications derive from Arguments and add custom parsing logic:
class MyAppArguments : public Arguments
{
bool mVerbose = false;
string mInputFile;
public:
MyAppArguments(int & argc, char * argv[]) : Arguments(argc, argv)
{
}
};
Type System
Type Definitions
File: typedefs.hpp
Establishes the unified type system used throughout BELFEM.
Fundamental Types
Location: typedefs.hpp:25-37
typedef size_t size_t;
typedef std::string string;
typedef int sint;
typedef long int lsint;
typedef unsigned int uint;
typedef short unsigned int suint;
typedef long unsigned int luint;
typedef long long unsigned int lluint;
typedef double real;
typedef std::complex<real> cplx;
Framework-Specific Types
Location: typedefs.hpp:40-52
typedef unsigned int id_t;
typedef int proc_t;
typedef long long unsigned int key_t;
typedef __uint128_t key128_t;
#ifdef BELFEM_INT64
typedef int64_t int_t;
typedef uint64_t index_t;
#else
typedef int32_t int_t;
typedef uint32_t index_t;
#endif
Rationale: The index_t type can be compiled in 32-bit or 64-bit mode depending on mesh size requirements. Most problems use 32-bit indices for memory efficiency.
⚠️ Portability Note: key128_t uses __uint128_t, a GCC/Clang extension not available in all compilers (e.g., MSVC). Code using 128-bit keys may not be portable to non-GNU toolchains.
⚠️ Type Safety Rule: Do not introduce using std::size_t or redefine fundamental typedefs in higher-level modules. BELFEM's typedefs are the single source of truth for ABI consistency.
Sentinel Values
Location: typedefs.hpp:56-62
constexpr index_t gNoIndex = std::numeric_limits<index_t>::max();
constexpr id_t gNoID = std::numeric_limits<id_t>::max();
constexpr proc_t gNoOwner = std::numeric_limits<proc_t>::max();
constexpr real gTfreeze = 273.15;
constexpr real gTref = 288.15;
constexpr real gTroom = 293.15;
Unit System
Location: typedefs.hpp:65-68
typedef std::array<real, 7> unit;
typedef std::pair<real, unit> value;
The unit type represents physical dimensions as a 7-element array following the SI base unit system.
⚠️ Limitation: The unit system performs dimension checking only via array comparison. It does not enforce unit consistency at compile time, nor does it track unit scaling automatically. Users must manually apply conversion factors via the constants in units.hpp.
Limits and Special Values
Location: typedefs.hpp:72-85
#define BELFEM_SINT_MAX std::numeric_limits<sint>::max()
#define BELFEM_UINT_MAX std::numeric_limits<uint>::max()
#define BELFEM_REAL_MAX std::numeric_limits<real>::max()
#define BELFEM_REAL_MIN std::numeric_limits<real>::min()
#define BELFEM_INT_MAX std::numeric_limits<int>::max()
#define BELFEM_LUINT_MAX std::numeric_limits<luint>::max()
#define BELFEM_KEY_MAX std::numeric_limits<key_t>::max()
#define BELFEM_SIGNALING_NAN std::numeric_limits<real>::signaling_NaN()
#define BELFEM_QUIET_NAN std::numeric_limits<real>::quiet_NaN()
#define BELFEM_INFINITY std::numeric_limits<real>::infinity()
constexpr real BELFEM_EPSILON = 10 * std::numeric_limits<real>::epsilon();
constexpr real BELFEM_EPS = std::numeric_limits<real>::epsilon();
constexpr real BELFEM_MESH_EPSILON = 1e-9;
Usage notes:
- BELFEM_EPSILON: Used for general floating-point comparisons (≈ 2.22e-15)
- BELFEM_EPS: Machine epsilon (≈ 2.22e-16)
- BELFEM_MESH_EPSILON: Geometric tolerance for mesh operations (1 nm)
⚠️ Critical Distinction: Use BELFEM_EPSILON for mathematical/algorithmic comparisons (convergence, residuals). Use BELFEM_MESH_EPSILON for spatial/geometric tolerances (node coincidence, element validity). Mixing these can cause topological errors in mesh operations.
Physical Constants
File: constants.hpp
NIST-compliant physical and mathematical constants.
Mathematical Constants
Location: constants.hpp:37-51
{
const real pi = 3.141592653589793238462643383279502884;
const real phi = 0.5 * (1.0 + std::sqrt(5.0));
}
Definition constants.hpp:33
const real phi
golden number
Definition constants.hpp:48
const real pi
circle number
Definition constants.hpp:41
const real deg
degree
Definition constants.hpp:55
double real
Definition typedefs.hpp:36
General Physics
Location: constants.hpp:61-100
const real c = 299792458.0;
const real mu0 = 1.25663706212e-6;
const real nu0 = 1.0/mu0;
const real epsilon0 = 1.0/(mu0*std::pow(c, 2));
const real G = 6.67430e-11;
const real e = 1.602176634e-19;
All values sourced from NIST (https://physics.nist.gov/cuu/Constants/).
Thermodynamics
Location: constants.hpp:110-178
const real calTh = 4.184;
const real kB = 1.380649e-23;
const real NA = 6.02214076e23;
const real u = 0.001 / NA;
const real Rm = kB * NA;
const real Rm_cal = Rm / calTh;
const real h = 6.62607015e-34;
const real sigma = 2.0 * std::pow(pi, 5) * std::pow(kB, 4) /
(15.0 * std::pow(h, 3) * std::pow(c, 2));
const real L0 = std::pow(pi * kB / e, 2) / 3.0;
Earth Constants
Location: constants.hpp:188-202
const real g0 = 9.80665;
const real mu_earth = 3.986004418e14;
const real R_earth = std::sqrt(mu_earth / g0);
Unit Conversions
File: units.hpp
Conversion factors from common units to SI base units.
Metric Units
Location: units.hpp:37-41
{
}
const real rpm
Definition units.hpp:41
const real atm
Definition units.hpp:40
const real km
Definition units.hpp:38
const real bar
Definition units.hpp:39
const real l
Definition units.hpp:37
Imperial/US Units
Location: units.hpp:47-54
const real in = 0.0254;
const real ft = 12.0 * in;
const real mi = 5280.0 * ft;
const real gal = 231.0 * in * in * in;
const real lb = 0.45359237;
const real lbf = lb * constant::g0;
const real psi = lbf / (in * in);
const real oz = gal / 128.0;
Note: These are "Freedom Units" as humorously labeled in the source code.
Unit Checking
File: fn_check_unit.hpp
Location: fn_check_unit.hpp:24-28
inline bool check_unit(const value & aValue, const string & aUnit)
{
return aValue.second == unit_to_si(aUnit).second;
}
Validates that a value (pair of magnitude and unit array) matches the expected unit dimensions.
Enum Conversion
File: fn_to_enum.hpp
Generic string-to-enum converter using template metaprogramming.
Key Features
- Generic: Works with any enum that defines UNDEFINED sentinel
- Case-insensitive: Uses string_to_lower()
- Safe: Returns UNDEFINED if no match found
Implementation
Location: fn_to_enum.hpp:25-48
template <typename T>
void to_enum(const string & aString, T & aEnum)
{
int tNumEntries = (int) T::UNDEFINED;
aEnum = T::UNDEFINED;
string tString = string_to_lower(aString);
for (int k = 0; k < tNumEntries; ++k)
{
aEnum = (T) k;
string tEnum = string_to_lower(to_string(aEnum));
if (tString == tEnum)
return;
}
aEnum = T::UNDEFINED;
}
Requirements: The enum type T must:
- Have an UNDEFINED member as the last entry
- Provide a to_string(T) overload
String Utilities
String Tools
File: stringtools.{hpp,cpp}
Comprehensive string manipulation library.
Type Introspection
Location: stringtools.hpp:62-130
template <typename T>
inline std::string datatype_string();
Returns human-readable type names for debugging and serialization.
Path Manipulation
Location: stringtools.hpp:136-162
std::string basename(const std::string & aFilePath);
std::string dirname(const std::string & aFilePath);
std::string filetype(const std::string & aFilePath);
std::string filename(const std::string & aFilePath);
String Processing
Location: stringtools.hpp:169-217
std::string clean_string(const std::string & aString);
string first_word(const std::string & aString, const char aDelimiter = ' ');
Cell<string> string_to_words(const std::string & aString,
const char aDelimiter = ' ');
std::string search_and_replace(const std::string & aString,
const std::string & aSearch,
const std::string & aReplace);
std::string string_to_lower(const std::string & aString);
std::string string_to_upper(const std::string & aString);
Type Conversion
Location: stringtools.hpp:220-230
bool string_to_bool(const std::string & aString);
real to_real(const std::string & aString);
value unit_to_si(const string & aString);
bool is_integer(const string & aString);
Formatting
Location: stringtools.hpp:234-245
string format_with_leading_zeros(const uint aNumber);
size_t utf8_character_count(const string & aString);
Advanced Parsing
Location: stringtools.hpp:249-308
template <typename T>
void string_to_cell(const string & aString, Cell<T> & aValues);
Parses complex numeric expressions into a Cell<T>:
- Comma delimited (blanks are ignored, so "1 2 3" reads as 123): "1, 2, 3" → {1, 2, 3}
- Range notation: "1:5" → {1, 2, 3, 4, 5}
- Reverse ranges: "5:1" → {5, 4, 3, 2, 1}
- Mixed format: "1, 3:5, 7" → {1, 3, 4, 5, 7}
- Braces ignored: "{1, 2, 3}" → {1, 2, 3}
- Semicolon terminator: "1, 2 ; ignored" and "1,2;ignored" both → {1, 2} (the parser pads ; into its own token)
Formatted Printing
File: fn_sprint.hpp
Printf-style formatting to std::string.
Implementation
Location: fn_sprint.hpp:43-57
template <typename ... Args>
std::string sprint(const char * aFormat, const Args ... aArgs)
{
auto tSize = std::snprintf(nullptr, 0, aFormat, aArgs ...);
std::unique_ptr<char[]> tBuffer(new char[tSize + 1]);
std::snprintf(tBuffer.get(), tSize + 1, aFormat, aArgs ...);
return string(tBuffer.get(), tBuffer.get() + tSize);
}
Usage
std::string msg = sprint("Iteration %d: residual = %.6e", iter, residual);
message(InfoLevel::Default,
"Matrix size: %dx%d", nrows, ncols);
Design note: Compiler warnings for format strings are deliberately suppressed via pragmas (stringtools.hpp:35-58) to allow flexible printf-style formatting. Use with care to avoid format/argument mismatches.
Error Handling
Assertion System
File: assert.{hpp,cpp}
Custom assertion framework with formatted error messages and ASCII art dragon.
Macros
Location: assert.hpp:243-275
#if !defined(NDEBUG) || defined(DEBUG)
#define BELFEM_ASSERT(aCheck, ...) \
do { \
if (!(aCheck)) { \
belfem::assert::belfem_assert(__FILE__, __LINE__, \
__PRETTY_FUNCTION__, #aCheck, __VA_ARGS__); \
} \
} while (false)
#else
#define BELFEM_ASSERT(aCheck, ...)
#endif
#define BELFEM_ERROR(aCheck, ...) \
do { \
if (!(aCheck)) { \
belfem::assert::belfem_assert(__FILE__, __LINE__, \
__PRETTY_FUNCTION__, #aCheck, __VA_ARGS__); \
} \
} while (false)
Difference:
Error Formatting and Reaction
Location: assert.hpp (template error()), assert.cpp (state)
template <typename Exception>
void error(const std::string & aLocation,
const std::string & aTask,
const std::string & aCheck,
const Exception & aException = Exception())
{
...
print_errorbox(aLocation, aTask, aCheck, tMessage);
if (syslog_on_error())
log_to_syslog(aLocation, aCheck, tMessage);
if (throw_on_error())
throw aException;
error_abort();
}
System Log Integration (2026-08-15)
Every failed BELFEM_ERROR / BELFEM_ASSERT also writes its message to the system log — identity belfem, priority LOG_CRIT, facility LOG_USER — so a crashed run leaves a trace even when the terminal scrollback or a redirected stderr is gone. Read it back with:
# Linux (systemd)
journalctl -t belfem # all BELFEM error events
journalctl -t belfem --since -1h # recent ones
journalctl -t belfem -f # follow live while a run is up
# macOS (unified logging — syslog(3) is bridged into it since 10.12)
log show --predicate 'senderImagePath CONTAINS "belfem" OR eventMessage CONTAINS "rank "' --last 1h
log stream --predicate 'eventMessage CONTAINS "rank "' # follow live
# syslog-only systems (BSD, older Linux)
grep belfem /var/log/messages
syslog(3) is POSIX, so the hook compiles and runs on every platform BELFEM targets — no Linux guard. Only the retrieval differs: macOS routes it into the unified log rather than a text file, and journalctl does not exist there. On macOS the messages are not guaranteed to reach /var/log/system.log; use log show.
Each event carries the MPI rank in the payload — LOG_PID only identifies local processes, and rank-local checks fire on a single rank that is usually not rank 0. Errors raised before gComm.init() log rank ?. Multi-line messages are written one syslog line per message line, so nothing is lost to the ~1 KiB per-call truncation.
Two properties worth knowing:
- The write happens before the reaction branch. This is deliberate: debug builds throw by default, and a hook on the abort path only would never fire in exactly the builds developers run daily.
- Test binaries that exercise error paths on purpose disable it (belfem::assert::set_syslog_on_error(false), set in every test main beside set_throw_on_error(true)), so make check does not spam the journal.
log_to_syslog() runs in normal control flow only — syslog() is not async-signal-safe, so it must never be called from a signal handler.
ASCII Art Dragon
Location: assert.cpp (implementation in hatch_dragon())
When an error occurs, BELFEM prints a decorative error box with an ASCII dragon character, making errors visually distinctive:
╔═══════════════════════════════════════════════════════╗
║ 🐉 ERROR 🐉 ║
╠═══════════════════════════════════════════════════════╣
║ Location: my_file.cpp (line 42) ║
║ Task: complete call to function my_function() ║
║ Check: Assertion index < size failed. ║
║ ║
║ Index 100 exceeds array size 50 ║
╚═══════════════════════════════════════════════════════╝
Helper Functions
Location: assert.hpp:31-58
std::vector<std::string> wrap_lines(std::size_t aMaxWidth, const std::string & aLine);
void hatch_dragon(std::vector<std::string> & aDragon);
void print_line(const std::vector<std::string> & aDragon, std::size_t & aCounter);
void get_lines(const string & aWhat, std::vector<std::string> & aLines);
void print_errorbox(const std::string & aLocation,
const std::string & aTask,
const std::string & aCheck,
const std::vector<std::string> & aMessage);
void error_abort();
std::string extract_function_name(const std::string & aPrettyFunction);
Usage
"Index %lu exceeds array size %lu", index, size);
"Failed to open file: %s", filename.c_str());
#define BELFEM_ERROR(aCheck,...)
Definition assert.hpp:264
#define BELFEM_ASSERT(aCheck,...)
Definition assert.hpp:244
Hashing
Hash Class
File: cl_Hash.hpp
Incremental hash computation with entropy mixing.
Algorithm
Location: cl_Hash.hpp:68-83
template <typename T>
Hash & operator += (const T aValue)
{
mValue +=
std::hash<T>{}(aValue)
+ 0x9e3779b97f4a7c15ULL
+ (mValue << 6)
+ (mValue >> 2);
return *this;
}
Entropy constant: 0x9e3779b97f4a7c15 is derived from the golden ratio φ = (1+√5)/2:
φ × 2^64 ≈ 11400714819323198485 = 0x9e3779b97f4a7c15
This value is commonly used in hash functions (Knuth, boost::hash_combine) for good avalanche properties.
API
Location: cl_Hash.hpp:30-80
class Hash
{
std::size_t mValue = 0;
public:
Hash() = default;
~Hash() = default;
void reset();
std::size_t value() const;
void set_value(const std::size_t aValue);
template <typename T>
Hash & operator += (const T aValue);
};
Usage Pattern
Hash hash;
hash += node_id;
hash += x_coordinate;
hash += y_coordinate;
hash += z_coordinate;
std::size_t key = hash.value();
Design rationale: The incremental interface allows computing hashes of composite objects field-by-field, useful for geometric point hashing, element signatures, etc.
⚠️ Collision Note: Hash is not cryptographic and does not guarantee collision-free behavior. Use only for caching, lookup heuristics, and non-security-critical map keys. For security-sensitive applications, use a proper cryptographic hash.
Random Number Generation
Random Number System
File: random.hpp
MPI-aware random number generation.
Key Features
- MPI-aware: Uses gComm.random() generator in MPI builds
- Serial builds: random_seed() reads a seed from /dev/urandom (or the clock) and seeds std::rand(), which rand() uses
- C++ <random> API: Modern random number generation
Seeding
Location: random.hpp:26-72
#ifndef BELFEM_MPI
template <typename T>
void random_seed(T & aSeed)
{
std::ifstream tStream("/dev/urandom", std::ios::binary);
if (tStream)
{
char tMemblock[sizeof(T)];
tStream.read(tMemblock, sizeof(T));
tStream.close();
aSeed = *reinterpret_cast<T*>(tMemblock);
}
else
{
aSeed = (T) time(NULL);
}
}
#endif
inline void random_seed()
{
#ifdef BELFEM_MPI
std::random_device rd;
#else
unsigned int tSeed;
random_seed(tSeed);
#endif
}
belfem::Communicator gComm
Definition belfem.cpp:35
std::mt19937 & random()
Definition cl_Communicator.hpp:165
Random Number Generation
Location: random.hpp:80-89
inline real rand()
{
#ifdef BELFEM_MPI
std::uniform_real_distribution<real> distribution(0.0, 1.0);
#else
return ((real) std::rand()) / RAND_MAX;
#endif
}
Usage Pattern
random_seed();
for (int i = 0; i < 1000; ++i)
{
real value = rand();
}
MPI considerations: In MPI builds, each process has its own independent random stream via gComm.random(), ensuring reproducibility and avoiding correlation between processes.
⚠️ Reproducibility:
- MPI builds guarantee independent streams per rank, not identical sequences across ranks
- For deterministic reproducibility across runs, use explicit seeding via communicator logic with a fixed seed
- Serial builds are random run to run as well once random_seed() has been called; without that call std::rand() keeps its default seed
Global Variables
Global Variable System
File: globals.hpp
Framework for managing global variables with MPI synchronization.
Macro Magic
Location: globals.hpp:18-22
#ifdef BELFEM_INITIALIZE_GLOBALS
#define greal real
#define gstring string
#else
#define greal extern real
#define gstring extern string
#endif
Rationale: This pattern ensures global variables are:
- Declared as extern in normal includes (no storage allocated)
- Defined only when Communicator::set_globals() is called
This avoids multiple definition errors while allowing global access.
Current Globals
Location: globals.hpp:41-53
{
}
#define gstring
Definition globals.hpp:23
#define greal
Definition globals.hpp:22
USER GUIDES:
Definition cl_Capacitor.cpp:16
greal gTbulk
Definition globals.hpp:45
greal gRhoMin
minimim resistrivity in Ohm*m, default: 0
Definition globals.hpp:48
gstring gBelfemDataPath
path to belfem data files, set by environment variable $BELFEM_DATA
Definition globals.hpp:54
greal gRhoMax
maximim resistrivity in Ohm*m, default: 1e10
Definition globals.hpp:51
Usage Guidelines
⚠️ CRITICAL WARNING: Globals should be used only for true global physical parameters (like bulk temperature, reference pressure) that are genuinely shared across the entire simulation. Do not use globals for:
- Algorithmic state or control flow
- Caches or temporary storage
- Mutable iteration counters
- Solver-specific configuration
Misuse of globals creates hidden dependencies, breaks modularity, and causes subtle MPI synchronization bugs.
From the source code comments (globals.hpp:24-38):
- Adding new parameters:
- Add declaration in globals.hpp
- Initialize in Communicator::set_globals()
- Use BELFEM_QUIET_NAN as default if no clear initial value
- Justify the need - is this truly a framework-wide physical constant?
- MPI synchronization:
- The BELFEM_INITIALIZE_GLOBALS macro must only be defined inside Communicator::set_globals()
- Defining it elsewhere causes multiple-definition linker errors
- If value is set only on master process, it is the developer's responsibility to broadcast:
gMyVariable = compute_on_master();
broadcast(gMyVariable);
Example
{
}
#define BELFEM_INITIALIZE_GLOBALS
void Communicator::set_globals()
{
}
gTbulk = 300.0;
broadcast(gTbulk);
#define BELFEM_QUIET_NAN
Definition typedefs.hpp:87
Thread Safety and MPI
Thread Safety Summary
Core utilities have varying thread-safety characteristics:
| Component | Thread-Safe (Read-Only) | Requires External Sync |
| Timer | ✓ (each thread uses own instance) | N/A |
| Hash | ✓ (each thread uses own instance) | N/A |
| unit_to_si() | ✓ (const function) | N/A |
| string_to_bool() | ✓ (const function) | N/A |
| all stringtools functions | ✓ (stateless) | N/A |
| Logger | ✗ | Needs mutex for concurrent writes |
| Progressbar | ✗ | Single-thread use only |
| Profiler | ✗ | Single-thread use only |
| rand() | ✗ | Needs per-thread RNG in OpenMP |
| gLog, gComm, globals | ✗ | External sync required |
MPI Behavioral Differences
Several utilities behave differently in MPI vs serial builds:
| Function | Non-MPI Behavior | MPI Behavior |
| rand() | Uses std::rand() | Uses gComm.random() per-rank generator |
| random_seed() | Seeds std::rand() from /dev/urandom or time() | Seeds from std::random_device |
| error_abort() | Calls std::abort() | Calls MPI_Abort(MPI_COMM_WORLD) |
| print_banner() | Always prints | Only rank 0 prints (typical usage) |
| Progressbar | stdout is a line-buffered terminal | stdout is a fully buffered pipe; the bar flushes each frame itself |
| Profiler | Single file | Separate file per rank (e.g., profiler.4.2.log and profiler.4.2.callgrind for size=4, rank=2) |
Best Practices
- Logger in MPI: Typically construct on rank 0 only, or use rank-specific log files
- Progressbar in MPI: Use only on rank 0 to avoid garbled output. The bar flushes every frame, so it streams through the mpirun pipe instead of arriving in one burst at exit
- Random in OpenMP: Create thread-local RNG instances instead of calling global rand()
- Globals: Always broadcast after modification on master rank:
gTbulk = compute_value();
}
broadcast(gTbulk);
proc_t rank() const
Definition cl_Communicator.hpp:192
Common Pitfalls
Logger and Output
Pitfall: Forgetting to check info_level() before expensive string formatting
message(InfoLevel::Verbose,
"Expensive: %s", compute_expensive_string().c_str());
message(InfoLevel::Verbose,
"Expensive: %s", compute_expensive_string().c_str());
}
uint info_level() const
return the info level of the logger
Definition cl_Logger.cpp:63
Pitfall: Calling Progressbar::step() after finish() is a silent no-op — finish() leaves the bar at full width, and step() only redraws when the width grows. Restart the bar with reset() if you need it again.
Pitfall: Long error messages exceeding terminal width - use wrap_lines() handles this automatically in assert.cpp
Timer and Profiling
Pitfall: Using Timer::next() for long-running timers (>49 days) causes truncation
uint64_t elapsed = timer.next();
uint64_t elapsed = timer.stop();
Pitfall: Forgetting to enable profiler at build time - Profiler::start()/stop() do nothing without -DUSE_PROFILER=ON
String Parsing
Pitfall: Range endpoints are parsed with std::stoll and cast to T
string_to_cell("5:1", nodes);
string_to_cell("-1:1", nodes);
Random Numbers
Pitfall: Not seeding RNG leads to deterministic sequences
for (int i = 0; i < 100; ++i) {
}
for (int i = 0; i < 100; ++i) {
}
void random_seed()
seed the random generator.
Definition random.hpp:67
real rand()
a random number between 0 and 1 must call seed first
Definition random.hpp:86
Pitfall: Assuming identical random sequences across MPI ranks - each rank has independent stream
Type System
Pitfall: Mixing epsilon constants
real distance = node_a.distance_to(node_b);
if (distance < BELFEM_EPSILON) { }
if (distance < BELFEM_MESH_EPSILON) { }
Pitfall: Progressbar constructor type mismatch on 64-bit builds
Progressbar bar(very_large_mesh_size);
Error Handling
Pitfall: Using BELFEM_ASSERT for runtime checks
Pitfall: Format string/argument mismatch in assertions
Global Variables
Pitfall: Defining BELFEM_INITIALIZE_GLOBALS outside Communicator::set_globals() causes linker errors
Pitfall: Using globals for algorithm state creates hidden dependencies and breaks modularity
Pitfall: Forgetting to broadcast globals after master-only computation
gTbulk = 300.0;
}
gTbulk = 300.0;
}
broadcast(gTbulk);
Usage Examples
Example 1: Basic Application Setup
int main(
int argc,
char * argv[])
{
uint64_t elapsed = timer.
stop();
return 0;
}
Command-line argument parsing.
Definition cl_Arguments.hpp:27
High-resolution wall-clock timing.
Definition cl_Timer.hpp:28
uint64_t stop()
Definition cl_Timer.hpp:43
void print_banner(const std::string aExecName)
prints the banner
Definition banner.cpp:213
Example 2: Profiling a Critical Section
void optimize_mesh()
{
profiler.start();
"Mesh optimization: %lu ms", timer.
stop());
}
gperftools CPU profiling with Callgrind-format output.
Definition cl_Profiler.hpp:28
@ Verbose
Definition cl_Logger.hpp:37
Example 3: Progress Tracking
void assemble_system(uint num_elements)
{
for (uint e = 0; e < num_elements; ++e)
{
assemble_element(e);
bar.step();
}
bar.finish();
}
Progress display for long-running loops.
Definition cl_Progressbar.hpp:27
Example 4: Error Handling
void compute_jacobian(const Matrix & J)
{
real det = J.determinant();
"Singular Jacobian detected: det(J) = %.3e", det);
real inv_det = 1.0 / det;
}
Example 5: String Parsing
void parse_node_list(const string & input)
{
Cell<index_t> nodes;
string_to_cell(input, nodes);
for (index_t node_id : nodes)
{
process_node(node_id);
}
}
Example 6: Hash-Based Caching
std::size_t compute_element_signature(const Element * element)
{
hash += static_cast<uint>(element->type());
for (uint i = 0; i < element->number_of_nodes(); ++i)
{
hash += element->node(i)->id();
}
}
std::unordered_map<std::size_t, Matrix> shape_function_cache;
if (shape_function_cache.find(signature) == shape_function_cache.end())
{
shape_function_cache[signature] = compute_shape_functions();
}
Incremental hash computation.
Definition cl_Hash.hpp:29
std::size_t value() const
Definition cl_Hash.hpp:53
Example 7: Unit Conversion
void set_pressure_boundary_condition()
{
real pressure_psi = 14.7;
real pressure_si = pressure_psi *
psi;
"Conversion error: expected %f Pa, got %f Pa",
atm, pressure_si);
apply_pressure(pressure_si);
}
const real psi
Definition units.hpp:53
Example 8: Random Mesh Perturbation
{
for (
uint n = 0; n <
mesh->number_of_nodes(); ++n)
{
Node * node =
mesh->node(n);
node->x() += dx;
node->y() += dy;
node->z() += dz;
}
}
Top-level container for all mesh entities.
Definition cl_Mesh.hpp:60
unsigned int uint
Definition typedefs.hpp:30
Summary
The core module provides essential infrastructure for BELFEM applications:
| Category | Components | Purpose |
| Logging | Logger, Progressbar, Banner | User communication and debugging |
| Performance | Timer, Profiler | Timing and profiling |
| CLI | Arguments | Command-line processing |
| Types | typedefs, constants, units | Type system and physical constants |
| Strings | stringtools, sprint | String manipulation and formatting |
| Errors | assert macros | Debugging and error handling |
| Utilities | Hash, random, globals | Common algorithmic utilities |
Design Philosophy
- Minimal dependencies: Core utilities should not depend on higher-level BELFEM modules
- Type safety: Strong typing via custom typedefs (index_t, id_t, etc.)
- Physical correctness: NIST-compliant constants and SI unit system
- Developer experience: Informative error messages, progress feedback, profiling tools
- Cross-platform: Compiler-agnostic with appropriate pragma guards
Further Reading
- Build system: See CLAUDE.md for CMake build instructions
- MPI integration: See src/comm/ for parallel communication
- Container library: See src/containers/cl_Cell.hpp for dynamic arrays
- Mesh infrastructure: See src/mesh/ for mesh data structures that use core types
Document version: 1.0 Last updated: 2026-01-16 Author: Claude Code (claude.ai/code)