From 14fe775bf3831c8f894deab012c57564b0458673 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 20:32:05 -0500 Subject: [PATCH 1/7] constraints --- docs/Doxyfile.in | 3 +- docs/documentation/case.md | 9 + docs/documentation/case_constraints.md | 264 +++++++++ docs/gen_constraints.sh | 17 + toolchain/mfc/gen_case_constraints_docs.py | 623 +++++++++++++++++++++ 5 files changed, 915 insertions(+), 1 deletion(-) create mode 100644 docs/documentation/case_constraints.md create mode 100755 docs/gen_constraints.sh create mode 100644 toolchain/mfc/gen_case_constraints_docs.py diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 26e1a0156b..114df3f720 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1163,7 +1163,8 @@ HTML_STYLESHEET = # list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = @DOXYGEN_HTML_EXTRA_STYLESHEET@ +HTML_EXTRA_STYLESHEET = @DOXYGEN_HTML_EXTRA_STYLESHEET@ \ + @CMAKE_SOURCE_DIR@/docs/custom-dark-fixes.css # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note diff --git a/docs/documentation/case.md b/docs/documentation/case.md index 3184e0689a..9fd3ea1abf 100644 --- a/docs/documentation/case.md +++ b/docs/documentation/case.md @@ -68,6 +68,15 @@ For example, to run the `scaling` case in "weak-scaling" mode: ## Parameters +## Feature Compatibility + +Before diving into parameter details, check the **[Feature Compatibility Guide](case_constraints.md)** to understand: +- Which features work together (MHD, bubbles, phase change, etc.) +- Common configuration patterns with copy-paste examples +- Requirements for each model equation and Riemann solver + +πŸ’‘ **Tip:** If you get a validation error, the compatibility guide explains what each parameter requires. + There are multiple sets of parameters that must be specified in the python input file: 1. [Runtime Parameters](#1-runtime) 2. [Computational Domain Parameters](#2-computational-domain) diff --git a/docs/documentation/case_constraints.md b/docs/documentation/case_constraints.md new file mode 100644 index 0000000000..f145cc7d36 --- /dev/null +++ b/docs/documentation/case_constraints.md @@ -0,0 +1,264 @@ +# MFC Feature Compatibility Guide + +> **Quick reference** for understanding which MFC features work together and configuration requirements. + +> Auto-generated from validation rules in `case_validator.py`. + +## πŸš€ Common Configuration Patterns + +Start with these proven combinations: + + +
+πŸ’§ Multiphase Flow (Bubbles) + +```python +'model_eqns': 2, # 5-equation model +'num_fluids': 2, # Two-phase +'bubbles_euler': 'T', # Ensemble-averaged bubbles +'riemann_solver': 2, # HLLC +'avg_state': 2, # Arithmetic average +``` +
+ +
+⚑ Magnetohydrodynamics (MHD) + +```python +'model_eqns': 2, # 5-equation model +'num_fluids': 1, # Single component +'mhd': 'T', # Enable MHD +'riemann_solver': 1, # HLL (or 4 for HLLD) +``` +
+ +
+🌑️ Phase Change + +```python +'model_eqns': 3, # 6-equation model +'num_fluids': 2, # Two-phase +'relax': 'T', # Phase change +'riemann_solver': 2, # HLLC +``` +
+ +
+πŸ’Ž Elastic Materials + +```python +'model_eqns': 2, # 5-equation model +'hypoelasticity': 'T', # Elastic solids +'riemann_solver': 1, # HLL +``` +
+ +## πŸ“Š Feature Compatibility + +What works together: + + +### Physics Models + +| Feature | Requirements | Notes | +|---------|-------------|-------| +| Magnetohydrodynamics (MHD) | β€” | βœ“ General use | +| Surface Tension Model | Specific model | βœ“ General use | +| Hypoelasticity | β€” | βœ“ General use | +| Hyperelasticity | β€” | βœ“ General use | +| Phase Change (Relaxation) | β€” | βœ“ General use | +| Viscosity | β€” | βœ“ General use | +| Acoustic Sources | β€” | βœ“ General use | + + +### Bubble Models + +| Feature | Requirements | Notes | +|---------|-------------|-------| +| Euler–Euler Bubble Model | β€” | βœ“ General use | +| Euler–Lagrange Bubble Model | β€” | βœ“ General use | +| Quadrature-Based Moment Method (QBMM) | β€” | βœ“ General use | +| Polydisperse Bubble Dynamics | β€” | βœ“ General use | +| adv_n | β€” | βœ“ General use | + + +### Numerics + +| Feature | Requirements | Notes | +|---------|-------------|-------| +| Riemann Solver | Specific model | βœ“ General use | +| WENO Order | β€” | βœ“ General use | +| MUSCL Order | β€” | βœ“ General use | + + +### Geometry + +| Feature | Requirements | Notes | +|---------|-------------|-------| +| Immersed Boundaries | β€” | βœ“ General use | +| Cylindrical Coordinates | β€” | βœ“ General use | + +## πŸ”’ Model Equations + +Choose your governing equations: + + +
+Model 1: Ο€-Ξ³ (Compressible Euler) + +- **Use for:** Single-fluid compressible flow +- **Value:** `model_eqns = 1` +- **Note:** Cannot use `num_fluids`, bubbles, or certain WENO variants +
+ +
+Model 2: 5-Equation (Most versatile) + +- **Use for:** Multiphase, bubbles, elastic materials, MHD +- **Value:** `model_eqns = 2` +- **Requirements:** Set `num_fluids` +- **Compatible with:** Most physics models +
+ +
+Model 3: 6-Equation (Phase change) + +- **Use for:** Phase change, cavitation +- **Value:** `model_eqns = 3` +- **Requirements:** `riemann_solver = 2` (HLLC), `avg_state = 2`, `wave_speeds = 1` +- **Note:** Not compatible with bubbles or 3D cylindrical +
+ +
+Model 4: 4-Equation (Single component) + +- **Use for:** Single-component flows with bubbles +- **Value:** `model_eqns = 4` +- **Requirements:** `num_fluids = 1`, set `rhoref` and `pref` +
+ +## βš™οΈ Riemann Solvers + +| Solver | `riemann_solver` | Best For | Requirements | +|--------|-----------------|----------|-------------| +| **HLL** | `1` | MHD, elastic materials | β€” | +| **HLLC** | `2` | Bubbles, phase change, multiphase | `avg_state=2` for bubbles | +| **Exact** | `3` | High accuracy (expensive) | β€” | +| **HLLD** | `4` | MHD (advanced) | MHD only, no relativity | +| **Lax-Friedrichs** | `5` | Robust fallback | Not with cylindrical+viscous | + +## πŸ’§ Bubble Models + + +
+Euler-Euler (`bubbles_euler`) + +**Requirements:** +- `model_eqns = 2` or `4` +- `riemann_solver = 2` (HLLC) +- `avg_state = 2` +- Set `nb` (number of bins) β‰₯ 1 + +**Extensions:** +- `polydisperse = T`: Multiple bubble sizes (requires odd `nb > 1`) +- `qbmm = T`: Quadrature method (requires `nnode = 4`) +- `adv_n = T`: Number density advection (requires `num_fluids = 1`) +
+ +
+Euler-Lagrange (`bubbles_lagrange`) + +**Requirements:** +- `n > 0` (2D or 3D only) +- `file_per_process = F` +- Not compatible with `model_eqns = 3` + +**Note:** Tracks individual bubbles +
+ +## πŸ“– Quick Parameter Reference + +Key parameters and their constraints: + + +
+MHD (`mhd`) + +**Requirements:** +- relativity requires mhd to be enabled +- Powell's method requires mhd to be enabled + +**Incompatibilities:** +- Bx0 must not be set if MHD is not enabled +- Bx0 must not be set in 2D/3D MHD simulations + +
+ + +
+Surface Tension (`surface_tension`) + +**Requirements:** +- sigma must be set if surface_tension is enabled +- The surface tension model requires model_eqns = 2 or model_eqns = 3 +- The surface tension model requires num_fluids = 2 + +**Valid values:** +- sigma must be greater than or equal to zero + +
+ + +
+Number of Fluids (`num_fluids`) + +**Requirements:** +- 5-equation model (model_eqns = 2) requires num_fluids to be set +- 6-equation model (model_eqns = 3) requires num_fluids to be set +- 4-equation model (model_eqns = 4) requires num_fluids to be set + +**Incompatibilities:** +- num_fluids is not supported for model_eqns = 1 +- num_fluids = 1 does not support mpp_lim + +**Valid values:** +- num_fluids must be positive +- perturb_flow_fluid must be between 0 and num_fluids + +
+ + +
+Cylindrical Coordinates (`cyl_coord`) + +**Incompatibilities:** +- 6-equation model (model_eqns = 3) does not support cylindrical coordinates (cyl_coord = T and p != 0) +- Bubble models untested in cylindrical coordinates +- Acoustic source is not supported in 3D cylindrical simulations + +**Valid values:** +- p must be odd for cylindrical coordinates + +
+ + +
+Immersed Boundaries (`ib`) + +**Requirements:** +- Immersed Boundaries do not work in 1D (requires n > 0) + +**Incompatibilities:** +- output_partial_domain is incompatible with certain output flags + +**Valid values:** +- num_ibs must be between 1 and num_patches_max (10) + +
+ + +--- + +πŸ’‘ **Tip:** If you encounter a validation error, check the relevant section above or +review [`case_validator.py`](../../toolchain/mfc/case_validator.py) for complete validation logic. + diff --git a/docs/gen_constraints.sh b/docs/gen_constraints.sh new file mode 100755 index 0000000000..e55d99ef33 --- /dev/null +++ b/docs/gen_constraints.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Generate case constraints documentation from case_validator.py + +set -e + +REPO_ROOT="$1" + +if [ -z "$REPO_ROOT" ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Generating case constraints documentation..." +python3 "$REPO_ROOT/toolchain/mfc/gen_case_constraints_docs.py" > "$REPO_ROOT/docs/documentation/case_constraints.md" +echo "βœ“ Generated docs/documentation/case_constraints.md" + + diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py new file mode 100644 index 0000000000..a01af1c3c6 --- /dev/null +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -0,0 +1,623 @@ +#!/usr/bin/env python3 +""" +Generate human-readable documentation for MFC case parameter constraints. + +Parses toolchain/mfc/case_validator.py, extracts all `self.prohibit(...)` rules, +maps them to parameters and stages, and emits Markdown to stdout. +""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Set, Iterable +from collections import defaultdict + +HERE = Path(__file__).resolve().parent +CASE_VALIDATOR_PATH = HERE / "case_validator.py" + + +# --------------------------------------------------------------------------- +# Data structures +# --------------------------------------------------------------------------- + +@dataclass +class Rule: + method: str # e.g. "check_igr_simulation" + lineno: int # line number of the prohibit call + params: List[str] # case parameter names used in condition + message: str # user-friendly error message + stages: Set[str] = field(default_factory=set) # e.g. {"simulation", "pre_process"} + + +# --------------------------------------------------------------------------- +# AST analysis: methods, call graph, rules +# --------------------------------------------------------------------------- + +class CaseValidatorAnalyzer(ast.NodeVisitor): + """ + Analyzes the CaseValidator class: + + - collects all methods + - builds a call graph between methods + - extracts all self.prohibit(...) rules + """ + + def __init__(self): + super().__init__() + self.in_case_validator = False + self.current_method: str | None = None + + self.methods: Dict[str, ast.FunctionDef] = {} + self.call_graph: Dict[str, Set[str]] = defaultdict(set) + self.rules: List[Rule] = [] + + # Stack of {local_name -> param_name} maps, one per method + self.local_param_stack: List[Dict[str, str]] = [] + + # --- top-level entrypoint --- + + def visit_ClassDef(self, node: ast.ClassDef): + if node.name == "CaseValidator": + self.in_case_validator = True + # collect methods + for item in node.body: + if isinstance(item, ast.FunctionDef): + self.methods[item.name] = item + # now analyze all methods + for method in self.methods.values(): + self._analyze_method(method) + self.in_case_validator = False + else: + self.generic_visit(node) + + # --- per-method analysis --- + + def _analyze_method(self, func: ast.FunctionDef): + """Analyze a single method: local param mapping, call graph, rules.""" + self.current_method = func.name + local_param_map = self._build_local_param_map(func) + self.local_param_stack.append(local_param_map) + self.generic_visit(func) + self.local_param_stack.pop() + self.current_method = None + + def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: + """ + Look for assignments like: + igr = self.get('igr', 'F') == 'T' + model_eqns = self.get('model_eqns') + and record local_name -> 'param_name'. + """ + m: Dict[str, str] = {} + for stmt in func.body: + if isinstance(stmt, ast.Assign): + # Handle both direct calls and comparisons + value = stmt.value + # Unwrap comparisons like "self.get('igr', 'F') == 'T'" + if isinstance(value, ast.Compare): + value = value.left + + if isinstance(value, ast.Call): + call = value + if ( + isinstance(call.func, ast.Attribute) + and isinstance(call.func.value, ast.Name) + and call.func.value.id == "self" + and call.func.attr == "get" + and call.args + and isinstance(call.args[0], ast.Constant) + and isinstance(call.args[0].value, str) + ): + param_name = call.args[0].value + for target in stmt.targets: + if isinstance(target, ast.Name): + m[target.id] = param_name + return m + + # --- visit calls to build call graph + rules --- + + def visit_Call(self, node: ast.Call): + # record method call edges: self.some_method(...) + if ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "self" + and isinstance(node.func.attr, str) + ): + callee = node.func.attr + if self.current_method is not None: + # method call on self + self.call_graph[self.current_method].add(callee) + + # detect self.prohibit(, "") + if ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "self" + and node.func.attr == "prohibit" + and len(node.args) >= 2 + ): + condition, msg_node = node.args[0], node.args[1] + if isinstance(msg_node, ast.Constant) and isinstance(msg_node.value, str): + params = sorted(self._extract_params(condition)) + rule = Rule( + method=self.current_method or "", + lineno=node.lineno, + params=params, + message=msg_node.value, + ) + self.rules.append(rule) + + self.generic_visit(node) + + def _extract_params(self, condition: ast.AST) -> Set[str]: + """ + Collect parameter names used in the condition via: + - local variables mapped from self.get(...) + - direct self.get('param_name', ...) calls + """ + params: Set[str] = set() + local_map = self.local_param_stack[-1] if self.local_param_stack else {} + + for node in ast.walk(condition): + # local names + if isinstance(node, ast.Name) and node.id in local_map: + params.add(local_map[node.id]) + + # direct self.get('param_name') + if isinstance(node, ast.Call): + if ( + isinstance(node.func, ast.Attribute) + and isinstance(node.func.value, ast.Name) + and node.func.value.id == "self" + and node.func.attr == "get" + and node.args + and isinstance(node.args[0], ast.Constant) + and isinstance(node.args[0].value, str) + ): + params.add(node.args[0].value) + + return params + + +# --------------------------------------------------------------------------- +# Stage inference from validate_* roots and call graph +# --------------------------------------------------------------------------- + +STAGE_ROOTS: Dict[str, List[str]] = { + "common": ["validate_common"], + "simulation": ["validate_simulation"], + "pre_process": ["validate_pre_process"], + "post_process": ["validate_post_process"], +} + + +def compute_method_stages(call_graph: Dict[str, Set[str]]) -> Dict[str, Set[str]]: + """ + For each stage (simulation/pre_process/post_process/common), starting from + validate_* roots, walk the call graph and record which methods belong to which stages. + """ + method_stages: Dict[str, Set[str]] = defaultdict(set) + + def dfs(start: str, stage: str): + stack = [start] + visited: Set[str] = set() + while stack: + m = stack.pop() + if m in visited: + continue + visited.add(m) + method_stages[m].add(stage) + for nxt in call_graph.get(m, ()): + if nxt not in visited: + stack.append(nxt) + + for stage, roots in STAGE_ROOTS.items(): + for root in roots: + dfs(root, stage) + + return method_stages + + +# --------------------------------------------------------------------------- +# Classification of messages for nicer grouping +# --------------------------------------------------------------------------- + +def classify_message(msg: str) -> str: + """ + Roughly classify rule messages for nicer grouping. + + Returns one of: "requirement", "incompatibility", "range", "other". + """ + text = msg.lower() + + if ( + "not compatible" in text + or "does not support" in text + or "cannot be used" in text + or "must not" in text + or "is not supported" in text + or "incompatible" in text + or "untested" in text + ): + return "incompatibility" + + if ( + "requires" in text + or "must be set if" in text + or "must be specified" in text + or "must be set with" in text + or "can only be enabled if" in text + or "must be set for" in text + ): + return "requirement" + + if ( + "must be between" in text + or "must be positive" in text + or "must be non-negative" in text + or "must be greater than" in text + or "must be less than" in text + or "must be at least" in text + or "must be <=" in text + or "must be >=" in text + or "must be odd" in text + or "divisible by" in text + ): + return "range" + + return "other" + + +# Optional: nicer display names / categories (you can extend this) +FEATURE_META = { + "igr": {"title": "Iterative Generalized Riemann (IGR)", "category": "solver"}, + "bubbles_euler": {"title": "Euler–Euler Bubble Model", "category": "bubbles"}, + "bubbles_lagrange": {"title": "Euler–Lagrange Bubble Model", "category": "bubbles"}, + "qbmm": {"title": "Quadrature-Based Moment Method (QBMM)", "category": "bubbles"}, + "polydisperse": {"title": "Polydisperse Bubble Dynamics", "category": "bubbles"}, + "mhd": {"title": "Magnetohydrodynamics (MHD)", "category": "physics"}, + "alt_soundspeed": {"title": "Alternative Sound Speed", "category": "physics"}, + "surface_tension": {"title": "Surface Tension Model", "category": "physics"}, + "hypoelasticity": {"title": "Hypoelasticity", "category": "physics"}, + "hyperelasticity": {"title": "Hyperelasticity", "category": "physics"}, + "relax": {"title": "Phase Change (Relaxation)", "category": "physics"}, + "viscous": {"title": "Viscosity", "category": "physics"}, + "acoustic_source": {"title": "Acoustic Sources", "category": "physics"}, + "ib": {"title": "Immersed Boundaries", "category": "geometry"}, + "cyl_coord": {"title": "Cylindrical Coordinates", "category": "geometry"}, + "weno_order": {"title": "WENO Order", "category": "numerics"}, + "muscl_order": {"title": "MUSCL Order", "category": "numerics"}, + "riemann_solver": {"title": "Riemann Solver", "category": "numerics"}, + "model_eqns": {"title": "Model Equations", "category": "fundamentals"}, + "num_fluids": {"title": "Number of Fluids", "category": "fundamentals"}, +} + + +def feature_title(param: str) -> str: + meta = FEATURE_META.get(param) + if meta and "title" in meta: + return meta["title"] + return param + + +# --------------------------------------------------------------------------- +# Markdown rendering +# --------------------------------------------------------------------------- + +def render_markdown(rules: Iterable[Rule]) -> str: + """ + Render user-friendly compatibility tables and summaries. + """ + # Group by parameter + by_param: Dict[str, List[Rule]] = defaultdict(list) + for r in rules: + if not r.params: + continue + for p in r.params: + by_param[p].append(r) + + lines: List[str] = [] + + lines.append("# MFC Feature Compatibility Guide\n") + lines.append( + "> **Quick reference** for understanding which MFC features work together " + "and configuration requirements.\n" + ) + lines.append( + "> Auto-generated from validation rules in `case_validator.py`.\n" + ) + + # Define major feature groups (excluding IGR) + major_features = { + "Physics Models": ["mhd", "surface_tension", "hypoelasticity", "hyperelasticity", "relax", "viscous", "acoustic_source"], + "Bubble Models": ["bubbles_euler", "bubbles_lagrange", "qbmm", "polydisperse", "adv_n"], + "Numerics": ["riemann_solver", "weno_order", "muscl_order"], + "Geometry": ["ib", "cyl_coord"], + } + + # 1. Quick Start: Common Configurations + lines.append("## πŸš€ Common Configuration Patterns\n") + lines.append("Start with these proven combinations:\n") + lines.append("") + lines.append("
") + lines.append("πŸ’§ Multiphase Flow (Bubbles)\n") + lines.append("```python") + lines.append("'model_eqns': 2, # 5-equation model") + lines.append("'num_fluids': 2, # Two-phase") + lines.append("'bubbles_euler': 'T', # Ensemble-averaged bubbles") + lines.append("'riemann_solver': 2, # HLLC") + lines.append("'avg_state': 2, # Arithmetic average") + lines.append("```") + lines.append("
\n") + + lines.append("
") + lines.append("⚑ Magnetohydrodynamics (MHD)\n") + lines.append("```python") + lines.append("'model_eqns': 2, # 5-equation model") + lines.append("'num_fluids': 1, # Single component") + lines.append("'mhd': 'T', # Enable MHD") + lines.append("'riemann_solver': 1, # HLL (or 4 for HLLD)") + lines.append("```") + lines.append("
\n") + + lines.append("
") + lines.append("🌑️ Phase Change\n") + lines.append("```python") + lines.append("'model_eqns': 3, # 6-equation model") + lines.append("'num_fluids': 2, # Two-phase") + lines.append("'relax': 'T', # Phase change") + lines.append("'riemann_solver': 2, # HLLC") + lines.append("```") + lines.append("
\n") + + lines.append("
") + lines.append("πŸ’Ž Elastic Materials\n") + lines.append("```python") + lines.append("'model_eqns': 2, # 5-equation model") + lines.append("'hypoelasticity': 'T', # Elastic solids") + lines.append("'riemann_solver': 1, # HLL") + lines.append("```") + lines.append("
\n") + + # 2. Feature Compatibility Matrix (simplified, no IGR column) + lines.append("## πŸ“Š Feature Compatibility\n") + lines.append("What works together:\n") + + for category, features in major_features.items(): + lines.append(f"\n### {category}\n") + + # Build compatibility info (exclude IGR from incompatibilities) + compat_info = {} + for feat in features: + if feat not in by_param: + continue + + incomp = [] + req = [] + for rule in by_param[feat]: + kind = classify_message(rule.message) + msg = rule.message + + # Skip IGR-related incompatibilities + if "IGR" in msg: + continue + + if kind == "incompatibility": + for other_feat in features: + if other_feat != feat and other_feat in rule.params: + incomp.append(feature_title(other_feat)) + elif kind == "requirement": + for other_feat in features: + if other_feat != feat and other_feat in rule.params: + req.append(feature_title(other_feat)) + + compat_info[feat] = {"incomp": list(set(incomp)), "req": list(set(req))} + + # Render as compact table + lines.append("| Feature | Requirements | Notes |") + lines.append("|---------|-------------|-------|") + + for feat in features: + if feat not in by_param: + continue + title = feature_title(feat) + info = compat_info.get(feat, {"incomp": [], "req": []}) + + # Get key requirement + reqs = [] + for rule in by_param[feat]: + if classify_message(rule.message) == "requirement": + msg = rule.message + if "model_eqns" in msg: + reqs.append("Specific model") + break + + req_str = reqs[0] if reqs else "β€”" + + # Get short note + incomp_with = [i for i in info["incomp"] if i not in ["IGR"]][:2] + if incomp_with: + note = f"⚠️ Not with: {', '.join(incomp_with)}" + else: + note = "βœ“ General use" + + lines.append(f"| {title} | {req_str} | {note} |") + + lines.append("") + + # 3. Model Equations + lines.append("## πŸ”’ Model Equations\n") + lines.append("Choose your governing equations:\n") + lines.append("") + + lines.append("
") + lines.append("Model 1: Ο€-Ξ³ (Compressible Euler)\n") + lines.append("- **Use for:** Single-fluid compressible flow") + lines.append("- **Value:** `model_eqns = 1`") + lines.append("- **Note:** Cannot use `num_fluids`, bubbles, or certain WENO variants") + lines.append("
\n") + + lines.append("
") + lines.append("Model 2: 5-Equation (Most versatile)\n") + lines.append("- **Use for:** Multiphase, bubbles, elastic materials, MHD") + lines.append("- **Value:** `model_eqns = 2`") + lines.append("- **Requirements:** Set `num_fluids`") + lines.append("- **Compatible with:** Most physics models") + lines.append("
\n") + + lines.append("
") + lines.append("Model 3: 6-Equation (Phase change)\n") + lines.append("- **Use for:** Phase change, cavitation") + lines.append("- **Value:** `model_eqns = 3`") + lines.append("- **Requirements:** `riemann_solver = 2` (HLLC), `avg_state = 2`, `wave_speeds = 1`") + lines.append("- **Note:** Not compatible with bubbles or 3D cylindrical") + lines.append("
\n") + + lines.append("
") + lines.append("Model 4: 4-Equation (Single component)\n") + lines.append("- **Use for:** Single-component flows with bubbles") + lines.append("- **Value:** `model_eqns = 4`") + lines.append("- **Requirements:** `num_fluids = 1`, set `rhoref` and `pref`") + lines.append("
\n") + + # 4. Riemann Solvers (simplified) + lines.append("## βš™οΈ Riemann Solvers\n") + lines.append("| Solver | `riemann_solver` | Best For | Requirements |") + lines.append("|--------|-----------------|----------|-------------|") + lines.append("| **HLL** | `1` | MHD, elastic materials | β€” |") + lines.append("| **HLLC** | `2` | Bubbles, phase change, multiphase | `avg_state=2` for bubbles |") + lines.append("| **Exact** | `3` | High accuracy (expensive) | β€” |") + lines.append("| **HLLD** | `4` | MHD (advanced) | MHD only, no relativity |") + lines.append("| **Lax-Friedrichs** | `5` | Robust fallback | Not with cylindrical+viscous |") + lines.append("") + + # 5. Bubble Models (enhanced with collapsible) + if "bubbles_euler" in by_param or "bubbles_lagrange" in by_param: + lines.append("## πŸ’§ Bubble Models\n") + lines.append("") + + lines.append("
") + lines.append("Euler-Euler (`bubbles_euler`)\n") + lines.append("**Requirements:**") + lines.append("- `model_eqns = 2` or `4`") + lines.append("- `riemann_solver = 2` (HLLC)") + lines.append("- `avg_state = 2`") + lines.append("- Set `nb` (number of bins) β‰₯ 1\n") + lines.append("**Extensions:**") + lines.append("- `polydisperse = T`: Multiple bubble sizes (requires odd `nb > 1`)") + lines.append("- `qbmm = T`: Quadrature method (requires `nnode = 4`)") + lines.append("- `adv_n = T`: Number density advection (requires `num_fluids = 1`)") + lines.append("
\n") + + lines.append("
") + lines.append("Euler-Lagrange (`bubbles_lagrange`)\n") + lines.append("**Requirements:**") + lines.append("- `n > 0` (2D or 3D only)") + lines.append("- `file_per_process = F`") + lines.append("- Not compatible with `model_eqns = 3`\n") + lines.append("**Note:** Tracks individual bubbles") + lines.append("
\n") + + # 6. Condensed Parameter Reference + lines.append("## πŸ“– Quick Parameter Reference\n") + lines.append("Key parameters and their constraints:\n") + + # Highlight only the most important parameters in collapsible sections + important_params = { + "MHD": "mhd", + "Surface Tension": "surface_tension", + "Viscosity": "viscous", + "Number of Fluids": "num_fluids", + "Cylindrical Coordinates": "cyl_coord", + "Immersed Boundaries": "ib", + } + + for title, param in important_params.items(): + if param not in by_param: + continue + + rules_for_param = by_param[param] + + # Get key info + requirements = [] + incompatibilities = [] + ranges = [] + + for rule in rules_for_param: + msg = rule.message + # Skip IGR-related messages + if "IGR" in msg: + continue + + kind = classify_message(msg) + if kind == "requirement": + requirements.append(msg) + elif kind == "incompatibility": + incompatibilities.append(msg) + elif kind == "range": + ranges.append(msg) + + if not (requirements or incompatibilities or ranges): + continue + + lines.append(f"\n
") + lines.append(f"{title} (`{param}`)\n") + + if requirements: + lines.append("**Requirements:**") + for req in requirements[:3]: + lines.append(f"- {req}") + lines.append("") + + if incompatibilities: + lines.append("**Incompatibilities:**") + for inc in incompatibilities[:3]: + lines.append(f"- {inc}") + lines.append("") + + if ranges: + lines.append("**Valid values:**") + for rng in ranges[:2]: + lines.append(f"- {rng}") + lines.append("") + + lines.append("
\n") + + # Add a footer with link to full validator + lines.append("\n---\n") + lines.append("πŸ’‘ **Tip:** If you encounter a validation error, check the relevant section above or ") + lines.append("review [`case_validator.py`](../../toolchain/mfc/case_validator.py) for complete validation logic.\n") + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + src = CASE_VALIDATOR_PATH.read_text(encoding="utf-8") + tree = ast.parse(src, filename=str(CASE_VALIDATOR_PATH)) + + analyzer = CaseValidatorAnalyzer() + analyzer.visit(tree) + + # Infer stages per method from call graph + method_stages = compute_method_stages(analyzer.call_graph) + + # Attach stages to rules + for r in analyzer.rules: + r.stages = method_stages.get(r.method, set()) + + md = render_markdown(analyzer.rules) + print(md) + + +if __name__ == "__main__": + main() + + + From 0213180ca412b2a332a00b4def3e4153c23b3cb7 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 20:55:22 -0500 Subject: [PATCH 2/7] add artifacts --- .github/workflows/docs.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d161d80342..3613c38b10 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,6 +34,14 @@ jobs: cmake -S . -B build -G Ninja --install-prefix=$(pwd)/build/install -D MFC_DOCUMENTATION=ON ninja -C build install + - name: Upload Built Documentation Artifact + uses: actions/upload-artifact@v4 + with: + name: mfc-docs + path: build/install/docs/mfc + if-no-files-found: error + retention-days: 7 + # From here https://github.com/cicirello/generate-sitemap - name: Generate the sitemap id: sitemap From 7bf48b18fcaecb1ad1f2c06eb35df010b495b00e Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 21:12:22 -0500 Subject: [PATCH 3/7] lint --- toolchain/mfc/gen_case_constraints_docs.py | 95 +++++++++++----------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py index a01af1c3c6..c8bb39b7a8 100644 --- a/toolchain/mfc/gen_case_constraints_docs.py +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -83,7 +83,7 @@ def _analyze_method(self, func: ast.FunctionDef): self.local_param_stack.pop() self.current_method = None - def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: + def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: # pylint: disable=too-many-nested-blocks """ Look for assignments like: igr = self.get('igr', 'F') == 'T' @@ -91,17 +91,17 @@ def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: and record local_name -> 'param_name'. """ m: Dict[str, str] = {} - for stmt in func.body: + for stmt in func.body: # pylint: disable=too-many-nested-blocks if isinstance(stmt, ast.Assign): # Handle both direct calls and comparisons value = stmt.value # Unwrap comparisons like "self.get('igr', 'F') == 'T'" if isinstance(value, ast.Compare): value = value.left - + if isinstance(value, ast.Call): call = value - if ( + if ( # pylint: disable=too-many-boolean-expressions isinstance(call.func, ast.Attribute) and isinstance(call.func.value, ast.Name) and call.func.value.id == "self" @@ -168,7 +168,7 @@ def _extract_params(self, condition: ast.AST) -> Set[str]: # direct self.get('param_name') if isinstance(node, ast.Call): - if ( + if ( # pylint: disable=too-many-boolean-expressions isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "self" @@ -233,7 +233,7 @@ def classify_message(msg: str) -> str: """ text = msg.lower() - if ( + if ( # pylint: disable=too-many-boolean-expressions "not compatible" in text or "does not support" in text or "cannot be used" in text @@ -244,7 +244,7 @@ def classify_message(msg: str) -> str: ): return "incompatibility" - if ( + if ( # pylint: disable=too-many-boolean-expressions "requires" in text or "must be set if" in text or "must be specified" in text @@ -254,7 +254,7 @@ def classify_message(msg: str) -> str: ): return "requirement" - if ( + if ( # pylint: disable=too-many-boolean-expressions "must be between" in text or "must be positive" in text or "must be non-negative" in text @@ -307,7 +307,7 @@ def feature_title(param: str) -> str: # Markdown rendering # --------------------------------------------------------------------------- -def render_markdown(rules: Iterable[Rule]) -> str: +def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-locals,too-many-branches,too-many-statements """ Render user-friendly compatibility tables and summaries. """ @@ -352,7 +352,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("'avg_state': 2, # Arithmetic average") lines.append("```") lines.append("\n") - + lines.append("
") lines.append("⚑ Magnetohydrodynamics (MHD)\n") lines.append("```python") @@ -362,7 +362,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("'riemann_solver': 1, # HLL (or 4 for HLLD)") lines.append("```") lines.append("
\n") - + lines.append("
") lines.append("🌑️ Phase Change\n") lines.append("```python") @@ -372,7 +372,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("'riemann_solver': 2, # HLLC") lines.append("```") lines.append("
\n") - + lines.append("
") lines.append("πŸ’Ž Elastic Materials\n") lines.append("```python") @@ -385,26 +385,26 @@ def render_markdown(rules: Iterable[Rule]) -> str: # 2. Feature Compatibility Matrix (simplified, no IGR column) lines.append("## πŸ“Š Feature Compatibility\n") lines.append("What works together:\n") - - for category, features in major_features.items(): + + for category, features in major_features.items(): # pylint: disable=too-many-nested-blocks lines.append(f"\n### {category}\n") - + # Build compatibility info (exclude IGR from incompatibilities) compat_info = {} for feat in features: if feat not in by_param: continue - + incomp = [] req = [] for rule in by_param[feat]: kind = classify_message(rule.message) msg = rule.message - + # Skip IGR-related incompatibilities if "IGR" in msg: continue - + if kind == "incompatibility": for other_feat in features: if other_feat != feat and other_feat in rule.params: @@ -413,19 +413,19 @@ def render_markdown(rules: Iterable[Rule]) -> str: for other_feat in features: if other_feat != feat and other_feat in rule.params: req.append(feature_title(other_feat)) - + compat_info[feat] = {"incomp": list(set(incomp)), "req": list(set(req))} - + # Render as compact table lines.append("| Feature | Requirements | Notes |") lines.append("|---------|-------------|-------|") - + for feat in features: if feat not in by_param: continue title = feature_title(feat) info = compat_info.get(feat, {"incomp": [], "req": []}) - + # Get key requirement reqs = [] for rule in by_param[feat]: @@ -434,32 +434,32 @@ def render_markdown(rules: Iterable[Rule]) -> str: if "model_eqns" in msg: reqs.append("Specific model") break - + req_str = reqs[0] if reqs else "β€”" - + # Get short note incomp_with = [i for i in info["incomp"] if i not in ["IGR"]][:2] if incomp_with: note = f"⚠️ Not with: {', '.join(incomp_with)}" else: note = "βœ“ General use" - + lines.append(f"| {title} | {req_str} | {note} |") - + lines.append("") - # 3. Model Equations + # 3. Model Equations lines.append("## πŸ”’ Model Equations\n") lines.append("Choose your governing equations:\n") lines.append("") - + lines.append("
") lines.append("Model 1: Ο€-Ξ³ (Compressible Euler)\n") lines.append("- **Use for:** Single-fluid compressible flow") lines.append("- **Value:** `model_eqns = 1`") lines.append("- **Note:** Cannot use `num_fluids`, bubbles, or certain WENO variants") lines.append("
\n") - + lines.append("
") lines.append("Model 2: 5-Equation (Most versatile)\n") lines.append("- **Use for:** Multiphase, bubbles, elastic materials, MHD") @@ -467,7 +467,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("- **Requirements:** Set `num_fluids`") lines.append("- **Compatible with:** Most physics models") lines.append("
\n") - + lines.append("
") lines.append("Model 3: 6-Equation (Phase change)\n") lines.append("- **Use for:** Phase change, cavitation") @@ -475,7 +475,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("- **Requirements:** `riemann_solver = 2` (HLLC), `avg_state = 2`, `wave_speeds = 1`") lines.append("- **Note:** Not compatible with bubbles or 3D cylindrical") lines.append("
\n") - + lines.append("
") lines.append("Model 4: 4-Equation (Single component)\n") lines.append("- **Use for:** Single-component flows with bubbles") @@ -498,7 +498,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: if "bubbles_euler" in by_param or "bubbles_lagrange" in by_param: lines.append("## πŸ’§ Bubble Models\n") lines.append("") - + lines.append("
") lines.append("Euler-Euler (`bubbles_euler`)\n") lines.append("**Requirements:**") @@ -511,7 +511,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: lines.append("- `qbmm = T`: Quadrature method (requires `nnode = 4`)") lines.append("- `adv_n = T`: Number density advection (requires `num_fluids = 1`)") lines.append("
\n") - + lines.append("
") lines.append("Euler-Lagrange (`bubbles_lagrange`)\n") lines.append("**Requirements:**") @@ -524,34 +524,34 @@ def render_markdown(rules: Iterable[Rule]) -> str: # 6. Condensed Parameter Reference lines.append("## πŸ“– Quick Parameter Reference\n") lines.append("Key parameters and their constraints:\n") - + # Highlight only the most important parameters in collapsible sections important_params = { "MHD": "mhd", - "Surface Tension": "surface_tension", + "Surface Tension": "surface_tension", "Viscosity": "viscous", "Number of Fluids": "num_fluids", "Cylindrical Coordinates": "cyl_coord", "Immersed Boundaries": "ib", } - + for title, param in important_params.items(): if param not in by_param: continue - + rules_for_param = by_param[param] - + # Get key info requirements = [] incompatibilities = [] ranges = [] - + for rule in rules_for_param: msg = rule.message # Skip IGR-related messages if "IGR" in msg: continue - + kind = classify_message(msg) if kind == "requirement": requirements.append(msg) @@ -559,31 +559,31 @@ def render_markdown(rules: Iterable[Rule]) -> str: incompatibilities.append(msg) elif kind == "range": ranges.append(msg) - + if not (requirements or incompatibilities or ranges): continue - + lines.append(f"\n
") lines.append(f"{title} (`{param}`)\n") - + if requirements: lines.append("**Requirements:**") for req in requirements[:3]: lines.append(f"- {req}") lines.append("") - + if incompatibilities: lines.append("**Incompatibilities:**") for inc in incompatibilities[:3]: lines.append(f"- {inc}") lines.append("") - + if ranges: lines.append("**Valid values:**") for rng in ranges[:2]: lines.append(f"- {rng}") lines.append("") - + lines.append("
\n") # Add a footer with link to full validator @@ -618,6 +618,3 @@ def main() -> None: if __name__ == "__main__": main() - - - From 6fd5be2c44412052a216a6632917ef365e2b7a27 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 22:48:29 -0500 Subject: [PATCH 4/7] better now? --- docs/documentation/case_constraints.md | 264 ------------------------- 1 file changed, 264 deletions(-) delete mode 100644 docs/documentation/case_constraints.md diff --git a/docs/documentation/case_constraints.md b/docs/documentation/case_constraints.md deleted file mode 100644 index f145cc7d36..0000000000 --- a/docs/documentation/case_constraints.md +++ /dev/null @@ -1,264 +0,0 @@ -# MFC Feature Compatibility Guide - -> **Quick reference** for understanding which MFC features work together and configuration requirements. - -> Auto-generated from validation rules in `case_validator.py`. - -## πŸš€ Common Configuration Patterns - -Start with these proven combinations: - - -
-πŸ’§ Multiphase Flow (Bubbles) - -```python -'model_eqns': 2, # 5-equation model -'num_fluids': 2, # Two-phase -'bubbles_euler': 'T', # Ensemble-averaged bubbles -'riemann_solver': 2, # HLLC -'avg_state': 2, # Arithmetic average -``` -
- -
-⚑ Magnetohydrodynamics (MHD) - -```python -'model_eqns': 2, # 5-equation model -'num_fluids': 1, # Single component -'mhd': 'T', # Enable MHD -'riemann_solver': 1, # HLL (or 4 for HLLD) -``` -
- -
-🌑️ Phase Change - -```python -'model_eqns': 3, # 6-equation model -'num_fluids': 2, # Two-phase -'relax': 'T', # Phase change -'riemann_solver': 2, # HLLC -``` -
- -
-πŸ’Ž Elastic Materials - -```python -'model_eqns': 2, # 5-equation model -'hypoelasticity': 'T', # Elastic solids -'riemann_solver': 1, # HLL -``` -
- -## πŸ“Š Feature Compatibility - -What works together: - - -### Physics Models - -| Feature | Requirements | Notes | -|---------|-------------|-------| -| Magnetohydrodynamics (MHD) | β€” | βœ“ General use | -| Surface Tension Model | Specific model | βœ“ General use | -| Hypoelasticity | β€” | βœ“ General use | -| Hyperelasticity | β€” | βœ“ General use | -| Phase Change (Relaxation) | β€” | βœ“ General use | -| Viscosity | β€” | βœ“ General use | -| Acoustic Sources | β€” | βœ“ General use | - - -### Bubble Models - -| Feature | Requirements | Notes | -|---------|-------------|-------| -| Euler–Euler Bubble Model | β€” | βœ“ General use | -| Euler–Lagrange Bubble Model | β€” | βœ“ General use | -| Quadrature-Based Moment Method (QBMM) | β€” | βœ“ General use | -| Polydisperse Bubble Dynamics | β€” | βœ“ General use | -| adv_n | β€” | βœ“ General use | - - -### Numerics - -| Feature | Requirements | Notes | -|---------|-------------|-------| -| Riemann Solver | Specific model | βœ“ General use | -| WENO Order | β€” | βœ“ General use | -| MUSCL Order | β€” | βœ“ General use | - - -### Geometry - -| Feature | Requirements | Notes | -|---------|-------------|-------| -| Immersed Boundaries | β€” | βœ“ General use | -| Cylindrical Coordinates | β€” | βœ“ General use | - -## πŸ”’ Model Equations - -Choose your governing equations: - - -
-Model 1: Ο€-Ξ³ (Compressible Euler) - -- **Use for:** Single-fluid compressible flow -- **Value:** `model_eqns = 1` -- **Note:** Cannot use `num_fluids`, bubbles, or certain WENO variants -
- -
-Model 2: 5-Equation (Most versatile) - -- **Use for:** Multiphase, bubbles, elastic materials, MHD -- **Value:** `model_eqns = 2` -- **Requirements:** Set `num_fluids` -- **Compatible with:** Most physics models -
- -
-Model 3: 6-Equation (Phase change) - -- **Use for:** Phase change, cavitation -- **Value:** `model_eqns = 3` -- **Requirements:** `riemann_solver = 2` (HLLC), `avg_state = 2`, `wave_speeds = 1` -- **Note:** Not compatible with bubbles or 3D cylindrical -
- -
-Model 4: 4-Equation (Single component) - -- **Use for:** Single-component flows with bubbles -- **Value:** `model_eqns = 4` -- **Requirements:** `num_fluids = 1`, set `rhoref` and `pref` -
- -## βš™οΈ Riemann Solvers - -| Solver | `riemann_solver` | Best For | Requirements | -|--------|-----------------|----------|-------------| -| **HLL** | `1` | MHD, elastic materials | β€” | -| **HLLC** | `2` | Bubbles, phase change, multiphase | `avg_state=2` for bubbles | -| **Exact** | `3` | High accuracy (expensive) | β€” | -| **HLLD** | `4` | MHD (advanced) | MHD only, no relativity | -| **Lax-Friedrichs** | `5` | Robust fallback | Not with cylindrical+viscous | - -## πŸ’§ Bubble Models - - -
-Euler-Euler (`bubbles_euler`) - -**Requirements:** -- `model_eqns = 2` or `4` -- `riemann_solver = 2` (HLLC) -- `avg_state = 2` -- Set `nb` (number of bins) β‰₯ 1 - -**Extensions:** -- `polydisperse = T`: Multiple bubble sizes (requires odd `nb > 1`) -- `qbmm = T`: Quadrature method (requires `nnode = 4`) -- `adv_n = T`: Number density advection (requires `num_fluids = 1`) -
- -
-Euler-Lagrange (`bubbles_lagrange`) - -**Requirements:** -- `n > 0` (2D or 3D only) -- `file_per_process = F` -- Not compatible with `model_eqns = 3` - -**Note:** Tracks individual bubbles -
- -## πŸ“– Quick Parameter Reference - -Key parameters and their constraints: - - -
-MHD (`mhd`) - -**Requirements:** -- relativity requires mhd to be enabled -- Powell's method requires mhd to be enabled - -**Incompatibilities:** -- Bx0 must not be set if MHD is not enabled -- Bx0 must not be set in 2D/3D MHD simulations - -
- - -
-Surface Tension (`surface_tension`) - -**Requirements:** -- sigma must be set if surface_tension is enabled -- The surface tension model requires model_eqns = 2 or model_eqns = 3 -- The surface tension model requires num_fluids = 2 - -**Valid values:** -- sigma must be greater than or equal to zero - -
- - -
-Number of Fluids (`num_fluids`) - -**Requirements:** -- 5-equation model (model_eqns = 2) requires num_fluids to be set -- 6-equation model (model_eqns = 3) requires num_fluids to be set -- 4-equation model (model_eqns = 4) requires num_fluids to be set - -**Incompatibilities:** -- num_fluids is not supported for model_eqns = 1 -- num_fluids = 1 does not support mpp_lim - -**Valid values:** -- num_fluids must be positive -- perturb_flow_fluid must be between 0 and num_fluids - -
- - -
-Cylindrical Coordinates (`cyl_coord`) - -**Incompatibilities:** -- 6-equation model (model_eqns = 3) does not support cylindrical coordinates (cyl_coord = T and p != 0) -- Bubble models untested in cylindrical coordinates -- Acoustic source is not supported in 3D cylindrical simulations - -**Valid values:** -- p must be odd for cylindrical coordinates - -
- - -
-Immersed Boundaries (`ib`) - -**Requirements:** -- Immersed Boundaries do not work in 1D (requires n > 0) - -**Incompatibilities:** -- output_partial_domain is incompatible with certain output flags - -**Valid values:** -- num_ibs must be between 1 and num_patches_max (10) - -
- - ---- - -πŸ’‘ **Tip:** If you encounter a validation error, check the relevant section above or -review [`case_validator.py`](../../toolchain/mfc/case_validator.py) for complete validation logic. - From 4137bb01ef17aee07de4562e35b4ac2fca32dd6d Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 22:48:38 -0500 Subject: [PATCH 5/7] again --- .gitignore | 1 + CMakeLists.txt | 15 + toolchain/mfc/gen_case_constraints_docs.py | 311 ++++++++++++++++++++- 3 files changed, 323 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a041d99587..2a6eea8203 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ docs/*/initial* docs/*/result* docs/documentation/*-example.png docs/documentation/examples.md +docs/documentation/case_constraints.md examples/*batch/*/ examples/**/D/* diff --git a/CMakeLists.txt b/CMakeLists.txt index d74602f7f4..751a690b55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -652,6 +652,18 @@ if (MFC_DOCUMENTATION) VERBATIM ) + # Generate case_constraints.md from case_validator.py and examples/ + add_custom_command( + OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/case_constraints.md" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/gen_case_constraints_docs.py" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/case_validator.py" + "${examples_DOCs}" + COMMAND "bash" "${CMAKE_CURRENT_SOURCE_DIR}/docs/gen_constraints.sh" + "${CMAKE_CURRENT_SOURCE_DIR}" + COMMENT "Generating case_constraints.md" + VERBATIM + ) + file(GLOB common_DOCs CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/*") # GEN_DOCS: Given a target name (herein ), this macro sets up a @@ -685,8 +697,10 @@ if (MFC_DOCUMENTATION) "${CMAKE_CURRENT_BINARY_DIR}/${target}-Doxyfile" @ONLY) set(opt_example_dependency "") + set(opt_constraints_dependency "") if (${target} STREQUAL documentation) set(opt_example_dependency "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/examples.md") + set(opt_constraints_dependency "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/case_constraints.md") endif() file(GLOB_RECURSE ${target}_DOCs CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/${target}/*") @@ -696,6 +710,7 @@ if (MFC_DOCUMENTATION) OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${target}/html/index.html" DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${target}-Doxyfile" "${opt_example_dependency}" + "${opt_constraints_dependency}" "${${target}_SRCs}" "${${target}_DOCs}" COMMAND "${DOXYGEN_EXECUTABLE}" "${target}-Doxyfile" WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py index c8bb39b7a8..8cc43da12a 100644 --- a/toolchain/mfc/gen_case_constraints_docs.py +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -4,18 +4,24 @@ Parses toolchain/mfc/case_validator.py, extracts all `self.prohibit(...)` rules, maps them to parameters and stages, and emits Markdown to stdout. + +Also generates case design playbook from curated working examples. """ from __future__ import annotations import ast +import json +import subprocess from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, List, Set, Iterable +from typing import Dict, List, Set, Iterable, Any from collections import defaultdict HERE = Path(__file__).resolve().parent CASE_VALIDATOR_PATH = HERE / "case_validator.py" +REPO_ROOT = HERE.parent.parent +EXAMPLES_DIR = REPO_ROOT / "examples" # --------------------------------------------------------------------------- @@ -303,6 +309,299 @@ def feature_title(param: str) -> str: return param +# --------------------------------------------------------------------------- +# Case Playbook Generation (from working examples) +# --------------------------------------------------------------------------- + +@dataclass +class PlaybookEntry: + """A curated example case for the playbook""" + case_dir: str + title: str + description: str + level: str # "Beginner", "Intermediate", "Advanced" + tags: List[str] + + +# Curated list of hero examples +PLAYBOOK_EXAMPLES = [ + PlaybookEntry( + "2D_shockbubble", + "2D Shock-Bubble Interaction", + "Two-fluid shock-interface benchmark. Classic validation case for compressible multiphase flows.", + "Beginner", + ["2D", "Multiphase", "Shock"] + ), + PlaybookEntry( + "1D_bubblescreen", + "1D Bubble Screen", + "Euler-Euler ensemble-averaged bubble dynamics through shock wave.", + "Intermediate", + ["1D", "Bubbles", "Euler-Euler"] + ), + PlaybookEntry( + "2D_lagrange_bubblescreen", + "2D Lagrangian Bubble Screen", + "Individual bubble tracking with Euler-Lagrange method.", + "Intermediate", + ["2D", "Bubbles", "Euler-Lagrange"] + ), + PlaybookEntry( + "2D_phasechange_bubble", + "2D Phase Change Bubble", + "Phase change and cavitation modeling with 6-equation model.", + "Advanced", + ["2D", "Phase-change", "Cavitation"] + ), + PlaybookEntry( + "2D_orszag_tang", + "2D Orszag-Tang MHD Vortex", + "Magnetohydrodynamics test problem with complex vortex structures.", + "Intermediate", + ["2D", "MHD"] + ), + PlaybookEntry( + "2D_ibm_airfoil", + "2D IBM Airfoil", + "Immersed boundary method around a NACA airfoil geometry.", + "Intermediate", + ["2D", "IBM", "Geometry"] + ), + PlaybookEntry( + "2D_viscous_shock_tube", + "2D Viscous Shock Tube", + "Shock tube with viscous effects and heat transfer.", + "Intermediate", + ["2D", "Viscous", "Shock"] + ), + PlaybookEntry( + "3D_TaylorGreenVortex", + "3D Taylor-Green Vortex", + "Classic 3D turbulence benchmark with viscous dissipation.", + "Advanced", + ["3D", "Viscous", "Turbulence"] + ), + PlaybookEntry( + "2D_IGR_triple_point", + "2D IGR Triple Point", + "Triple point problem using Iterative Generalized Riemann solver.", + "Advanced", + ["2D", "IGR", "Multiphase"] + ), +] + + +def load_case_params(case_dir: str) -> Dict[str, Any]: + """Load parameters from a case.py file""" + case_path = EXAMPLES_DIR / case_dir / "case.py" + if not case_path.exists(): + return {} + + try: + result = subprocess.run( + ["python3", str(case_path)], + capture_output=True, + text=True, + timeout=10, + check=True + ) + params = json.loads(result.stdout) + return params + except (subprocess.CalledProcessError, json.JSONDecodeError, subprocess.TimeoutExpired): + return {} + + +def summarize_case_params(params: Dict[str, Any]) -> Dict[str, Any]: + """Extract key features from case parameters""" + return { + "model_eqns": params.get("model_eqns"), + "num_fluids": params.get("num_fluids"), + "surface_tension": params.get("surface_tension") == "T", + "bubbles_euler": params.get("bubbles_euler") == "T", + "bubbles_lagrange": params.get("bubbles_lagrange") == "T", + "qbmm": params.get("qbmm") == "T", + "polydisperse": params.get("polydisperse") == "T", + "mhd": params.get("mhd") == "T", + "relax": params.get("relax") == "T", + "hypoelasticity": params.get("hypoelasticity") == "T", + "viscous": params.get("viscous") == "T", + "ib": params.get("ib") == "T", + "igr": params.get("igr") == "T", + "acoustic_source": params.get("acoustic_source") == "T", + "cyl_coord": params.get("cyl_coord") == "T", + "m": params.get("m"), + "n": params.get("n", 0), + "p": params.get("p", 0), + "recon_type": params.get("recon_type", 1), + "weno_order": params.get("weno_order"), + "muscl_order": params.get("muscl_order"), + "riemann_solver": params.get("riemann_solver"), + "time_stepper": params.get("time_stepper"), + } + + +def get_model_name(model_eqns: int | None) -> str: + """Get human-friendly model name""" + models = { + 1: "Ο€-Ξ³ (Compressible Euler)", + 2: "5-Equation (Multiphase)", + 3: "6-Equation (Phase Change)", + 4: "4-Equation (Single Component)" + } + return models.get(model_eqns, "Not specified") + + +def get_riemann_solver_name(solver: int | None) -> str: + """Get Riemann solver name""" + solvers = { + 1: "HLL", + 2: "HLLC", + 3: "Exact", + 4: "HLLD", + 5: "Lax-Friedrichs" + } + return solvers.get(solver, "Not specified") + + +def get_time_stepper_name(stepper: int | None) -> str: + """Get time stepper name""" + steppers = { + 1: "RK1 (Forward Euler)", + 2: "RK2", + 3: "RK3 (SSP)" + } + return steppers.get(stepper, "Not specified") + + +def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: # pylint: disable=too-many-branches,too-many-statements + """Render a single playbook entry as Markdown""" + lines = [] + + tags_str = " Β· ".join(entry.tags) + level_emoji = {"Beginner": "🟒", "Intermediate": "🟑", "Advanced": "πŸ”΄"}.get(entry.level, "") + + lines.append("
") + lines.append(f'{entry.title} {level_emoji} {entry.level} · {entry.case_dir}\n') + lines.append(f"**{entry.description}**\n") + lines.append(f"**Tags:** {tags_str}\n") + + lines.append("**Physics Configuration:**\n") + lines.append(f"- **Model:** {get_model_name(summary['model_eqns'])} (`model_eqns = {summary['model_eqns']}`)") + + if summary['num_fluids'] is not None: + lines.append(f"- **Number of fluids:** {summary['num_fluids']}") + + # Dimensionality + n, p = summary['n'], summary['p'] + dim_str = "3D" if p > 0 else ("2D" if n > 0 else "1D") + lines.append(f"- **Dimensionality:** {dim_str}") + + if summary['cyl_coord']: + lines.append("- **Coordinates:** Cylindrical/Axisymmetric") + + # Active features + active_features = [] + if summary['bubbles_euler']: + active_features.append("Euler-Euler bubbles") + if summary['bubbles_lagrange']: + active_features.append("Euler-Lagrange bubbles") + if summary['qbmm']: + active_features.append("QBMM") + if summary['polydisperse']: + active_features.append("Polydisperse") + if summary['surface_tension']: + active_features.append("Surface tension") + if summary['mhd']: + active_features.append("MHD") + if summary['relax']: + active_features.append("Phase change") + if summary['hypoelasticity']: + active_features.append("Hypoelasticity") + if summary['viscous']: + active_features.append("Viscous") + if summary['ib']: + active_features.append("Immersed boundaries") + if summary['igr']: + active_features.append("IGR solver") + if summary['acoustic_source']: + active_features.append("Acoustic sources") + + if active_features: + lines.append(f"- **Active features:** {', '.join(active_features)}") + + # Numerics + lines.append("\n**Numerical Methods:**\n") + + if summary['recon_type'] == 1 and summary['weno_order']: + lines.append(f"- **Reconstruction:** WENO-{summary['weno_order']}") + elif summary['recon_type'] == 2 and summary['muscl_order']: + lines.append(f"- **Reconstruction:** MUSCL (order {summary['muscl_order']})") + + if summary['riemann_solver']: + solver_name = get_riemann_solver_name(summary['riemann_solver']) + lines.append(f"- **Riemann solver:** {solver_name} (`riemann_solver = {summary['riemann_solver']}`)") + + if summary['time_stepper']: + stepper_name = get_time_stepper_name(summary['time_stepper']) + lines.append(f"- **Time stepping:** {stepper_name}") + + # Links + lines.append("\n**Related Documentation:**") + lines.append(f"- [Model Equations (model_eqns = {summary['model_eqns']})](#-model-equations)") + + if summary['riemann_solver']: + lines.append("- [Riemann Solvers](#️-riemann-solvers)") + + if summary['bubbles_euler'] or summary['bubbles_lagrange']: + lines.append("- [Bubble Models](#-bubble-models)") + + if summary['mhd']: + lines.append("- [MHD](#magnetohydrodynamics-mhd-mhd)") + + if summary['ib']: + lines.append("- [Immersed Boundaries](#immersed-boundaries-ib)") + + if summary['viscous']: + lines.append("- [Viscosity](#viscosity-viscous)") + + lines.append("\n
\n") + return "\n".join(lines) + + +def generate_playbook() -> str: + """Generate complete playbook from curated examples""" + lines = [] + + lines.append("## 🧩 Case Design Playbook\n") + lines.append( + "> **Learn by example:** The cases below are curated from MFC's `examples/` " + "directory and are validated, working configurations. " + "Use them as blueprints for building your own simulations.\n" + ) + + # Group by level + for level in ["Beginner", "Intermediate", "Advanced"]: + level_entries = [e for e in PLAYBOOK_EXAMPLES if e.level == level] + if not level_entries: + continue + + level_emoji = {"Beginner": "🟒", "Intermediate": "🟑", "Advanced": "πŸ”΄"}.get(level, "") + lines.append(f"\n### {level_emoji} {level} Examples\n") + + for entry in level_entries: + try: + params = load_case_params(entry.case_dir) + if not params: + continue + summary = summarize_case_params(params) + card = render_playbook_card(entry, summary) + lines.append(card) + except Exception: # pylint: disable=broad-except + continue + + return "\n".join(lines) + + # --------------------------------------------------------------------------- # Markdown rendering # --------------------------------------------------------------------------- @@ -321,15 +620,19 @@ def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-l lines: List[str] = [] - lines.append("# MFC Feature Compatibility Guide\n") + lines.append("# Case Creator Guide\n") lines.append( - "> **Quick reference** for understanding which MFC features work together " + "> **Quick reference** for building MFC cases: working examples, compatibility rules, " "and configuration requirements.\n" ) lines.append( - "> Auto-generated from validation rules in `case_validator.py`.\n" + "> Auto-generated from `case_validator.py` and `examples/`.\n" ) + # Add playbook at the top + playbook = generate_playbook() + lines.append(playbook) + # Define major feature groups (excluding IGR) major_features = { "Physics Models": ["mhd", "surface_tension", "hypoelasticity", "hyperelasticity", "relax", "viscous", "acoustic_source"], From ec6f33aee12ebed7df80e24e51153d04643ef98e Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 23:17:55 -0500 Subject: [PATCH 6/7] fail --- toolchain/mfc/gen_case_constraints_docs.py | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py index 8cc43da12a..daac233e20 100644 --- a/toolchain/mfc/gen_case_constraints_docs.py +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -12,6 +12,7 @@ import ast import json +import sys import subprocess from dataclasses import dataclass, field from pathlib import Path @@ -391,6 +392,26 @@ class PlaybookEntry: ] +def validate_playbook_examples(): + """Check that all curated examples exist and error if any are missing""" + missing = [] + for entry in PLAYBOOK_EXAMPLES: + case_path = EXAMPLES_DIR / entry.case_dir / "case.py" + if not case_path.exists(): + missing.append(entry.case_dir) + + if missing: + print("=" * 70, file=sys.stderr) + print("ERROR: Missing playbook examples:", file=sys.stderr) + for example in missing: + print(f" - {example}", file=sys.stderr) + print("\nPlease update PLAYBOOK_EXAMPLES in:", file=sys.stderr) + print(f" {Path(__file__).relative_to(REPO_ROOT)}", file=sys.stderr) + print("\nRemove the missing examples from the list or restore them.", file=sys.stderr) + print("=" * 70, file=sys.stderr) + sys.exit(1) + + def load_case_params(case_dir: str) -> Dict[str, Any]: """Load parameters from a case.py file""" case_path = EXAMPLES_DIR / case_dir / "case.py" @@ -572,6 +593,9 @@ def generate_playbook() -> str: """Generate complete playbook from curated examples""" lines = [] + # Validate examples - will exit(1) if any are missing + validate_playbook_examples() + lines.append("## 🧩 Case Design Playbook\n") lines.append( "> **Learn by example:** The cases below are curated from MFC's `examples/` " From dac20d03496717e4d45820b37e6ada530c8fab22 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sun, 30 Nov 2025 23:58:50 -0500 Subject: [PATCH 7/7] cleanup --- docs/Doxyfile.in | 3 +-- toolchain/mfc/gen_case_constraints_docs.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 114df3f720..26e1a0156b 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1163,8 +1163,7 @@ HTML_STYLESHEET = # list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_EXTRA_STYLESHEET = @DOXYGEN_HTML_EXTRA_STYLESHEET@ \ - @CMAKE_SOURCE_DIR@/docs/custom-dark-fixes.css +HTML_EXTRA_STYLESHEET = @DOXYGEN_HTML_EXTRA_STYLESHEET@ # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py index daac233e20..2d31e0789d 100644 --- a/toolchain/mfc/gen_case_constraints_docs.py +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -428,7 +428,8 @@ def load_case_params(case_dir: str) -> Dict[str, Any]: ) params = json.loads(result.stdout) return params - except (subprocess.CalledProcessError, json.JSONDecodeError, subprocess.TimeoutExpired): + except (subprocess.CalledProcessError, json.JSONDecodeError, subprocess.TimeoutExpired) as e: + print(f"WARNING: Failed to load params from {case_path}: {e}", file=sys.stderr) return {} @@ -620,7 +621,8 @@ def generate_playbook() -> str: summary = summarize_case_params(params) card = render_playbook_card(entry, summary) lines.append(card) - except Exception: # pylint: disable=broad-except + except Exception as e: # pylint: disable=broad-except + print(f"WARNING: Failed to process playbook entry '{entry.case_dir}': {e}", file=sys.stderr) continue return "\n".join(lines)