Skip to content

Commit 3d0d843

Browse files
sbryngelsonclaude
andcommitted
Add auto-generated architecture page with module map
- Add docs/documentation/architecture.md.in template with hand-written sections (pipeline, data structures, simulation loop, MPI, extending) - Add docs/gen_architecture.py to generate module map from source briefs - Add docs/module_categories.json as single source of truth for groupings - Add linter check: warns when a module is missing from categories JSON - Update contributing guide with step to register new modules - Link architecture page from User Guide sidebar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ce68fe commit 3d0d843

13 files changed

Lines changed: 378 additions & 187 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ docs/documentation/*-example.png
4545
docs/documentation/examples.md
4646
docs/documentation/case_constraints.md
4747
docs/documentation/physics_constraints.md
48+
docs/documentation/architecture.md
49+
docs/pre_process/readme.md
50+
docs/simulation/readme.md
51+
docs/post_process/readme.md
52+
docs/api/readme.md
4853

4954
examples/*batch/*/
5055
examples/**/D/*

CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,24 @@ if (MFC_DOCUMENTATION)
848848
add_dependencies(simulation_doxygen fix_file_briefs)
849849
add_dependencies(post_process_doxygen fix_file_briefs)
850850

851+
# Generate architecture.md from template + module_categories.json + source briefs.
852+
add_custom_command(
853+
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/gen-architecture.stamp"
854+
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/gen_architecture.py"
855+
"${CMAKE_CURRENT_SOURCE_DIR}/docs/module_categories.json"
856+
"${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/architecture.md.in"
857+
${pre_process_FPPs} ${pre_process_F90s}
858+
${simulation_FPPs} ${simulation_F90s}
859+
${post_process_FPPs} ${post_process_F90s}
860+
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/docs/gen_architecture.py"
861+
"${CMAKE_CURRENT_SOURCE_DIR}"
862+
COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_CURRENT_BINARY_DIR}/gen-architecture.stamp"
863+
COMMENT "Generating architecture page"
864+
VERBATIM
865+
)
866+
add_custom_target(gen_architecture DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/gen-architecture.stamp")
867+
add_dependencies(documentation_doxygen gen_architecture)
868+
851869
# Inject per-page last-updated dates into documentation markdown files.
852870
# Runs after auto-generated .md files exist, before Doxygen processes them.
853871
# Uses a stamp file so it only runs once per build.

docs/api/readme.md

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Code Architecture {#architecture}
2+
3+
This page explains how MFC's source code is organized, how data flows through the solver, and where to find things. Read this before diving into the source.
4+
5+
## Three-Phase Pipeline
6+
7+
MFC runs as three separate executables that communicate via binary files on disk:
8+
9+
```
10+
pre_process ──> simulation ──> post_process
11+
(grid + (time (derived
12+
initial advance) quantities +
13+
conditions) visualization)
14+
```
15+
16+
| Phase | Entry Point | What It Does |
17+
|-------|-------------|--------------|
18+
| **Pre-Process** | `src/pre_process/p_main.f90` | Generates the computational grid and initial conditions from patch definitions. Writes binary grid and state files. |
19+
| **Simulation** | `src/simulation/p_main.fpp` | Reads the initial state and advances the governing equations in time. Periodically writes solution snapshots. |
20+
| **Post-Process** | `src/post_process/p_main.fpp` | Reads snapshots, computes derived quantities (vorticity, Schlieren, etc.), and writes Silo/HDF5 files for VisIt or ParaView. |
21+
22+
Each phase is an independent MPI program. The simulation phase is where nearly all compute time is spent.
23+
24+
## Directory Layout
25+
26+
```
27+
src/
28+
common/ Shared modules used by all three phases
29+
pre_process/ Pre-process source (grid generation, patch construction)
30+
simulation/ Simulation source (solver core, physics models)
31+
post_process/ Post-process source (derived quantities, formatted I/O)
32+
```
33+
34+
Shared modules in `src/common/` include MPI communication, derived types, variable conversion, and utility functions. They are compiled into each phase.
35+
36+
## Key Data Structures
37+
38+
Two arrays carry the solution through the entire simulation:
39+
40+
| Variable | Contents | When Used |
41+
|----------|----------|-----------|
42+
| `q_cons_vf` | **Conservative** variables: \f$\alpha\rho\f$, \f$\rho u\f$, \f$\rho v\f$, \f$\rho w\f$, \f$E\f$, \f$\alpha\f$ | Storage, time integration, I/O |
43+
| `q_prim_vf` | **Primitive** variables: \f$\rho\f$, \f$u\f$, \f$v\f$, \f$w\f$, \f$p\f$, \f$\alpha\f$ | Reconstruction, Riemann solving, physics |
44+
45+
Both are `vector_field` types (defined in `m_derived_types`), which are arrays of `scalar_field`. Each `scalar_field` wraps a 3D real array `sf(0:m, 0:n, 0:p)` representing one variable on the grid.
46+
47+
The index layout within `q_cons_vf` depends on the flow model:
48+
49+
```
50+
For model_eqns == 2 (5-equation, multi-fluid):
51+
52+
Index: 1 .. num_fluids | num_fluids+1 .. +num_vels | E_idx | adv_idx
53+
Meaning: alpha*rho_k | momentum components | energy | volume fractions
54+
```
55+
56+
Additional variables are appended for bubbles, elastic stress, magnetic fields, or chemistry species when those models are enabled. The total count is `sys_size`.
57+
58+
## The Simulation Loop
59+
60+
The simulation advances the solution through this call chain each time step:
61+
62+
```
63+
p_main (time-step loop)
64+
└─ s_perform_time_step
65+
├─ s_compute_dt [adaptive CFL time step]
66+
└─ s_tvd_rk [Runge-Kutta stages]
67+
├─ s_compute_rhs [assemble dq/dt]
68+
│ ├─ s_convert_conservative_to_primitive_variables
69+
│ ├─ s_populate_variables_buffers [MPI halo exchange]
70+
│ └─ for each direction (x, y, z):
71+
│ ├─ s_reconstruct_cell_boundary_values [WENO]
72+
│ ├─ s_riemann_solver [HLL/HLLC/HLLD]
73+
│ ├─ s_compute_advection_source_term [flux divergence]
74+
│ └─ (physics source terms: viscous, bubbles, etc.)
75+
├─ RK update: q_cons = weighted combination of stages
76+
├─ s_apply_bodyforces [if enabled]
77+
├─ s_pressure_relaxation [if 6-equation model]
78+
└─ s_ibm_correct_state [if immersed boundaries]
79+
```
80+
81+
### What happens at each stage
82+
83+
1. **Conservative → Primitive**: Convert stored `q_cons_vf` to `q_prim_vf` (density, velocity, pressure) using the equation of state. This is done by `m_variables_conversion`.
84+
85+
2. **MPI Halo Exchange**: Ghost cells at subdomain boundaries are filled by communicating with neighbor ranks. Handled by `m_mpi_proxy`.
86+
87+
3. **WENO Reconstruction** (`m_weno`): For each coordinate direction, reconstruct left and right states at cell faces from cell-average primitives using high-order weighted essentially non-oscillatory stencils.
88+
89+
4. **Riemann Solver** (`m_riemann_solvers`): At each cell face, solve the Riemann problem between left and right states to compute intercell fluxes. Available solvers: HLL, HLLC, HLLD.
90+
91+
5. **Flux Differencing** (`m_rhs`): Accumulate the RHS as \f$\partial q / \partial t = -\frac{1}{\Delta x}(F_{j+1/2} - F_{j-1/2})\f$ plus source terms (viscous stress, surface tension, bubble dynamics, body forces, etc.).
92+
93+
6. **Runge-Kutta Update** (`m_time_steppers`): Combine the RHS with the current state using TVD Runge-Kutta coefficients (1st, 2nd, or 3rd order SSP).
94+
95+
## Module Map
96+
97+
MFC has ~80 Fortran modules organized by function. Here is where to look for what:
98+
99+
<!-- MODULE_MAP -->
100+
101+
## MPI Parallelization
102+
103+
The computational domain is decomposed into subdomains via `MPI_Cart_create`. Each rank owns a contiguous block of cells in (x, y, z). Ghost cells of width `buff_size` surround each subdomain and are filled by halo exchange before each RHS evaluation.
104+
105+
On GPUs, the same domain decomposition applies. GPU kernels operate on the local subdomain, with explicit host-device transfers for MPI communication (unless GPU-aware MPI / RDMA is available).
106+
107+
## Adding New Physics
108+
109+
To add a new source term or physics model:
110+
111+
1. **Create a module** in `src/simulation/` (e.g., `m_my_model.fpp`)
112+
2. **Add initialization/finalization** subroutines called from `m_start_up`
113+
3. **Add RHS contributions** called from the dimensional loop in `m_rhs:s_compute_rhs`
114+
4. **Add parameters** to `m_global_parameters` and input validation to `m_checker`
115+
5. **Add a module-level brief** (enforced by the linter in `lint_docs.py`)
116+
6. **Add the module to `docs/module_categories.json`** so it appears in this page
117+
118+
Follow the pattern of existing modules like `m_body_forces` (simple) or `m_viscous` (more involved) as a template.

docs/documentation/contributing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,10 @@ contains
536536
end module m_my_feature
537537
```
538538

539+
**Step 3: Register the module in the architecture docs**
540+
541+
Add your module name to the appropriate category in `docs/module_categories.json`. This ensures it appears on the @ref architecture "Code Architecture" page. The precheck linter will fail if a module is missing from this file.
542+
539543
Key conventions:
540544
- `private` by default, explicitly `public` for the module API
541545
- Initialize/finalize subroutines for allocation lifecycle

docs/documentation/readme.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Welcome to the Multi-component Flow Code (MFC) documentation.
2424

2525
## Advanced Topics
2626

27+
- @ref architecture "Code Architecture" - How the source code is organized, data flow, and module map
2728
- @ref expectedPerformance "Performance" - Optimization and benchmarks
2829
- @ref gpuParallelization "GPU Parallelization" - GPU macro API (developer reference)
2930
- @ref docker "Containers" - Docker usage
@@ -32,9 +33,7 @@ Welcome to the Multi-component Flow Code (MFC) documentation.
3233
## Development
3334

3435
- @ref contributing "Contributing" - Developer guide and coding standards
35-
- [Pre-Process API](../pre_process/index.html) - Source code reference for mesh generation and initial conditions
36-
- [Simulation API](../simulation/index.html) - Source code reference for the flow solver
37-
- [Post-Process API](../post_process/index.html) - Source code reference for data extraction and visualization
36+
- [API Documentation](../api/index.html) - Source code reference for all three components
3837

3938
## About
4039

docs/gen_architecture.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
"""Generate architecture.md from template + auto-generated module map.
3+
4+
Usage: python3 gen_architecture.py [source_dir]
5+
source_dir defaults to current directory.
6+
7+
Reads docs/documentation/architecture.md.in as a template, generates the
8+
module map section from docs/module_categories.json and source file briefs,
9+
and writes the final docs/documentation/architecture.md.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import json
15+
import re
16+
import sys
17+
from pathlib import Path
18+
19+
src_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
20+
21+
_BRIEF_RE = re.compile(r"^!>\s*@brief\s+(.+)", re.IGNORECASE)
22+
23+
24+
def _extract_brief(path: Path) -> str:
25+
"""Extract the module-level @brief from a Fortran source file."""
26+
lines = path.read_text(encoding="utf-8").splitlines()
27+
parts: list[str] = []
28+
in_file_block = False
29+
collecting = False
30+
for line in lines[:40]:
31+
stripped = line.strip()
32+
if stripped.startswith("!>") and not stripped.startswith("!> @brief"):
33+
in_file_block = True
34+
continue
35+
if in_file_block and stripped.startswith("!!"):
36+
continue
37+
if in_file_block:
38+
in_file_block = False
39+
m = _BRIEF_RE.match(stripped)
40+
if m and not collecting:
41+
brief_text = m.group(1).strip()
42+
if re.match(r"Contains\s+(module|program)\s+\w+", brief_text):
43+
continue
44+
parts.append(brief_text)
45+
collecting = True
46+
continue
47+
if collecting:
48+
if stripped.startswith("!!"):
49+
parts.append(stripped.lstrip("! ").strip())
50+
else:
51+
break
52+
if not parts:
53+
return ""
54+
text = " ".join(parts)
55+
dot = text.find(". ")
56+
if dot != -1:
57+
text = text[:dot]
58+
return text.rstrip(". ").strip()
59+
60+
61+
def _find_module_file(name: str) -> Path | None:
62+
"""Find the source file for a module name across all src/ directories."""
63+
for subdir in ("common", "pre_process", "simulation", "post_process"):
64+
for ext in (".fpp", ".f90", ".F90"):
65+
path = src_dir / "src" / subdir / (name + ext)
66+
if path.exists():
67+
return path
68+
return None
69+
70+
71+
def generate_module_map() -> str:
72+
"""Generate the module map markdown from categories + source briefs."""
73+
categories_file = src_dir / "docs" / "module_categories.json"
74+
categories = json.loads(categories_file.read_text(encoding="utf-8"))
75+
76+
lines: list[str] = []
77+
for entry in categories:
78+
cat = entry["category"]
79+
modules = entry["modules"]
80+
81+
lines.append(f"### {cat}")
82+
lines.append("| Module | Role |")
83+
lines.append("|--------|------|")
84+
85+
for mod in modules:
86+
path = _find_module_file(mod)
87+
brief = _extract_brief(path) if path else ""
88+
lines.append(f"| `{mod}` | {brief} |")
89+
90+
lines.append("")
91+
92+
return "\n".join(lines)
93+
94+
95+
def main() -> None:
96+
template = src_dir / "docs" / "documentation" / "architecture.md.in"
97+
output = src_dir / "docs" / "documentation" / "architecture.md"
98+
99+
text = template.read_text(encoding="utf-8")
100+
module_map = generate_module_map()
101+
text = text.replace("<!-- MODULE_MAP -->", module_map)
102+
103+
output.write_text(text, encoding="utf-8")
104+
print(f"Generated {output}")
105+
106+
107+
if __name__ == "__main__":
108+
main()

docs/index.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,6 @@
192192
<i class="pr-4 fa-solid fa-book"></i>
193193
<span class="flex-1">Documentation</span>
194194
</a>
195-
<a class="px-4 flex flex-row items-center py-4 border-b-2 hover:border-amber-400" href="simulation/index.html">
196-
<i class="pr-4 fa-solid fa-code"></i>
197-
<span class="flex-1">API</span>
198-
</a>
199195
<a class="px-4 flex flex-row items-center py-4 border-b-2 hover:border-amber-400" href="documentation/papers.html">
200196
<i class="pr-4 fa-solid fa-newspaper"></i>
201197
<span class="flex-1">Papers</span>

0 commit comments

Comments
 (0)