Skip to content

Commit 4ca85e0

Browse files
sbryngelsonclaude
andcommitted
Add 4 staleness-prevention lint checks and complete PHYSICS_DOCS coverage
New lint checks in lint_docs.py: - check_physics_docs_coverage: flags check methods missing PHYSICS_DOCS entries - check_identifier_refs: validates Python identifiers in contributing.md (PHYSICS_DOCS, CONSTRAINTS, DEPENDENCIES, REGISTRY) still exist in source - check_cli_refs: validates ./mfc.sh commands in running.md match CLI schema - Expanded DOCS list to cover all hand-written docs for file path validation Added PHYSICS_DOCS entries for 16 previously undocumented check methods and added 11 mechanical/structural methods to the skip set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2f95da3 commit 4ca85e0

2 files changed

Lines changed: 286 additions & 1 deletion

File tree

toolchain/mfc/case_validator.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,145 @@
242242
"Global dimensions must be even. Incompatible with cylindrical coordinates."
243243
),
244244
},
245+
"check_adaptive_time_stepping": {
246+
"title": "Adaptive Time Stepping",
247+
"category": "Numerical Schemes",
248+
"explanation": (
249+
"Requires RK3 time stepper. Only compatible with polytropic or "
250+
"Lagrangian bubble models. Incompatible with QBMM."
251+
),
252+
},
253+
"check_adv_n": {
254+
"title": "Bubble Advection",
255+
"category": "Bubble Physics",
256+
"explanation": (
257+
"Bubble advection (adv_n) requires single-fluid Euler bubble solver. "
258+
"Incompatible with QBMM."
259+
),
260+
},
261+
"check_body_forces": {
262+
"title": "Body Forces",
263+
"category": "Feature Compatibility",
264+
"explanation": (
265+
"Body force parameters (k, w, p, g components) must be fully specified "
266+
"for each active dimension."
267+
),
268+
},
269+
"check_continuum_damage": {
270+
"title": "Continuum Damage Modeling",
271+
"category": "Feature Compatibility",
272+
"explanation": (
273+
"Requires model_eqns = 2 and specification of tau_star, "
274+
"cont_damage_s, and alpha_bar parameters."
275+
),
276+
},
277+
"check_grcbc": {
278+
"title": "Generalized Relaxation CBC",
279+
"category": "Boundary Conditions",
280+
"explanation": (
281+
"GRCBC requires specific BC values: -7 (inflow) or -8 (outflow). "
282+
"Inflow requires velocity and pressure specification."
283+
),
284+
},
285+
"check_hyperelasticity": {
286+
"title": "Hyperelasticity",
287+
"category": "Feature Compatibility",
288+
"explanation": (
289+
"Pre-stress requires hyperelasticity. Not supported with "
290+
"model_eqns = 1. Requires HLL Riemann solver."
291+
),
292+
},
293+
"check_ibm": {
294+
"title": "Immersed Boundary Method",
295+
"category": "Domain and Geometry",
296+
"explanation": (
297+
"Requires 2D or 3D (n > 0). Number of immersed boundaries "
298+
"must be between 1 and 10."
299+
),
300+
},
301+
"check_igr_simulation": {
302+
"title": "IGR Simulation Constraints",
303+
"category": "Numerical Schemes",
304+
"explanation": (
305+
"Iterative ghost-cell remapping has numerous incompatibilities "
306+
"including polytropic, bubbles, surface tension, MHD, and elasticity."
307+
),
308+
},
309+
"check_mhd_simulation": {
310+
"title": "MHD Simulation Constraints",
311+
"category": "Feature Compatibility",
312+
"explanation": (
313+
"MHD requires HLL or HLLD Riemann solver. HLLD unavailable for "
314+
"relativistic MHD. Hyperbolic divergence cleaning not supported in 1D."
315+
),
316+
},
317+
"check_model_eqns_simulation": {
318+
"title": "Six-Equation Model Constraints",
319+
"category": "Model Equations",
320+
"explanation": (
321+
"The six-equation model (model_eqns = 3) requires arithmetic "
322+
"averaging and specific wave speed calculation methods."
323+
),
324+
"references": ["Saurel09"],
325+
},
326+
"check_muscl_simulation": {
327+
"title": "MUSCL Simulation Constraints",
328+
"category": "Numerical Schemes",
329+
"explanation": (
330+
"Second-order MUSCL reconstruction requires a flux limiter "
331+
"selection (muscl_lim in 1-5)."
332+
),
333+
},
334+
"check_partial_density": {
335+
"title": "Partial Density Output",
336+
"category": "Post-Processing",
337+
"explanation": (
338+
"Multi-phase partial density output is incompatible with "
339+
"model_eqns = 1 (single-fluid gamma-law)."
340+
),
341+
},
342+
"check_qbmm_and_polydisperse": {
343+
"title": "QBMM and Polydisperse Bubbles",
344+
"category": "Bubble Physics",
345+
"explanation": (
346+
"Both require Euler bubble solver. Polydisperse requires "
347+
"poly_sigma > 0. QBMM requires nnode = 4."
348+
),
349+
"references": ["Bryngelson21"],
350+
},
351+
"check_riemann_solver": {
352+
"title": "Riemann Solver Selection",
353+
"category": "Numerical Schemes",
354+
"explanation": (
355+
"Five solvers available with different model compatibility. "
356+
"Six-equation model requires HLLC. Exact solver does not support "
357+
"wave_speeds. Low-Mach correction 2 requires HLLC."
358+
),
359+
},
360+
"check_schlieren": {
361+
"title": "Schlieren Visualization",
362+
"category": "Post-Processing",
363+
"explanation": (
364+
"Requires finite difference order and at least 2D (n > 0). "
365+
"Each schlieren_alpha coefficient must be positive."
366+
),
367+
},
368+
"check_volume_fraction": {
369+
"title": "Volume Fraction Output",
370+
"category": "Post-Processing",
371+
"explanation": (
372+
"Volume fraction output is incompatible with model_eqns = 1 "
373+
"(single-fluid gamma-law). Requires multi-component model."
374+
),
375+
},
376+
"check_weno_simulation": {
377+
"title": "WENO Simulation Constraints",
378+
"category": "Numerical Schemes",
379+
"explanation": (
380+
"Five WENO variants with mutual exclusivity constraints. "
381+
"weno_avg incompatible with model_eqns = 1."
382+
),
383+
},
245384
}
246385

