You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document details the motivation, process, and final architecture resulting from the refactoring of the main function in laghost.cpp. The goal of this effort was to enhance the modularity, readability, and long-term maintainability of the codebase.
The changes will be merged through the pull request process.
Once merged, this summary will become the official documentation for the main trunk.
1. The Refactoring Process
This section explains the "why" and "how" of the refactoring journey. It's intended for co-developers interested in the technical decisions and challenges encountered.
1.1. Motivation and Intention
The primary goal was to improve the modularity, readability, and maintainability of the laghost.cpp executable. The original main function was monolithic, containing all logic for setup, execution, and cleanup in a single, long block of code. This made it difficult to navigate and modify without risking unintended side effects.
The intended final structure was to separate these concerns into a clean, high-level workflow, driven by three distinct functions:
initialize(): Sets up the entire simulation state, from MPI and parameters to mesh and initial conditions.
run(): Executes the core time-integration loop, including the complex remeshing logic.
finalize(): Reports results, calculates final errors, and cleans up all allocated resources.
This separation makes the program's lifecycle explicit and helps future developers quickly locate the relevant code for their modifications.
1.2. Challenges & Stepwise Evolution
A direct, single-step refactoring proved impossible due to the complexity of the code and subtle C++ object lifetime rules. The process evolved iteratively to solve a series of compilation and runtime errors, which were diagnosed with the help of GDB.
Initial Compile Errors (Use of deleted function): The first attempts involved creating a large AppState struct to hold all shared variables. This failed because many MFEM objects (like mfem::BlockVector) do not have default constructors, which prevented the AppState struct from being instantiated directly.
Move Semantics Error: The next approach fixed the construction issue by using smart pointers (std::unique_ptr) but failed when trying to return the AppState struct by value from initialize(). A struct containing std::unique_ptr is not copyable, and its move constructor was implicitly deleted, preventing the return.
Runtime Crashes (SIGSEGV & pure virtual method called): After switching to a "pass-by-reference" initialization pattern, the program compiled but crashed during the run phase. Debugging with GDB revealed a series of dangling pointer issues:
Dangling ess_tdofs: The ess_tdofs array, critical for boundary conditions, was local to initialize(), but the LagrangianGeoOperator stored a reference to it.
Dangling FECollection: The FECollection objects were also local to initialize(), but the ParFiniteElementSpace objects stored pointers to them.
Invalid Mesh Pointer: The most persistent error was a segmentation fault caused by a GridFunction having a FiniteElementSpace that pointed to a null or garbage Mesh pointer. This was traced back to the L2FESpace_mat for the composition (comp_gf) being created locally within initialize() and destroyed on exit.
1.3. Final Design and Key Decisions
The successful final architecture is a direct result of solving the challenges above through systematic debugging:
AppState Struct: A single, comprehensive struct holds the entire state of the simulation. This makes the data flow explicit and prevents global variables.
Smart Pointers (std::unique_ptr): All objects that are dynamically allocated and have a single clear owner (e.g., ParMesh, ODESolver, FECollection, ParFiniteElementSpace) are managed by std::unique_ptr. This automates memory management, prevents leaks, and enforces clear ownership semantics.
Pass-by-Reference Initialization: The initialize(AppState&, ...) pattern is used. main owns the AppState object, and passes a reference to initialize to be populated. This avoids all problematic copying or moving of the large state object.
Correct Initialization Order: The final, stable initialization sequence was determined through debugging. The key was ensuring that all ParFiniteElementSpace objects have their dependent FECollections managed with the same lifetime and that their internal tables are built with Update() after the mesh's nodal grid function is set.
2. Guide to the Refactored Code
This section serves as a guide for developers using or modifying the new code structure.
2.1. High-Level Architecture
The program's execution flow is now controlled by three clear, distinct phases, driven by the main function:
Its only responsibility is to create the AppState object and orchestrate the three main phases of the simulation.
AppState struct
Acts as a central container for all persistent data and objects used throughout the simulation.
If you need to add a new variable that must be accessed in more than one phase (e.g., set during initialization and used in the run loop), it should be added as a member to this struct.
Resource ownership is managed via std::unique_ptr for dynamically allocated objects.
initialize(AppState& appState, ...)
This function handles all setup and resource allocation.
Its responsibilities include: MPI initialization, parameter parsing, mesh generation, creating FiniteElementSpaces, setting up ODE solvers, allocating state vectors, setting initial conditions, and preparing visualization data.
It takes a reference to the AppState object and populates all its members.
run(AppState& appState)
This function contains the core time-integration loop.
It is responsible for advancing the simulation state from t=0 to t_final.
All per-step logic resides here, including: calling the ODE solver, handling surface processes, performing plasticity calculations, and managing adaptive time-stepping.
Crucially, it also contains the logic for periodic remeshing and data visualization.
finalize(AppState& appState)
This function handles all post-simulation tasks.
It is responsible for: printing final performance metrics (like Figure of Merit), calculating final energy differences and errors, and cleaning up any remaining resources (like closing visualization sockets or deleting raw pointers).
Memory managed by std::unique_ptr in AppState is cleaned up automatically when appState goes out of scope after this function completes.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Documentation for the Refactoring of laghost.cpp
Date: July 15, 2025
This document details the motivation, process, and final architecture resulting from the refactoring of the main function in laghost.cpp. The goal of this effort was to enhance the modularity, readability, and long-term maintainability of the codebase.
The changes will be merged through the pull request process.
Once merged, this summary will become the official documentation for the main trunk.
1. The Refactoring Process
This section explains the "why" and "how" of the refactoring journey. It's intended for co-developers interested in the technical decisions and challenges encountered.
1.1. Motivation and Intention
The primary goal was to improve the modularity, readability, and maintainability of the laghost.cpp executable. The original main function was monolithic, containing all logic for setup, execution, and cleanup in a single, long block of code. This made it difficult to navigate and modify without risking unintended side effects.
The intended final structure was to separate these concerns into a clean, high-level workflow, driven by three distinct functions:
This separation makes the program's lifecycle explicit and helps future developers quickly locate the relevant code for their modifications.
1.2. Challenges & Stepwise Evolution
A direct, single-step refactoring proved impossible due to the complexity of the code and subtle C++ object lifetime rules. The process evolved iteratively to solve a series of compilation and runtime errors, which were diagnosed with the help of GDB.
1.3. Final Design and Key Decisions
The successful final architecture is a direct result of solving the challenges above through systematic debugging:
2. Guide to the Refactored Code
This section serves as a guide for developers using or modifying the new code structure.
2.1. High-Level Architecture
The program's execution flow is now controlled by three clear, distinct phases, driven by the main function:
2.2. Component Breakdown
main()AppStatestructinitialize(AppState& appState, ...)run(AppState& appState)finalize(AppState& appState)Beta Was this translation helpful? Give feedback.
All reactions