From b1002b7a994d807e4eca796c2380330284c08a43 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 19:11:10 -0500 Subject: [PATCH 01/12] Add dimensional analysis documentation to equations reference Add Section 1b to equations.md documenting how MFC handles dimensions: unit-agnostic flow solver, non-dimensional bubble dynamics, reference scales, non-dimensionalization formulas for all bub_pp parameters, the two different viscosity parameters (fluid_pp%Re vs bub_pp%mu_l), and a worked example. Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 141 ++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index efa881f570..4ca000f981 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -29,6 +29,147 @@ The parameter `model_eqns` (1, 2, 3, or 4) selects the governing equation set. --- +## 1b. Units, Dimensions, and Non-Dimensionalization + +### Dimensional Handling in the Flow Solver + +The main flow solver (Navier-Stokes equations, Riemann solvers, viscous stress, body forces, surface tension, etc.) is **unit-agnostic**: whatever units the user provides for the initial and boundary conditions, the solver preserves them throughout the computation. If the user inputs SI units, the outputs are in SI units. If the user inputs CGS, the outputs are in CGS. No internal non-dimensionalization is performed by the flow solver. + +This means that for simulations **without** sub-grid bubble models, the user can work in any consistent unit system without additional effort. + +### Non-Dimensional Bubble Dynamics + +The sub-grid bubble models (`bubbles_euler = .true.` or `bubbles_lagrange = .true.`) solve the bubble wall dynamics in **non-dimensional form**. The bubble wall pressure equation as implemented is: + +\f[p_{bw} = \left(\text{Ca} + \frac{2}{\text{We}_b\,R_0}\right)\left(\frac{R_0}{R}\right)^{3\gamma} - \text{Ca} - \frac{4\,\text{Re}_{\text{inv}}\,\dot{R}}{R} - \frac{2}{R\,\text{We}_b}\f] + +where the dimensionless groups are: + +| Dimensionless group | Definition | Code variable | Computed from | +|---|---|---|---| +| \f$\text{Ca}\f$ (Cavitation number) | \f$p_{0,\text{ref}} - p_v\f$ | `Ca` | `bub_pp%p0ref - bub_pp%pv` | +| \f$\text{Eu}\f$ (Euler number) | \f$p_{0,\text{ref}}\f$ | `Eu` | `bub_pp%p0ref` | +| \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%ss` | +| \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%mu_l` | + +These groups are computed in `src/common/m_helper.fpp` (lines 143--194). Because the bubble equations use these dimensionless numbers directly, **all** `bub_pp%` parameters must be provided in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these inputs. + +### Reference Scales + +When using bubble models, the user must choose reference scales and non-dimensionalize **all** inputs (flow and bubble) consistently. The standard convention used in the MFC examples is: + +| Reference quantity | Symbol | Typical choice | +|---|---|---| +| Length | \f$x_0\f$ | \f$R_{0,\text{ref}}\f$ (reference bubble radius) | +| Pressure | \f$p_0\f$ | \f$p_{0,\text{ref}}\f$ (reference bubble pressure) | +| Density | \f$\rho_0\f$ | \f$\rho_{0,\text{ref}}\f$ (reference liquid density) | +| Velocity | \f$u_0\f$ | \f$\sqrt{p_0 / \rho_0}\f$ (derived) | +| Time | \f$t_0\f$ | \f$x_0 / u_0\f$ (derived) | +| Temperature | \f$T_0\f$ | \f$T_{0,\text{ref}}\f$ (reference temperature) | + +### Non-Dimensionalization of Input Parameters + +The following table lists every `bub_pp%` parameter and its required non-dimensionalization: + +| Parameter | Physical meaning | Non-dimensional form | +|---|---|---| +| `bub_pp%R0ref` | Reference bubble radius | \f$R_{0,\text{ref}} / x_0\f$ | +| `bub_pp%p0ref` | Reference bubble pressure | \f$p_{0,\text{ref}} / p_0\f$ | +| `bub_pp%rho0ref` | Reference liquid density | \f$\rho_{0,\text{ref}} / \rho_0\f$ | +| `bub_pp%T0ref` | Reference temperature | \f$T_{0,\text{ref}} / T_0\f$ (typically 1) | +| `bub_pp%ss` | Surface tension \f$\sigma\f$ | \f$\sigma / (\rho_0\,x_0\,u_0^2)\f$ | +| `bub_pp%pv` | Vapor pressure | \f$p_v / p_0\f$ | +| `bub_pp%mu_l` | Liquid dynamic viscosity | \f$\mu_l / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%mu_v` | Vapor dynamic viscosity | \f$\mu_v / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%mu_g` | Gas dynamic viscosity | \f$\mu_g / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%k_v` | Vapor thermal conductivity | \f$k_v\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | +| `bub_pp%k_g` | Gas thermal conductivity | \f$k_g\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | +| `bub_pp%cp_v` | Vapor specific heat | \f$c_{p,v}\,T_0 / u_0^2\f$ | +| `bub_pp%cp_g` | Gas specific heat | \f$c_{p,g}\,T_0 / u_0^2\f$ | +| `bub_pp%R_v` | Vapor gas constant | \f$R_v\,T_0 / u_0^2\f$ | +| `bub_pp%R_g` | Gas gas constant | \f$R_g\,T_0 / u_0^2\f$ | +| `bub_pp%gam_v` | Vapor heat capacity ratio | Already dimensionless (no scaling) | +| `bub_pp%gam_g` | Gas heat capacity ratio | Already dimensionless (no scaling) | +| `bub_pp%M_v` | Vapor molar mass | Already dimensionless (no scaling) | +| `bub_pp%M_g` | Gas molar mass | Already dimensionless (no scaling) | + +When the reference scales match the bubble reference values (e.g., \f$x_0 = R_{0,\text{ref}}\f$, \f$p_0 = p_{0,\text{ref}}\f$, \f$\rho_0 = \rho_{0,\text{ref}}\f$), the reference parameters simplify to unity: `bub_pp%R0ref = 1`, `bub_pp%p0ref = 1`, `bub_pp%rho0ref = 1`. + +### Flow Parameters with Bubbles + +When bubbles are enabled, the flow-level parameters must also be non-dimensionalized with the same reference scales: + +| Parameter | Non-dimensional form | +|---|---| +| `x_domain%beg`, `x_domain%end` | Domain bounds divided by \f$x_0\f$ | +| `patch_icpp(i)%pres` | Pressure divided by \f$p_0\f$ | +| `patch_icpp(i)%alpha_rho(j)` | Partial density divided by \f$\rho_0\f$ | +| `patch_icpp(i)%vel(j)` | Velocity divided by \f$u_0\f$ | +| `fluid_pp(i)%gamma` | \f$1/(\gamma_i - 1)\f$ (dimensionless, same as without bubbles) | +| `fluid_pp(i)%pi_inf` | \f$\gamma_i\,\pi_{\infty,i} / [(\gamma_i - 1)\,p_0]\f$ (scaled by reference pressure) | +| `fluid_pp(i)%Re(1)` | \f$\rho_0\,x_0\,u_0 / \mu_i\f$ (Reynolds number, inverse viscosity) | +| `dt` | Time step divided by \f$t_0\f$ | + +### Two Different Viscosity Parameters + +MFC has two conceptually distinct viscosity-related parameters that serve different physical roles: + +1. **`fluid_pp(i)%Re(1)`** — Used for the **macroscopic flow viscous stress tensor** (Navier-Stokes equations). This is \f$1/\mu\f$ in dimensional simulations, or \f$\rho_0 x_0 u_0 / \mu\f$ (a Reynolds number) when non-dimensionalized. It appears as a **divisor** in the viscous stress computation: + \f[\tau_{ij} \propto \frac{\nabla u}{\text{Re}}\f] + Stored in the `physical_parameters` derived type (`src/common/m_derived_types.fpp`). + +2. **`bub_pp%mu_l`** — Used for **microscale bubble wall viscous damping** (Rayleigh-Plesset / Keller-Miksis equations). This is the non-dimensional liquid viscosity \f$\mu_l / (\rho_0 x_0 u_0)\f$. It appears as a **multiplier** in the bubble wall pressure: + \f[p_{bw} \ni -\frac{4\,\text{Re}_{\text{inv}}\,\dot{R}}{R}\f] + Stored in the `subgrid_bubble_physical_parameters` derived type (`src/common/m_derived_types.fpp`). + +These two parameters represent viscous effects at fundamentally different scales — bulk flow dissipation vs. single-bubble-wall damping — and are stored in separate derived types with separate code paths. They are **not** interchangeable: `fluid_pp%Re(1)` is an inverse viscosity while `bub_pp%mu_l` is a viscosity (non-dimensionalized). + +### Example: Non-Dimensionalizing a Bubble Case + +A typical bubble case setup in `case.py` follows this pattern: + +```python +import math + +# Physical properties (SI units) +rho_l = 1.0e03 # liquid density [kg/m³] +mu_l = 1.002e-03 # liquid viscosity [kg/(m·s)] +ss = 0.07275 # surface tension [kg/s²] +pv = 2.3388e03 # vapor pressure [Pa] +gam_l = 7.15 # liquid stiffened gas gamma +pi_inf = 306.0e06 # liquid stiffened gas pi_inf [Pa] + +# Bubble reference values (SI) +R0ref = 10.0e-06 # reference bubble radius [m] +p0ref = 112.9e03 # reference bubble pressure [Pa] +rho0ref = rho_l # reference density [kg/m³] + +# Derived reference scales +x0 = R0ref +p0 = p0ref +rho0 = rho0ref +u0 = math.sqrt(p0 / rho0) +t0 = x0 / u0 + +# Non-dimensional inputs +params = { + "bub_pp%R0ref": R0ref / x0, # = 1.0 + "bub_pp%p0ref": p0ref / p0, # = 1.0 + "bub_pp%rho0ref": rho0ref / rho0, # = 1.0 + "bub_pp%ss": ss / (rho0 * x0 * u0**2), # surface tension + "bub_pp%pv": pv / p0, # vapor pressure + "bub_pp%mu_l": mu_l / (rho0 * x0 * u0), # liquid viscosity + + "fluid_pp(1)%gamma": 1.0 / (gam_l - 1.0), + "fluid_pp(1)%pi_inf": gam_l * (pi_inf / p0) / (gam_l - 1.0), + "fluid_pp(1)%Re(1)": rho0 * x0 * u0 / mu_l, # flow Re (inverse!) +} +``` + +Note the inverse relationship: `fluid_pp%Re(1) = 1 / bub_pp%mu_l` when both use the same reference scales and the same physical viscosity. This is expected — they encode the same physical viscosity but in reciprocal forms for their respective equations. + +--- + ## 2. Governing PDEs ### 2.1 Five-Equation Model (`model_eqns = 2`) From 3a8806cbe284406203f861da7c1795face0c69ea Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 20:23:19 -0500 Subject: [PATCH 02/12] Address reviewer feedback on dimensional analysis docs - Reference subroutine name instead of brittle line numbers - Add missing bub_pp%vd (vapor diffusivity) to parameter table - Clarify M_v/M_g are not dimensionless but only used in ratios Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index 4ca000f981..517b137393 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -52,7 +52,7 @@ where the dimensionless groups are: | \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%ss` | | \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%mu_l` | -These groups are computed in `src/common/m_helper.fpp` (lines 143--194). Because the bubble equations use these dimensionless numbers directly, **all** `bub_pp%` parameters must be provided in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these inputs. +These groups are computed during bubble-parameter initialization in `src/common/m_helper.fpp` (see subroutine `s_initialize_bubble_vars`). Because the bubble equations use these dimensionless numbers directly, all dimensional bubble inputs (pressures, viscosities, surface tension, etc.) in `bub_pp%` must be supplied in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these quantities. ### Reference Scales @@ -82,6 +82,7 @@ The following table lists every `bub_pp%` parameter and its required non-dimensi | `bub_pp%mu_l` | Liquid dynamic viscosity | \f$\mu_l / (\rho_0\,x_0\,u_0)\f$ | | `bub_pp%mu_v` | Vapor dynamic viscosity | \f$\mu_v / (\rho_0\,x_0\,u_0)\f$ | | `bub_pp%mu_g` | Gas dynamic viscosity | \f$\mu_g / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%vd` | Vapor diffusivity | \f$D / (x_0\,u_0)\f$ | | `bub_pp%k_v` | Vapor thermal conductivity | \f$k_v\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | | `bub_pp%k_g` | Gas thermal conductivity | \f$k_g\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | | `bub_pp%cp_v` | Vapor specific heat | \f$c_{p,v}\,T_0 / u_0^2\f$ | @@ -90,8 +91,8 @@ The following table lists every `bub_pp%` parameter and its required non-dimensi | `bub_pp%R_g` | Gas gas constant | \f$R_g\,T_0 / u_0^2\f$ | | `bub_pp%gam_v` | Vapor heat capacity ratio | Already dimensionless (no scaling) | | `bub_pp%gam_g` | Gas heat capacity ratio | Already dimensionless (no scaling) | -| `bub_pp%M_v` | Vapor molar mass | Already dimensionless (no scaling) | -| `bub_pp%M_g` | Gas molar mass | Already dimensionless (no scaling) | +| `bub_pp%M_v` | Vapor molar mass | Consistent units; only ratios are used (no scaling needed) | +| `bub_pp%M_g` | Gas molar mass | Consistent units; only ratios are used (no scaling needed) | When the reference scales match the bubble reference values (e.g., \f$x_0 = R_{0,\text{ref}}\f$, \f$p_0 = p_{0,\text{ref}}\f$, \f$\rho_0 = \rho_{0,\text{ref}}\f$), the reference parameters simplify to unity: `bub_pp%R0ref = 1`, `bub_pp%p0ref = 1`, `bub_pp%rho0ref = 1`. From 357f1b5f33dc208cc86f1d003b023308d12d8260 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 20:31:53 -0500 Subject: [PATCH 03/12] Clarify non-dimensional variables and add consistency warning - Note that R, R0, Rdot in the bubble equation are non-dimensional - Add explicit warning that all inputs must use the same reference scales when bubbles are enabled Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index 517b137393..ce01fa4416 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -43,7 +43,9 @@ The sub-grid bubble models (`bubbles_euler = .true.` or `bubbles_lagrange = .tru \f[p_{bw} = \left(\text{Ca} + \frac{2}{\text{We}_b\,R_0}\right)\left(\frac{R_0}{R}\right)^{3\gamma} - \text{Ca} - \frac{4\,\text{Re}_{\text{inv}}\,\dot{R}}{R} - \frac{2}{R\,\text{We}_b}\f] -where the dimensionless groups are: +Here \f$R\f$ and \f$R_0\f$ are non-dimensional radii (scaled by \f$x_0\f$), and \f$\dot{R}\f$ is a non-dimensional wall speed (scaled by \f$u_0\f$); the entire bubble ODE is solved in non-dimensional variables. + +The dimensionless groups are: | Dimensionless group | Definition | Code variable | Computed from | |---|---|---|---| @@ -52,7 +54,7 @@ where the dimensionless groups are: | \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%ss` | | \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%mu_l` | -These groups are computed during bubble-parameter initialization in `src/common/m_helper.fpp` (see subroutine `s_initialize_bubble_vars`). Because the bubble equations use these dimensionless numbers directly, all dimensional bubble inputs (pressures, viscosities, surface tension, etc.) in `bub_pp%` must be supplied in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these quantities. +These groups are computed during bubble-parameter initialization in `src/common/m_helper.fpp` (see subroutine `s_initialize_bubble_vars`). Because the bubble equations use these dimensionless numbers directly, all dimensional bubble inputs (pressures, viscosities, surface tension, etc.) in `bub_pp%` must be supplied in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these quantities. When bubbles are enabled, **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%` values — must be scaled with the same reference quantities, or the coupled solution will be physically incorrect. ### Reference Scales From ce4dc84827e67af953948a8490130c5b91dd5183 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 21:24:25 -0500 Subject: [PATCH 04/12] Update docs/documentation/equations.md Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com> --- docs/documentation/equations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index fb3b8cb0af..69d2e7c3b9 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -54,7 +54,7 @@ The dimensionless groups are: | \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%ss` | | \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%mu_l` | -These groups are computed during bubble-parameter initialization in `src/common/m_helper.fpp` (see subroutine `s_initialize_bubble_vars`). Because the bubble equations use these dimensionless numbers directly, all dimensional bubble inputs (pressures, viscosities, surface tension, etc.) in `bub_pp%` must be supplied in non-dimensional form by the user. The code does not perform any internal non-dimensionalization of these quantities. When bubbles are enabled, **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%` values — must be scaled with the same reference quantities, or the coupled solution will be physically incorrect. +Because the bubble equations use these dimensionless numbers directly, `bub_pp%...` is interpreted by the code as **already non-dimensional**. The code does **not** non-dimensionalize bubble quantities internally. Therefore, when bubbles are enabled, the simulation must be run in a **fully non-dimensional** form: **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%` values — must be scaled with the same \f$(x_0, p_0, \rho_0, u_0, t_0, T_0)\f$ reference quantities, or the coupled solution will be physically incorrect. ### Reference Scales From 0616ade261dbcbadbd1952350ae8eae42b6fc15e Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 22:10:33 -0500 Subject: [PATCH 05/12] Document stored parameter conventions and common material values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add documentation for non-bubble users explaining that fluid_pp%gamma, pi_inf, and Re use transformed stored forms (1/(γ-1), γπ∞/(γ-1), 1/μ). Includes a common materials reference table and worked examples. Also fixes misleading EOS section that implied raw γ/π∞ are input directly. Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 54 ++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index 69d2e7c3b9..12e53787cf 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -37,6 +37,58 @@ The main flow solver (Navier-Stokes equations, Riemann solvers, viscous stress, This means that for simulations **without** sub-grid bubble models, the user can work in any consistent unit system without additional effort. +### Stored Parameter Conventions + +Several EOS and transport parameters use **transformed stored forms** that differ from the standard physical values. This is the most common source of input errors: + +| Parameter | Physical quantity | What MFC expects (stored form) | +|---|---|---| +| `fluid_pp(i)%gamma` | Heat capacity ratio \f$\gamma\f$ | \f$\Gamma = \dfrac{1}{\gamma - 1}\f$ | +| `fluid_pp(i)%pi_inf` | Stiffness pressure \f$\pi_\infty\f$ [Pa] | \f$\Pi_\infty = \dfrac{\gamma\,\pi_\infty}{\gamma - 1}\f$ [Pa] | +| `fluid_pp(i)%Re(1)` | Dynamic viscosity \f$\mu\f$ | \f$1/\mu\f$ (inverse viscosity) | +| `fluid_pp(i)%Re(2)` | Bulk viscosity \f$\mu_b\f$ | \f$1/\mu_b\f$ (inverse bulk viscosity) | + +These transformations arise because MFC internally solves the energy equation using the transformed variables \f$\Gamma\f$ and \f$\Pi_\infty\f$ (see Section 3.1), and the viscous stress is computed by dividing by `Re` rather than multiplying by \f$\mu\f$. + +**Common mistake:** setting `fluid_pp(1)%gamma = 1.4` for air. The correct value is `1.0 / (1.4 - 1.0) = 2.5`. Setting `gamma = 1.4` corresponds to a physical \f$\gamma \approx 1.71\f$, which is not a standard gas. + +### Common Material Values + +Pre-computed stored-form values for common fluids (SI units): + +| Material | \f$\gamma\f$ | \f$\pi_\infty\f$ [Pa] | `gamma` (stored) | `pi_inf` (stored) [Pa] | +|---|---|---|---|---| +| Air | 1.4 | 0 | 2.5 | 0 | +| Helium | 5/3 | 0 | 1.5 | 0 | +| Water (Tait) | 4.4 | 6.0e8 | 0.2941 | 7.76e8 | +| Water (\cite LeMetayer04) | 6.12 | 3.43e8 | 0.1953 | 4.10e8 | + +Example for an air-water simulation: + +```python +# Air (fluid 1) +gam_a = 1.4 +"fluid_pp(1)%gamma": 1.0 / (gam_a - 1.0), # = 2.5 +"fluid_pp(1)%pi_inf": 0.0, + +# Water (fluid 2) +gam_w = 4.4 +pi_w = 6.0e8 # Pa +"fluid_pp(2)%gamma": 1.0 / (gam_w - 1.0), # ≈ 0.294 +"fluid_pp(2)%pi_inf": gam_w * pi_w / (gam_w - 1.0), # ≈ 7.76e8 +``` + +For viscous cases, provide the **reciprocal** of the dynamic viscosity: + +```python +mu = 1.002e-3 # water viscosity [Pa·s] +"fluid_pp(1)%Re(1)": 1.0 / mu, # ≈ 998 +``` + +### Unit Consistency + +The solver does not check or convert units. All inputs must use the **same consistent unit system** (e.g., all SI or all CGS). Mixing units — for example, pressures in atmospheres with densities in kg/m³ — will produce silently incorrect results. + ### Non-Dimensional Bubble Dynamics The sub-grid bubble models (`bubbles_euler = .true.` or `bubbles_lagrange = .true.`) solve the bubble wall dynamics in **non-dimensional form**. The bubble wall pressure equation as implemented is: @@ -281,7 +333,7 @@ The pressure is recovered from the total energy as: \f[\frac{1}{\rho\,c^2} = \sum_k \frac{\alpha_k}{\rho_k\,c_k^2}\f] -Input parameters per fluid: `gamma` (\f$\gamma_k\f$), `pi_inf` (\f$\pi_{\infty,k}\f$), `cv` (\f$c_{v,k}\f$), `qv` (\f$q_{v,k}\f$), `qvp` (\f$q'_{v,k}\f$). +Input parameters per fluid: `gamma` (\f$\Gamma_k = 1/(\gamma_k - 1)\f$), `pi_inf` (\f$\Pi_{\infty,k} = \gamma_k\,\pi_{\infty,k}/(\gamma_k - 1)\f$), `cv` (\f$c_{v,k}\f$), `qv` (\f$q_{v,k}\f$), `qvp` (\f$q'_{v,k}\f$). Note that `gamma` and `pi_inf` are stored in transformed form, not as the raw physical values (see Section 1b). ### 3.2 Ideal Gas EOS (Chemistry, `chemistry = .true.`) From b3925a9ac84a76a444d13785197a6ada7b7142d7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 23:07:18 -0500 Subject: [PATCH 06/12] Fix Doxygen % escaping in equations.md backtick spans Doxygen treats % as "suppress auto-link" and silently eats the % character, even inside backtick code spans. All inline code references to Fortran derived-type accessors (fluid_pp%gamma, bub_pp%mu_l, etc.) must use %% to produce a literal % in the rendered output. Also add bub_pp%% to the lint_docs.py equations.md skip set since it is used as a family prefix reference, not a specific parameter. Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 92 ++++++++++++++++----------------- toolchain/mfc/lint_docs.py | 2 +- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index 12e53787cf..7b41109865 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -43,14 +43,14 @@ Several EOS and transport parameters use **transformed stored forms** that diffe | Parameter | Physical quantity | What MFC expects (stored form) | |---|---|---| -| `fluid_pp(i)%gamma` | Heat capacity ratio \f$\gamma\f$ | \f$\Gamma = \dfrac{1}{\gamma - 1}\f$ | -| `fluid_pp(i)%pi_inf` | Stiffness pressure \f$\pi_\infty\f$ [Pa] | \f$\Pi_\infty = \dfrac{\gamma\,\pi_\infty}{\gamma - 1}\f$ [Pa] | -| `fluid_pp(i)%Re(1)` | Dynamic viscosity \f$\mu\f$ | \f$1/\mu\f$ (inverse viscosity) | -| `fluid_pp(i)%Re(2)` | Bulk viscosity \f$\mu_b\f$ | \f$1/\mu_b\f$ (inverse bulk viscosity) | +| `fluid_pp(i)%%gamma` | Heat capacity ratio \f$\gamma\f$ | \f$\Gamma = \dfrac{1}{\gamma - 1}\f$ | +| `fluid_pp(i)%%pi_inf` | Stiffness pressure \f$\pi_\infty\f$ [Pa] | \f$\Pi_\infty = \dfrac{\gamma\,\pi_\infty}{\gamma - 1}\f$ [Pa] | +| `fluid_pp(i)%%Re(1)` | Dynamic viscosity \f$\mu\f$ | \f$1/\mu\f$ (inverse viscosity) | +| `fluid_pp(i)%%Re(2)` | Bulk viscosity \f$\mu_b\f$ | \f$1/\mu_b\f$ (inverse bulk viscosity) | These transformations arise because MFC internally solves the energy equation using the transformed variables \f$\Gamma\f$ and \f$\Pi_\infty\f$ (see Section 3.1), and the viscous stress is computed by dividing by `Re` rather than multiplying by \f$\mu\f$. -**Common mistake:** setting `fluid_pp(1)%gamma = 1.4` for air. The correct value is `1.0 / (1.4 - 1.0) = 2.5`. Setting `gamma = 1.4` corresponds to a physical \f$\gamma \approx 1.71\f$, which is not a standard gas. +**Common mistake:** setting `fluid_pp(1)%%gamma = 1.4` for air. The correct value is `1.0 / (1.4 - 1.0) = 2.5`. Setting `gamma = 1.4` corresponds to a physical \f$\gamma \approx 1.71\f$, which is not a standard gas. ### Common Material Values @@ -101,12 +101,12 @@ The dimensionless groups are: | Dimensionless group | Definition | Code variable | Computed from | |---|---|---|---| -| \f$\text{Ca}\f$ (Cavitation number) | \f$p_{0,\text{ref}} - p_v\f$ | `Ca` | `bub_pp%p0ref - bub_pp%pv` | -| \f$\text{Eu}\f$ (Euler number) | \f$p_{0,\text{ref}}\f$ | `Eu` | `bub_pp%p0ref` | -| \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%ss` | -| \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%mu_l` | +| \f$\text{Ca}\f$ (Cavitation number) | \f$p_{0,\text{ref}} - p_v\f$ | `Ca` | `bub_pp%%p0ref - bub_pp%%pv` | +| \f$\text{Eu}\f$ (Euler number) | \f$p_{0,\text{ref}}\f$ | `Eu` | `bub_pp%%p0ref` | +| \f$\text{We}_b\f$ (bubble Weber number) | \f$1/\sigma\f$ | `Web` | `1 / bub_pp%%ss` | +| \f$\text{Re}_{\text{inv}}\f$ (inverse bubble Reynolds number) | \f$\mu_l\f$ | `Re_inv` | `bub_pp%%mu_l` | -Because the bubble equations use these dimensionless numbers directly, `bub_pp%...` is interpreted by the code as **already non-dimensional**. The code does **not** non-dimensionalize bubble quantities internally. Therefore, when bubbles are enabled, the simulation must be run in a **fully non-dimensional** form: **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%` values — must be scaled with the same \f$(x_0, p_0, \rho_0, u_0, t_0, T_0)\f$ reference quantities, or the coupled solution will be physically incorrect. +Because the bubble equations use these dimensionless numbers directly, all `bub_pp%%` inputs are interpreted by the code as **already non-dimensional**. The code does **not** non-dimensionalize bubble quantities internally. Therefore, when bubbles are enabled, the simulation must be run in a **fully non-dimensional** form: **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%%` values — must be scaled with the same \f$(x_0, p_0, \rho_0, u_0, t_0, T_0)\f$ reference quantities, or the coupled solution will be physically incorrect. ### Reference Scales @@ -123,32 +123,32 @@ When using bubble models, the user must choose reference scales and non-dimensio ### Non-Dimensionalization of Input Parameters -The following table lists every `bub_pp%` parameter and its required non-dimensionalization: +The following table lists every `bub_pp%%` parameter and its required non-dimensionalization: | Parameter | Physical meaning | Non-dimensional form | |---|---|---| -| `bub_pp%R0ref` | Reference bubble radius | \f$R_{0,\text{ref}} / x_0\f$ | -| `bub_pp%p0ref` | Reference bubble pressure | \f$p_{0,\text{ref}} / p_0\f$ | -| `bub_pp%rho0ref` | Reference liquid density | \f$\rho_{0,\text{ref}} / \rho_0\f$ | -| `bub_pp%T0ref` | Reference temperature | \f$T_{0,\text{ref}} / T_0\f$ (typically 1) | -| `bub_pp%ss` | Surface tension \f$\sigma\f$ | \f$\sigma / (\rho_0\,x_0\,u_0^2)\f$ | -| `bub_pp%pv` | Vapor pressure | \f$p_v / p_0\f$ | -| `bub_pp%mu_l` | Liquid dynamic viscosity | \f$\mu_l / (\rho_0\,x_0\,u_0)\f$ | -| `bub_pp%mu_v` | Vapor dynamic viscosity | \f$\mu_v / (\rho_0\,x_0\,u_0)\f$ | -| `bub_pp%mu_g` | Gas dynamic viscosity | \f$\mu_g / (\rho_0\,x_0\,u_0)\f$ | -| `bub_pp%vd` | Vapor diffusivity | \f$D / (x_0\,u_0)\f$ | -| `bub_pp%k_v` | Vapor thermal conductivity | \f$k_v\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | -| `bub_pp%k_g` | Gas thermal conductivity | \f$k_g\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | -| `bub_pp%cp_v` | Vapor specific heat | \f$c_{p,v}\,T_0 / u_0^2\f$ | -| `bub_pp%cp_g` | Gas specific heat | \f$c_{p,g}\,T_0 / u_0^2\f$ | -| `bub_pp%R_v` | Vapor gas constant | \f$R_v\,T_0 / u_0^2\f$ | -| `bub_pp%R_g` | Gas gas constant | \f$R_g\,T_0 / u_0^2\f$ | -| `bub_pp%gam_v` | Vapor heat capacity ratio | Already dimensionless (no scaling) | -| `bub_pp%gam_g` | Gas heat capacity ratio | Already dimensionless (no scaling) | -| `bub_pp%M_v` | Vapor molar mass | Consistent units; only ratios are used (no scaling needed) | -| `bub_pp%M_g` | Gas molar mass | Consistent units; only ratios are used (no scaling needed) | - -When the reference scales match the bubble reference values (e.g., \f$x_0 = R_{0,\text{ref}}\f$, \f$p_0 = p_{0,\text{ref}}\f$, \f$\rho_0 = \rho_{0,\text{ref}}\f$), the reference parameters simplify to unity: `bub_pp%R0ref = 1`, `bub_pp%p0ref = 1`, `bub_pp%rho0ref = 1`. +| `bub_pp%%R0ref` | Reference bubble radius | \f$R_{0,\text{ref}} / x_0\f$ | +| `bub_pp%%p0ref` | Reference bubble pressure | \f$p_{0,\text{ref}} / p_0\f$ | +| `bub_pp%%rho0ref` | Reference liquid density | \f$\rho_{0,\text{ref}} / \rho_0\f$ | +| `bub_pp%%T0ref` | Reference temperature | \f$T_{0,\text{ref}} / T_0\f$ (typically 1) | +| `bub_pp%%ss` | Surface tension \f$\sigma\f$ | \f$\sigma / (\rho_0\,x_0\,u_0^2)\f$ | +| `bub_pp%%pv` | Vapor pressure | \f$p_v / p_0\f$ | +| `bub_pp%%mu_l` | Liquid dynamic viscosity | \f$\mu_l / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%%mu_v` | Vapor dynamic viscosity | \f$\mu_v / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%%mu_g` | Gas dynamic viscosity | \f$\mu_g / (\rho_0\,x_0\,u_0)\f$ | +| `bub_pp%%vd` | Vapor diffusivity | \f$D / (x_0\,u_0)\f$ | +| `bub_pp%%k_v` | Vapor thermal conductivity | \f$k_v\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | +| `bub_pp%%k_g` | Gas thermal conductivity | \f$k_g\,T_0 / (x_0\,\rho_0\,u_0^3)\f$ | +| `bub_pp%%cp_v` | Vapor specific heat | \f$c_{p,v}\,T_0 / u_0^2\f$ | +| `bub_pp%%cp_g` | Gas specific heat | \f$c_{p,g}\,T_0 / u_0^2\f$ | +| `bub_pp%%R_v` | Vapor gas constant | \f$R_v\,T_0 / u_0^2\f$ | +| `bub_pp%%R_g` | Gas gas constant | \f$R_g\,T_0 / u_0^2\f$ | +| `bub_pp%%gam_v` | Vapor heat capacity ratio | Already dimensionless (no scaling) | +| `bub_pp%%gam_g` | Gas heat capacity ratio | Already dimensionless (no scaling) | +| `bub_pp%%M_v` | Vapor molar mass | Consistent units; only ratios are used (no scaling needed) | +| `bub_pp%%M_g` | Gas molar mass | Consistent units; only ratios are used (no scaling needed) | + +When the reference scales match the bubble reference values (e.g., \f$x_0 = R_{0,\text{ref}}\f$, \f$p_0 = p_{0,\text{ref}}\f$, \f$\rho_0 = \rho_{0,\text{ref}}\f$), the reference parameters simplify to unity: `bub_pp%%R0ref = 1`, `bub_pp%%p0ref = 1`, `bub_pp%%rho0ref = 1`. ### Flow Parameters with Bubbles @@ -156,28 +156,28 @@ When bubbles are enabled, the flow-level parameters must also be non-dimensional | Parameter | Non-dimensional form | |---|---| -| `x_domain%beg`, `x_domain%end` | Domain bounds divided by \f$x_0\f$ | -| `patch_icpp(i)%pres` | Pressure divided by \f$p_0\f$ | -| `patch_icpp(i)%alpha_rho(j)` | Partial density divided by \f$\rho_0\f$ | -| `patch_icpp(i)%vel(j)` | Velocity divided by \f$u_0\f$ | -| `fluid_pp(i)%gamma` | \f$1/(\gamma_i - 1)\f$ (dimensionless, same as without bubbles) | -| `fluid_pp(i)%pi_inf` | \f$\gamma_i\,\pi_{\infty,i} / [(\gamma_i - 1)\,p_0]\f$ (scaled by reference pressure) | -| `fluid_pp(i)%Re(1)` | \f$\rho_0\,x_0\,u_0 / \mu_i\f$ (Reynolds number, inverse viscosity) | +| `x_domain%%beg`, `x_domain%%end` | Domain bounds divided by \f$x_0\f$ | +| `patch_icpp(i)%%pres` | Pressure divided by \f$p_0\f$ | +| `patch_icpp(i)%%alpha_rho(j)` | Partial density divided by \f$\rho_0\f$ | +| `patch_icpp(i)%%vel(j)` | Velocity divided by \f$u_0\f$ | +| `fluid_pp(i)%%gamma` | \f$1/(\gamma_i - 1)\f$ (dimensionless, same as without bubbles) | +| `fluid_pp(i)%%pi_inf` | \f$\gamma_i\,\pi_{\infty,i} / [(\gamma_i - 1)\,p_0]\f$ (scaled by reference pressure) | +| `fluid_pp(i)%%Re(1)` | \f$\rho_0\,x_0\,u_0 / \mu_i\f$ (Reynolds number, inverse viscosity) | | `dt` | Time step divided by \f$t_0\f$ | ### Two Different Viscosity Parameters MFC has two conceptually distinct viscosity-related parameters that serve different physical roles: -1. **`fluid_pp(i)%Re(1)`** — Used for the **macroscopic flow viscous stress tensor** (Navier-Stokes equations). This is \f$1/\mu\f$ in dimensional simulations, or \f$\rho_0 x_0 u_0 / \mu\f$ (a Reynolds number) when non-dimensionalized. It appears as a **divisor** in the viscous stress computation: +1. **`fluid_pp(i)%%Re(1)`** — Used for the **macroscopic flow viscous stress tensor** (Navier-Stokes equations). This is \f$1/\mu\f$ in dimensional simulations, or \f$\rho_0 x_0 u_0 / \mu\f$ (a Reynolds number) when non-dimensionalized. It appears as a **divisor** in the viscous stress computation: \f[\tau_{ij} \propto \frac{\nabla u}{\text{Re}}\f] - Stored in the `physical_parameters` derived type (`src/common/m_derived_types.fpp`). + Stored in the physical\_parameters derived type (`src/common/m_derived_types.fpp`). -2. **`bub_pp%mu_l`** — Used for **microscale bubble wall viscous damping** (Rayleigh-Plesset / Keller-Miksis equations). This is the non-dimensional liquid viscosity \f$\mu_l / (\rho_0 x_0 u_0)\f$. It appears as a **multiplier** in the bubble wall pressure: +2. **`bub_pp%%mu_l`** — Used for **microscale bubble wall viscous damping** (Rayleigh-Plesset / Keller-Miksis equations). This is the non-dimensional liquid viscosity \f$\mu_l / (\rho_0 x_0 u_0)\f$. It appears as a **multiplier** in the bubble wall pressure: \f[p_{bw} \ni -\frac{4\,\text{Re}_{\text{inv}}\,\dot{R}}{R}\f] - Stored in the `subgrid_bubble_physical_parameters` derived type (`src/common/m_derived_types.fpp`). + Stored in the subgrid\_bubble\_physical\_parameters derived type (`src/common/m_derived_types.fpp`). -These two parameters represent viscous effects at fundamentally different scales — bulk flow dissipation vs. single-bubble-wall damping — and are stored in separate derived types with separate code paths. They are **not** interchangeable: `fluid_pp%Re(1)` is an inverse viscosity while `bub_pp%mu_l` is a viscosity (non-dimensionalized). +These two parameters represent viscous effects at fundamentally different scales — bulk flow dissipation vs. single-bubble-wall damping — and are stored in separate derived types with separate code paths. They are **not** interchangeable: `fluid_pp%%Re(1)` is an inverse viscosity while `bub_pp%%mu_l` is a viscosity (non-dimensionalized). ### Example: Non-Dimensionalizing a Bubble Case @@ -221,7 +221,7 @@ params = { } ``` -Note the inverse relationship: `fluid_pp%Re(1) = 1 / bub_pp%mu_l` when both use the same reference scales and the same physical viscosity. This is expected — they encode the same physical viscosity but in reciprocal forms for their respective equations. +Note the inverse relationship: `fluid_pp%%Re(1) = 1 / bub_pp%%mu_l` when both use the same reference scales and the same physical viscosity. This is expected — they encode the same physical viscosity but in reciprocal forms for their respective equations. --- diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 6f2f2b6b0b..b019fff4ba 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -55,7 +55,7 @@ # Docs to check for parameter references, with per-file skip sets PARAM_DOCS = { - "docs/documentation/equations.md": set(), + "docs/documentation/equations.md": {"bub_pp%%"}, "docs/documentation/case.md": CASE_MD_SKIP, } From 00c24858b94e326d6cba1692e54de2f606bf82f2 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 23:08:20 -0500 Subject: [PATCH 07/12] Add doc lint check to CI lint-gate The precheck script runs lint_docs.py (step 5/5) but CI's lint-gate job was missing it. This meant Doxygen % escaping issues and other doc reference errors were not caught in CI. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0be51076ec..ec964794ff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,6 +49,9 @@ jobs: run: | ! grep -iR -e '\.\.\.' -e '\-\-\-' -e '===' ./src/* + - name: Lint Docs + run: python3 toolchain/mfc/lint_docs.py + file-changes: name: Detect File Changes runs-on: 'ubuntu-latest' From 42b738efd910636041d83ef3574681c0f72b8fc4 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 23:14:17 -0500 Subject: [PATCH 08/12] Strengthen doc linter with new Doxygen rendering checks Add three new checks to lint_docs.py: - check_unpaired_math: catches unpaired \f$ delimiters and unbalanced \f[/\f] display math blocks that break all subsequent rendering - check_doxygen_commands_in_backticks: catches Doxygen @ and \ block commands inside backtick code spans (known Doxygen bug #6054) - check_single_quote_in_backtick: catches single quotes in single- backtick spans which Doxygen treats as ending the span Fix 7 pre-existing single-quote issues found by the new check in case.md, contributing.md, and testing.md (use double backticks). Co-Authored-By: Claude Opus 4.6 --- docs/documentation/case.md | 8 +- docs/documentation/contributing.md | 2 +- docs/documentation/testing.md | 2 +- toolchain/mfc/lint_docs.py | 162 +++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 6 deletions(-) diff --git a/docs/documentation/case.md b/docs/documentation/case.md index c10fb411cc..d34847ac01 100644 --- a/docs/documentation/case.md +++ b/docs/documentation/case.md @@ -298,7 +298,7 @@ These physical parameters must be consistent with fluid material's parameters de #### Elliptic Smoothing Initial conditions in which not all patches support the `patch_icpp(j)%%smoothen` parameter can still be smoothed by applying iterations of the heat equation to the initial condition. -This is enabled by adding `'elliptic_smoothing': "T",` and `'elliptic_smoothing_iters': N,` to the case dictionary, where `N` is the number of smoothing iterations to apply. +This is enabled by adding ``'elliptic_smoothing': "T",`` and ``'elliptic_smoothing_iters': N,`` to the case dictionary, where `N` is the number of smoothing iterations to apply. ### 4. Immersed Boundary Patches {#sec-immersed-boundary-patches} @@ -658,7 +658,7 @@ If `file_per_process` is true, then pre_process, simulation, and post_process mu - `cons_vars_wrt` and `prim_vars_wrt` activate the output of conservative and primitive state variables into the database. -- `[variable's name]_wrt` activates the output of each specified variable into the database. +- ``[variable's name]_wrt`` activates the output of each specified variable into the database. - `schlieren_alpha(i)` specifies the intensity of the numerical Schlieren of $i$-th component. @@ -898,11 +898,11 @@ The parameters are optionally used to define initial velocity profiles and pertu - `mixlayer_vel_profile` activates setting the mean streamwise velocity to a hyperbolic tangent profile. This option works only for `n > 0`. -- `mixlayer_vel_coef` is a parameter for the hyperbolic tangent profile of a mixing layer when `mixlayer_vel_profile = 'T'`. The mean streamwise velocity profile is given as: +- `mixlayer_vel_coef` is a parameter for the hyperbolic tangent profile of a mixing layer when ``mixlayer_vel_profile = 'T'``. The mean streamwise velocity profile is given as: \f[ u = \text{patch\_icpp(1)\%vel(1)} \cdot \tanh( y_{cc} \cdot \text{mixlayer\_vel\_coef}) \f] -- `mixlayer_perturb` activates the velocity perturbation for a temporal mixing layer with hyperbolic tangent mean streamwise velocity profile, using an inverter version of the spectrum-based synthetic turbulence generation method proposed by \cite Guo23. This option only works for `p > 0` and `mixlayer_vel_profile = 'T'`. +- `mixlayer_perturb` activates the velocity perturbation for a temporal mixing layer with hyperbolic tangent mean streamwise velocity profile, using an inverter version of the spectrum-based synthetic turbulence generation method proposed by \cite Guo23. This option only works for `p > 0` and ``mixlayer_vel_profile = 'T'``. ### 11. Phase Change Model {#sec-phase-change} | Parameter | Type | Description | diff --git a/docs/documentation/contributing.md b/docs/documentation/contributing.md index 7ebcc0c067..2fa302c6ed 100644 --- a/docs/documentation/contributing.md +++ b/docs/documentation/contributing.md @@ -380,7 +380,7 @@ $:END_GPU_PARALLEL_LOOP() Key rules: - Always pair `$:GPU_PARALLEL_LOOP(...)` with `$:END_GPU_PARALLEL_LOOP()` - Use `collapse(n)` to fuse nested loops when the loop bounds are independent -- Declare all loop-local temporaries in `private='[...]'` +- Declare all loop-local temporaries in ``private='[...]'`` - Never use `stop` or `error stop` inside a GPU loop ### How to Allocate and Manage GPU Arrays diff --git a/docs/documentation/testing.md b/docs/documentation/testing.md index ffb5ee3402..b08a0746f5 100644 --- a/docs/documentation/testing.md +++ b/docs/documentation/testing.md @@ -99,7 +99,7 @@ To test the post-processing code, append the `-a` or `--test-all` option: ./mfc.sh test -a -j 8 ``` -This argument will re-run the test stack with `parallel_io='T'`, which generates silo_hdf5 files. +This argument will re-run the test stack with ``parallel_io='T'``, which generates silo_hdf5 files. It will also turn most write parameters (`*_wrt`) on. Then, it searches through the silo files using `h5dump` to ensure that there are no `NaN`s or `Infinity`s. Although adding this option does not guarantee that accurate `.silo` files are generated, it does ensure that the post-process code does not fail or produce malformed data. diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index b019fff4ba..a01c1077d9 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -549,6 +549,165 @@ def check_cli_refs(repo_root: Path) -> list[str]: return errors +def check_unpaired_math(repo_root: Path) -> list[str]: + """Check for unpaired \\f$ inline math and unbalanced \\f[/\\f] display math.""" + doc_dir = repo_root / "docs" / "documentation" + if not doc_dir.exists(): + return [] + + ignored = _gitignored_docs(repo_root) + errors = [] + + for md_file in sorted(doc_dir.glob("*.md")): + if md_file.name in ignored: + continue + text = md_file.read_text(encoding="utf-8") + rel = md_file.relative_to(repo_root) + in_code = False + display_math_open = 0 # line number where \f[ was opened, 0 = closed + + for i, line in enumerate(text.split("\n"), 1): + if line.strip().startswith("```"): + in_code = not in_code + continue + if in_code: + continue + + # Count \f$ occurrences (should be even per line for inline math) + inline_count = len(re.findall(r"\\f\$", line)) + if inline_count % 2 != 0: + errors.append( + f" {rel}:{i} has {inline_count} \\f$ delimiter(s) (odd)." + " Fix: ensure every \\f$ has a matching closing \\f$" + ) + + # Track \f[ / \f] balance + opens = len(re.findall(r"\\f\[", line)) + closes = len(re.findall(r"\\f\]", line)) + for _ in range(opens): + if display_math_open: + errors.append( + f" {rel}:{i} opens \\f[ but previous \\f[" + f" from line {display_math_open} is still open." + " Fix: add missing \\f]" + ) + display_math_open = i + for _ in range(closes): + if not display_math_open: + errors.append( + f" {rel}:{i} has \\f] without a preceding \\f[." + " Fix: add missing \\f[ or remove extra \\f]" + ) + else: + display_math_open = 0 + + if display_math_open: + errors.append( + f" {rel}:{display_math_open} opens \\f[ that is never closed." + " Fix: add \\f] to close the display math block" + ) + + return errors + + +# Doxygen block commands that are incorrectly processed inside backtick +# code spans (known Doxygen bug, see github.com/doxygen/doxygen/issues/6054). +_DOXYGEN_BLOCK_CMDS = { + "code", "endcode", "verbatim", "endverbatim", + "dot", "enddot", "msc", "endmsc", + "startuml", "enduml", + "latexonly", "endlatexonly", "htmlonly", "endhtmlonly", + "xmlonly", "endxmlonly", "rtfonly", "endrtfonly", + "manonly", "endmanonly", "docbookonly", "enddocbookonly", + "todo", "deprecated", "bug", "test", + "note", "warning", "attention", "remark", + "brief", "details", "param", "return", "returns", +} + + +def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: + """Check for Doxygen @/\\ commands inside backtick code spans. + + Doxygen processes certain block commands even inside backtick code + spans, which can break rendering. Flag these so authors can + restructure or use fenced code blocks instead. + """ + doc_dir = repo_root / "docs" / "documentation" + if not doc_dir.exists(): + return [] + + ignored = _gitignored_docs(repo_root) + code_span_re = re.compile(r"``([^`\n]+)``|`([^`\n]+)`") + # Match @cmd or \cmd where cmd is a known Doxygen block command + cmd_pattern = "|".join(re.escape(c) for c in sorted(_DOXYGEN_BLOCK_CMDS)) + doxy_cmd_re = re.compile(rf"(?:@|\\)({cmd_pattern})\b") + + errors = [] + for md_file in sorted(doc_dir.glob("*.md")): + if md_file.name in ignored: + continue + text = md_file.read_text(encoding="utf-8") + rel = md_file.relative_to(repo_root) + in_code = False + for i, line in enumerate(text.split("\n"), 1): + if line.strip().startswith("```"): + in_code = not in_code + continue + if in_code: + continue + for m in code_span_re.finditer(line): + span = m.group(1) or m.group(2) + cmd_match = doxy_cmd_re.search(span) + if cmd_match: + cmd = cmd_match.group(0) + errors.append( + f" {rel}:{i} backtick span contains Doxygen" + f" command '{cmd}' which may be processed." + " Fix: use a fenced code block or rephrase" + ) + + return errors + + +def check_single_quote_in_backtick(repo_root: Path) -> list[str]: + """Check for single quotes inside single-backtick code spans. + + Doxygen treats a single quote inside a single-backtick code span as + ending the span (backward-compat quirk). Use double backticks instead. + """ + doc_dir = repo_root / "docs" / "documentation" + if not doc_dir.exists(): + return [] + + ignored = _gitignored_docs(repo_root) + # Match single-backtick spans (not double-backtick) + single_bt_re = re.compile(r"(? Date: Sat, 14 Feb 2026 23:41:45 -0500 Subject: [PATCH 09/12] Fix \dfrac rendering and add AMSmath lint check Replace \dfrac with \frac in equations.md (MathJax doesn't reliably load AMSmath extensions). Add check_amsmath_in_doxygen_math to the doc linter to flag AMSmath-only commands (\dfrac, \tfrac, \dbinom, etc.) in Doxygen math delimiters. Co-Authored-By: Claude Opus 4.6 --- docs/documentation/equations.md | 4 +- toolchain/mfc/lint_docs.py | 68 ++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index 7b41109865..fb9fd2e48b 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -43,8 +43,8 @@ Several EOS and transport parameters use **transformed stored forms** that diffe | Parameter | Physical quantity | What MFC expects (stored form) | |---|---|---| -| `fluid_pp(i)%%gamma` | Heat capacity ratio \f$\gamma\f$ | \f$\Gamma = \dfrac{1}{\gamma - 1}\f$ | -| `fluid_pp(i)%%pi_inf` | Stiffness pressure \f$\pi_\infty\f$ [Pa] | \f$\Pi_\infty = \dfrac{\gamma\,\pi_\infty}{\gamma - 1}\f$ [Pa] | +| `fluid_pp(i)%%gamma` | Heat capacity ratio \f$\gamma\f$ | \f$\Gamma = \frac{1}{\gamma - 1}\f$ | +| `fluid_pp(i)%%pi_inf` | Stiffness pressure \f$\pi_\infty\f$ [Pa] | \f$\Pi_\infty = \frac{\gamma\,\pi_\infty}{\gamma - 1}\f$ [Pa] | | `fluid_pp(i)%%Re(1)` | Dynamic viscosity \f$\mu\f$ | \f$1/\mu\f$ (inverse viscosity) | | `fluid_pp(i)%%Re(2)` | Bulk viscosity \f$\mu_b\f$ | \f$1/\mu_b\f$ (inverse bulk viscosity) | diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index a01c1077d9..4a1172956c 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -625,7 +625,7 @@ def check_unpaired_math(repo_root: Path) -> list[str]: } -def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: +def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: # pylint: disable=too-many-locals """Check for Doxygen @/\\ commands inside backtick code spans. Doxygen processes certain block commands even inside backtick code @@ -708,6 +708,71 @@ def check_single_quote_in_backtick(repo_root: Path) -> list[str]: return errors +# LaTeX commands that require AMSmath but are not reliably loaded by the +# MathJax configuration (config.js). Use the standard alternatives instead. +_AMSMATH_ONLY_CMDS = { + "dfrac": "frac", + "tfrac": "frac", + "dbinom": "binom", + "tbinom": "binom", + "dddot": "dot", + "ddddot": "dot", +} + + +def check_amsmath_in_doxygen_math(repo_root: Path) -> list[str]: # pylint: disable=too-many-locals + """Flag AMSmath-only commands in Doxygen math that may not render.""" + doc_dir = repo_root / "docs" / "documentation" + if not doc_dir.exists(): + return [] + + ignored = _gitignored_docs(repo_root) + # Match \f$...\f$ inline or content between \f[ and \f] + inline_re = re.compile(r"\\f\$(.*?)\\f\$") + cmd_names = "|".join(re.escape(c) for c in sorted(_AMSMATH_ONLY_CMDS)) + ams_re = re.compile(rf"\\({cmd_names})\b") + + errors = [] + for md_file in sorted(doc_dir.glob("*.md")): + if md_file.name in ignored: + continue + text = md_file.read_text(encoding="utf-8") + rel = md_file.relative_to(repo_root) + in_code = False + in_display = False + + for i, line in enumerate(text.split("\n"), 1): + if line.strip().startswith("```"): + in_code = not in_code + continue + if in_code: + continue + + # Check inline math \f$...\f$ + for m in inline_re.finditer(line): + for cm in ams_re.finditer(m.group(1)): + alt = _AMSMATH_ONLY_CMDS[cm.group(1)] + errors.append( + f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in" + f" math. Fix: use \\{alt} instead" + ) + + # Check display math lines between \f[ and \f] + if "\\f[" in line: + in_display = True + if in_display: + for cm in ams_re.finditer(line): + alt = _AMSMATH_ONLY_CMDS[cm.group(1)] + errors.append( + f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in" + f" math. Fix: use \\{alt} instead" + ) + if "\\f]" in line: + in_display = False + + return errors + + def main(): repo_root = Path(__file__).resolve().parents[2] @@ -721,6 +786,7 @@ def main(): all_errors.extend(check_doxygen_percent(repo_root)) all_errors.extend(check_doxygen_commands_in_backticks(repo_root)) all_errors.extend(check_single_quote_in_backtick(repo_root)) + all_errors.extend(check_amsmath_in_doxygen_math(repo_root)) all_errors.extend(check_section_anchors(repo_root)) all_errors.extend(check_physics_docs_coverage(repo_root)) all_errors.extend(check_identifier_refs(repo_root)) From 8baccbeca586ac5f0333258fde186a3516eac4c7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 14 Feb 2026 23:50:15 -0500 Subject: [PATCH 10/12] Fix @ref/@page regex to support hyphenated IDs The \w+ pattern stopped at hyphens, so @page getting-started was extracted as "getting" and @ref cli-reference as "cli". Use [\w-]+ to match the full hyphenated identifiers. Remove the cli-reference hardcoded workaround that was papering over this bug. Co-Authored-By: Claude Opus 4.6 --- toolchain/mfc/lint_docs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 4a1172956c..833ed9c4d3 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -59,8 +59,8 @@ "docs/documentation/case.md": CASE_MD_SKIP, } -# Match @ref page_id patterns -REF_RE = re.compile(r"@ref\s+(\w+)") +# Match @ref page_id patterns (page IDs may contain hyphens) +REF_RE = re.compile(r"@ref\s+([\w-]+)") def check_docs(repo_root: Path) -> list[str]: @@ -368,12 +368,12 @@ def check_page_refs(repo_root: Path) -> list[str]: if not doc_dir.exists(): return [] - # Collect all @page identifiers + # Collect all @page identifiers (IDs may contain hyphens) # Include Doxygen built-ins and auto-generated pages (created by ./mfc.sh generate) - page_ids = {"citelist", "parameters", "case_constraints", "physics_constraints", "examples", "cli-reference"} + page_ids = {"citelist", "parameters", "case_constraints", "physics_constraints", "examples"} for md_file in doc_dir.glob("*.md"): text = md_file.read_text(encoding="utf-8") - m = re.search(r"^\s*@page\s+(\w+)", text, flags=re.MULTILINE) + m = re.search(r"^\s*@page\s+([\w-]+)", text, flags=re.MULTILINE) if m: page_ids.add(m.group(1)) From 4c007ffc0364184521bb34241bed94df37131798 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 15 Feb 2026 01:37:39 -0500 Subject: [PATCH 11/12] Fix @ref/@page regex to support hyphenated IDs The \w+ pattern stopped at hyphens, so @page getting-started was extracted as "getting" and @ref cli-reference as "cli". Use [\w-]+ to match the full hyphenated identifiers. Re-add cli-reference to the auto-generated page ID set (file only exists after doc build). Co-Authored-By: Claude Opus 4.6 --- toolchain/mfc/lint_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 833ed9c4d3..027d630426 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -370,7 +370,7 @@ def check_page_refs(repo_root: Path) -> list[str]: # Collect all @page identifiers (IDs may contain hyphens) # Include Doxygen built-ins and auto-generated pages (created by ./mfc.sh generate) - page_ids = {"citelist", "parameters", "case_constraints", "physics_constraints", "examples"} + page_ids = {"citelist", "parameters", "case_constraints", "physics_constraints", "examples", "cli-reference"} for md_file in doc_dir.glob("*.md"): text = md_file.read_text(encoding="utf-8") m = re.search(r"^\s*@page\s+([\w-]+)", text, flags=re.MULTILINE) From ec9bfb99e2e63a7e5e673e9b52b4b9c66f70dedd Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 15 Feb 2026 14:46:42 -0500 Subject: [PATCH 12/12] Surface dimensional handling docs and reorganize Section 1b Reorganize equations.md Section 1b into three logical groups: - General Users (dimensions in = dimensions out, stored forms, materials) - Bubble Users (non-dimensional framework, reference scales, parameters) - Worked Examples Add stored-form parameter callout to case.md Section 5 (Fluid Materials) so users see the gamma/pi_inf/Re transforms at point of use. Add "Units and Dimensions" section to getting-started.md for new users. Update doc linter to collect {#id} anchors for cross-file @ref validation. Co-Authored-By: Claude Opus 4.6 --- docs/documentation/case.md | 8 ++++++++ docs/documentation/equations.md | 28 ++++++++++++++++----------- docs/documentation/getting-started.md | 6 ++++++ toolchain/mfc/lint_docs.py | 11 +++++++++++ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/docs/documentation/case.md b/docs/documentation/case.md index d34847ac01..0b65178bbc 100644 --- a/docs/documentation/case.md +++ b/docs/documentation/case.md @@ -387,6 +387,14 @@ Details of implementation of viscosity in MFC can be found in \cite Coralic15. - `fluid_pp(i)%%G` is required for `hypoelasticity`. +> **Stored-form parameters:** The values `gamma`, `pi_inf`, and `Re(1)`/`Re(2)` are **not** the raw physical quantities. MFC expects transformed stored forms: +> - `gamma` = \f$1/(\gamma-1)\f$, not \f$\gamma\f$ itself +> - `pi_inf` = \f$\gamma\,\pi_\infty / (\gamma - 1)\f$, not \f$\pi_\infty\f$ itself +> - `Re(1)` = \f$1/\mu\f$ (inverse viscosity), not \f$\mu\f$ itself +> +> Setting `gamma = 1.4` for air is a common mistake; the correct value is `1.0 / (1.4 - 1.0) = 2.5`. +> See @ref sec-stored-forms and @ref sec-material-values in the Equations reference for the full table. + ### 6. Simulation Algorithm {#sec-simulation-algorithm} See @ref equations "Equations" for the mathematical models these parameters control. diff --git a/docs/documentation/equations.md b/docs/documentation/equations.md index fb9fd2e48b..2c8ac4d1a7 100644 --- a/docs/documentation/equations.md +++ b/docs/documentation/equations.md @@ -29,15 +29,17 @@ The parameter `model_eqns` (1, 2, 3, or 4) selects the governing equation set. --- -## 1b. Units, Dimensions, and Non-Dimensionalization +## 1b. Units, Dimensions, and Non-Dimensionalization {#sec-units-dimensions} -### Dimensional Handling in the Flow Solver +### General Users: Dimensional Handling {#sec-dimensional-handling} + +#### Dimensions In = Dimensions Out {#sec-dimensions-in-out} The main flow solver (Navier-Stokes equations, Riemann solvers, viscous stress, body forces, surface tension, etc.) is **unit-agnostic**: whatever units the user provides for the initial and boundary conditions, the solver preserves them throughout the computation. If the user inputs SI units, the outputs are in SI units. If the user inputs CGS, the outputs are in CGS. No internal non-dimensionalization is performed by the flow solver. This means that for simulations **without** sub-grid bubble models, the user can work in any consistent unit system without additional effort. -### Stored Parameter Conventions +#### Stored Parameter Conventions {#sec-stored-forms} Several EOS and transport parameters use **transformed stored forms** that differ from the standard physical values. This is the most common source of input errors: @@ -52,7 +54,7 @@ These transformations arise because MFC internally solves the energy equation us **Common mistake:** setting `fluid_pp(1)%%gamma = 1.4` for air. The correct value is `1.0 / (1.4 - 1.0) = 2.5`. Setting `gamma = 1.4` corresponds to a physical \f$\gamma \approx 1.71\f$, which is not a standard gas. -### Common Material Values +#### Common Material Values {#sec-material-values} Pre-computed stored-form values for common fluids (SI units): @@ -85,11 +87,13 @@ mu = 1.002e-3 # water viscosity [Pa·s] "fluid_pp(1)%Re(1)": 1.0 / mu, # ≈ 998 ``` -### Unit Consistency +#### Unit Consistency {#sec-unit-consistency} The solver does not check or convert units. All inputs must use the **same consistent unit system** (e.g., all SI or all CGS). Mixing units — for example, pressures in atmospheres with densities in kg/m³ — will produce silently incorrect results. -### Non-Dimensional Bubble Dynamics +### Bubble Users: Non-Dimensional Framework {#sec-bubble-nondim} + +#### Non-Dimensional Bubble Dynamics {#sec-nondim-bubble-dynamics} The sub-grid bubble models (`bubbles_euler = .true.` or `bubbles_lagrange = .true.`) solve the bubble wall dynamics in **non-dimensional form**. The bubble wall pressure equation as implemented is: @@ -108,7 +112,7 @@ The dimensionless groups are: Because the bubble equations use these dimensionless numbers directly, all `bub_pp%%` inputs are interpreted by the code as **already non-dimensional**. The code does **not** non-dimensionalize bubble quantities internally. Therefore, when bubbles are enabled, the simulation must be run in a **fully non-dimensional** form: **all** inputs — flow ICs/BCs, EOS parameters, domain lengths, `dt`, and `bub_pp%%` values — must be scaled with the same \f$(x_0, p_0, \rho_0, u_0, t_0, T_0)\f$ reference quantities, or the coupled solution will be physically incorrect. -### Reference Scales +#### Reference Scales {#sec-reference-scales} When using bubble models, the user must choose reference scales and non-dimensionalize **all** inputs (flow and bubble) consistently. The standard convention used in the MFC examples is: @@ -121,7 +125,7 @@ When using bubble models, the user must choose reference scales and non-dimensio | Time | \f$t_0\f$ | \f$x_0 / u_0\f$ (derived) | | Temperature | \f$T_0\f$ | \f$T_{0,\text{ref}}\f$ (reference temperature) | -### Non-Dimensionalization of Input Parameters +#### Non-Dimensionalization of Input Parameters {#sec-nondim-inputs} The following table lists every `bub_pp%%` parameter and its required non-dimensionalization: @@ -150,7 +154,7 @@ The following table lists every `bub_pp%%` parameter and its required non-dimens When the reference scales match the bubble reference values (e.g., \f$x_0 = R_{0,\text{ref}}\f$, \f$p_0 = p_{0,\text{ref}}\f$, \f$\rho_0 = \rho_{0,\text{ref}}\f$), the reference parameters simplify to unity: `bub_pp%%R0ref = 1`, `bub_pp%%p0ref = 1`, `bub_pp%%rho0ref = 1`. -### Flow Parameters with Bubbles +#### Flow Parameters with Bubbles {#sec-flow-params-bubbles} When bubbles are enabled, the flow-level parameters must also be non-dimensionalized with the same reference scales: @@ -165,7 +169,7 @@ When bubbles are enabled, the flow-level parameters must also be non-dimensional | `fluid_pp(i)%%Re(1)` | \f$\rho_0\,x_0\,u_0 / \mu_i\f$ (Reynolds number, inverse viscosity) | | `dt` | Time step divided by \f$t_0\f$ | -### Two Different Viscosity Parameters +#### Two Different Viscosity Parameters {#sec-two-viscosities} MFC has two conceptually distinct viscosity-related parameters that serve different physical roles: @@ -179,7 +183,9 @@ MFC has two conceptually distinct viscosity-related parameters that serve differ These two parameters represent viscous effects at fundamentally different scales — bulk flow dissipation vs. single-bubble-wall damping — and are stored in separate derived types with separate code paths. They are **not** interchangeable: `fluid_pp%%Re(1)` is an inverse viscosity while `bub_pp%%mu_l` is a viscosity (non-dimensionalized). -### Example: Non-Dimensionalizing a Bubble Case +### Worked Examples {#sec-nondim-example} + +#### Example: Non-Dimensionalizing a Bubble Case {#sec-bubble-example} A typical bubble case setup in `case.py` follows this pattern: diff --git a/docs/documentation/getting-started.md b/docs/documentation/getting-started.md index 0e305023b5..c33fd25251 100644 --- a/docs/documentation/getting-started.md +++ b/docs/documentation/getting-started.md @@ -198,6 +198,12 @@ MFC has example cases in the `examples` folder. You can run such a case interact Please refer to the @ref running "Running" document for more information on `case.py` files and how to run them. +## Units and Dimensions + +MFC is **unit-agnostic**: the solver performs no internal unit conversions. Whatever units you provide for initial conditions, boundary conditions, and material properties, the same units appear in the output. + +The only requirement is **consistency** — all inputs must use the same unit system. Note that some parameters use **transformed stored forms** rather than standard physical values (e.g., `gamma` expects \f$1/(\gamma-1)\f$, not \f$\gamma\f$ itself). See @ref sec-stored-forms for details. + ## Helpful Tools ### Parameter Lookup diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 027d630426..ddd63a1503 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -377,6 +377,17 @@ def check_page_refs(repo_root: Path) -> list[str]: if m: page_ids.add(m.group(1)) + # Also collect {#id} anchors (valid @ref targets across files) + for md_file in doc_dir.glob("*.md"): + text = md_file.read_text(encoding="utf-8") + in_code = False + for line in text.split("\n"): + if line.strip().startswith("```"): + in_code = not in_code + continue + if not in_code: + page_ids.update(re.findall(r"\{#([\w-]+)\}", line)) + errors = [] for md_file in sorted(doc_dir.glob("*.md")): text = _strip_code_blocks(md_file.read_text(encoding="utf-8"))