247386

toolchain/mfc/lint_docs.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
import sys
55
from pathlib import Path
66

7-
# Docs to scan for file path references
7+
# Docs to scan for file path references (all hand-written docs with code refs)
88
DOCS = [
99
"docs/documentation/contributing.md",
1010
"docs/documentation/gpuParallelization.md",
1111
"docs/documentation/running.md",
1212
"docs/documentation/case.md",
1313
"docs/documentation/equations.md",
14+
"docs/documentation/testing.md",
15+
"docs/documentation/getting-started.md",
16+
"docs/documentation/docker.md",
17+
"docs/documentation/troubleshooting.md",
18+
"docs/documentation/visualization.md",
19+
"docs/documentation/expectedPerformance.md",
1420
".github/copilot-instructions.md",
1521
]
1622

@@ -386,6 +392,143 @@ def check_page_refs(repo_root: Path) -> list[str]:
386392
return errors
387393

388394

395+
def check_physics_docs_coverage(repo_root: Path) -> list[str]:
396+
"""Check that all check methods with enforcement calls have PHYSICS_DOCS entries."""
397+
toolchain_dir = str(repo_root / "toolchain")
398+
if toolchain_dir not in sys.path:
399+
sys.path.insert(0, toolchain_dir)
400+
try:
401+
from mfc.case_validator import PHYSICS_DOCS # pylint: disable=import-outside-toplevel
402+
from mfc.params.ast_analyzer import analyze_case_validator # pylint: disable=import-outside-toplevel
403+
except ImportError:
404+
return []
405+
406+
# Methods that are purely structural/mechanical and don't need physics docs
407+
skip = {
408+
"check_parameter_types", # type validation, not physics
409+
"check_output_format", # output format selection
410+
"check_restart", # restart file logistics
411+
"check_parallel_io_pre_process", # parallel I/O settings
412+
"check_misc_pre_process", # miscellaneous pre-process flags
413+
"check_bc_patches", # boundary patch geometry
414+
"check_grid_stretching", # grid stretching parameters
415+
"check_qbmm_pre_process", # QBMM pre-process settings
416+
"check_probe_integral_output", # probe/integral output settings
417+
"check_finite_difference", # fd_order value validation
418+
"check_flux_limiter", # output dimension requirements
419+
"check_liutex_post", # output dimension requirements
420+
"check_momentum_post", # output dimension requirements
421+
"check_velocity_post", # output dimension requirements
422+
"check_surface_tension_post", # output feature dependency
423+
"check_no_flow_variables", # output variable selection
424+
"check_partial_domain", # output format settings
425+
"check_perturb_density", # parameter pairing validation
426+
"check_qm", # output dimension requirements
427+
"check_chemistry", # runtime Cantera validation only
428+
}
429+
430+
validator_path = repo_root / "toolchain" / "mfc" / "case_validator.py"
431+
analysis = analyze_case_validator(validator_path)
432+
rules = analysis["rules"]
433+
434+
# Find methods that have at least one prohibit/warn call
435+
methods_with_rules = {r.method for r in rules}
436+
437+
errors = []
438+
for method in sorted(methods_with_rules):
439+
if method in PHYSICS_DOCS:
440+
continue
441+
if method in skip:
442+
continue
443+
errors.append(
444+
f" {method} has validation rules but no PHYSICS_DOCS entry."
445+
" Fix: add entry to PHYSICS_DOCS in case_validator.py"
446+
" or add to skip set in lint_docs.py"
447+
)
448+
449+
return errors
450+
451+
452+
# Important Python identifiers in contributing.md mapped to files where they must exist.
453+
# If an identifier is renamed or removed, this check catches the stale doc reference.
454+
_CONTRIBUTING_IDENTIFIERS = {
455+
"PHYSICS_DOCS": "toolchain/mfc/case_validator.py",
456+
"CONSTRAINTS": "toolchain/mfc/params/definitions.py",
457+
"DEPENDENCIES": "toolchain/mfc/params/definitions.py",
458+
"REGISTRY": "toolchain/mfc/params/__init__.py",
459+
}
460+
461+
462+
def check_identifier_refs(repo_root: Path) -> list[str]:
463+
"""Check that important identifiers referenced in contributing.md still exist."""
464+
doc_path = repo_root / "docs" / "documentation" / "contributing.md"
465+
if not doc_path.exists():
466+
return []
467+
468+
text = _strip_code_blocks(doc_path.read_text(encoding="utf-8"))
469+
errors = []
470+
471+
for identifier, source_file in _CONTRIBUTING_IDENTIFIERS.items():
472+
# Check identifier is actually referenced in the doc
473+
if f"`{identifier}" not in text:
474+
continue
475+
source_path = repo_root / source_file
476+
if not source_path.exists():
477+
errors.append(
478+
f" contributing.md references `{identifier}` in {source_file}"
479+
f" but {source_file} does not exist"
480+
)
481+
continue
482+
source_text = source_path.read_text(encoding="utf-8")
483+
if identifier not in source_text:
484+
errors.append(
485+
f" contributing.md references `{identifier}` but it was not"
486+
f" found in {source_file}. Fix: update the docs or the identifier"
487+
)
488+
489+
return errors
490+
491+
492+
# Match ./mfc.sh <command> patterns (the subcommand name)
493+
_CLI_CMD_RE = re.compile(r"\./mfc\.sh\s+([a-z][a-z_-]*)")
494+
495+
496+
def check_cli_refs(repo_root: Path) -> list[str]:
497+
"""Check that ./mfc.sh commands referenced in docs exist in the CLI schema."""
498+
toolchain_dir = str(repo_root / "toolchain")
499+
if toolchain_dir not in sys.path:
500+
sys.path.insert(0, toolchain_dir)
501+
try:
502+
from mfc.cli.commands import MFC_CLI_SCHEMA # pylint: disable=import-outside-toplevel
503+
except ImportError:
504+
return []
505+
506+
valid_commands = {cmd.name for cmd in MFC_CLI_SCHEMA.commands}
507+
# Also accept "init" (shell function) and "load" (shell function)
508+
valid_commands.update({"init", "load"})
509+
510+
errors = []
511+
doc_path = repo_root / "docs" / "documentation" / "running.md"
512+
if not doc_path.exists():
513+
return []
514+
515+
text = _strip_code_blocks(doc_path.read_text(encoding="utf-8"))
516+
seen = set()
517+
for match in _CLI_CMD_RE.finditer(text):
518+
cmd = match.group(1)
519+
if cmd in seen or cmd in valid_commands:
520+
seen.add(cmd)
521+
continue
522+
seen.add(cmd)
523+
errors.append(
524+
f" running.md references './mfc.sh {cmd}' but '{cmd}'"
525+
" is not a known CLI command."
526+
" Fix: update the command name or remove the reference"
527+
)
528+
529+
return errors
530+
531+
389532
def main():
390533
repo_root = Path(__file__).resolve().parents[2]
391534

@@ -397,6 +540,9 @@ def main():
397540
all_errors.extend(check_math_syntax(repo_root))
398541
all_errors.extend(check_doxygen_percent(repo_root))
399542
all_errors.extend(check_section_anchors(repo_root))
543+
all_errors.extend(check_physics_docs_coverage(repo_root))
544+
all_errors.extend(check_identifier_refs(repo_root))
545+
all_errors.extend(check_cli_refs(repo_root))
400546

401547
if all_errors:
402548
print("Doc reference check failed:")

0 commit comments

Comments
 (0)