Skip to content

Commit 4137bb0

Browse files
committed
again
1 parent 6fd5be2 commit 4137bb0

File tree

3 files changed

+323
-4
lines changed

3 files changed

+323
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ docs/*/initial*
3636
docs/*/result*
3737
docs/documentation/*-example.png
3838
docs/documentation/examples.md
39+
docs/documentation/case_constraints.md
3940

4041
examples/*batch/*/
4142
examples/**/D/*

CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,18 @@ if (MFC_DOCUMENTATION)
652652
VERBATIM
653653
)
654654

655+
# Generate case_constraints.md from case_validator.py and examples/
656+
add_custom_command(
657+
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/case_constraints.md"
658+
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/gen_case_constraints_docs.py"
659+
"${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/case_validator.py"
660+
"${examples_DOCs}"
661+
COMMAND "bash" "${CMAKE_CURRENT_SOURCE_DIR}/docs/gen_constraints.sh"
662+
"${CMAKE_CURRENT_SOURCE_DIR}"
663+
COMMENT "Generating case_constraints.md"
664+
VERBATIM
665+
)
666+
655667
file(GLOB common_DOCs CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/*")
656668

657669
# GEN_DOCS: Given a target name (herein <target>), this macro sets up a
@@ -685,8 +697,10 @@ if (MFC_DOCUMENTATION)
685697
"${CMAKE_CURRENT_BINARY_DIR}/${target}-Doxyfile" @ONLY)
686698

687699
set(opt_example_dependency "")
700+
set(opt_constraints_dependency "")
688701
if (${target} STREQUAL documentation)
689702
set(opt_example_dependency "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/examples.md")
703+
set(opt_constraints_dependency "${CMAKE_CURRENT_SOURCE_DIR}/docs/documentation/case_constraints.md")
690704
endif()
691705

692706
file(GLOB_RECURSE ${target}_DOCs CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/docs/${target}/*")
@@ -696,6 +710,7 @@ if (MFC_DOCUMENTATION)
696710
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${target}/html/index.html"
697711
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${target}-Doxyfile"
698712
"${opt_example_dependency}"
713+
"${opt_constraints_dependency}"
699714
"${${target}_SRCs}" "${${target}_DOCs}"
700715
COMMAND "${DOXYGEN_EXECUTABLE}" "${target}-Doxyfile"
701716
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"

toolchain/mfc/gen_case_constraints_docs.py

Lines changed: 307 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@
44
55
Parses toolchain/mfc/case_validator.py, extracts all `self.prohibit(...)` rules,
66
maps them to parameters and stages, and emits Markdown to stdout.
7+
8+
Also generates case design playbook from curated working examples.
79
"""
810

911
from __future__ import annotations
1012

1113
import ast
14+
import json
15+
import subprocess
1216
from dataclasses import dataclass, field
1317
from pathlib import Path
14-
from typing import Dict, List, Set, Iterable
18+
from typing import Dict, List, Set, Iterable, Any
1519
from collections import defaultdict
1620

1721
HERE = Path(__file__).resolve().parent
1822
CASE_VALIDATOR_PATH = HERE / "case_validator.py"
23+
REPO_ROOT = HERE.parent.parent
24+
EXAMPLES_DIR = REPO_ROOT / "examples"
1925

2026

2127
# ---------------------------------------------------------------------------
@@ -303,6 +309,299 @@ def feature_title(param: str) -> str:
303309
return param
304310

305311

312+
# ---------------------------------------------------------------------------
313+
# Case Playbook Generation (from working examples)
314+
# ---------------------------------------------------------------------------
315+
316+
@dataclass
317+
class PlaybookEntry:
318+
"""A curated example case for the playbook"""
319+
case_dir: str
320+
title: str
321+
description: str
322+
level: str # "Beginner", "Intermediate", "Advanced"
323+
tags: List[str]
324+
325+
326+
# Curated list of hero examples
327+
PLAYBOOK_EXAMPLES = [
328+
PlaybookEntry(
329+
"2D_shockbubble",
330+
"2D Shock-Bubble Interaction",
331+
"Two-fluid shock-interface benchmark. Classic validation case for compressible multiphase flows.",
332+
"Beginner",
333+
["2D", "Multiphase", "Shock"]
334+
),
335+
PlaybookEntry(
336+
"1D_bubblescreen",
337+
"1D Bubble Screen",
338+
"Euler-Euler ensemble-averaged bubble dynamics through shock wave.",
339+
"Intermediate",
340+
["1D", "Bubbles", "Euler-Euler"]
341+
),
342+
PlaybookEntry(
343+
"2D_lagrange_bubblescreen",
344+
"2D Lagrangian Bubble Screen",
345+
"Individual bubble tracking with Euler-Lagrange method.",
346+
"Intermediate",
347+
["2D", "Bubbles", "Euler-Lagrange"]
348+
),
349+
PlaybookEntry(
350+
"2D_phasechange_bubble",
351+
"2D Phase Change Bubble",
352+
"Phase change and cavitation modeling with 6-equation model.",
353+
"Advanced",
354+
["2D", "Phase-change", "Cavitation"]
355+
),
356+
PlaybookEntry(
357+
"2D_orszag_tang",
358+
"2D Orszag-Tang MHD Vortex",
359+
"Magnetohydrodynamics test problem with complex vortex structures.",
360+
"Intermediate",
361+
["2D", "MHD"]
362+
),
363+
PlaybookEntry(
364+
"2D_ibm_airfoil",
365+
"2D IBM Airfoil",
366+
"Immersed boundary method around a NACA airfoil geometry.",
367+
"Intermediate",
368+
["2D", "IBM", "Geometry"]
369+
),
370+
PlaybookEntry(
371+
"2D_viscous_shock_tube",
372+
"2D Viscous Shock Tube",
373+
"Shock tube with viscous effects and heat transfer.",
374+
"Intermediate",
375+
["2D", "Viscous", "Shock"]
376+
),
377+
PlaybookEntry(
378+
"3D_TaylorGreenVortex",
379+
"3D Taylor-Green Vortex",
380+
"Classic 3D turbulence benchmark with viscous dissipation.",
381+
"Advanced",
382+
["3D", "Viscous", "Turbulence"]
383+
),
384+
PlaybookEntry(
385+
"2D_IGR_triple_point",
386+
"2D IGR Triple Point",
387+
"Triple point problem using Iterative Generalized Riemann solver.",
388+
"Advanced",
389+
["2D", "IGR", "Multiphase"]
390+
),
391+
]
392+
393+
394+
def load_case_params(case_dir: str) -> Dict[str, Any]:
395+
"""Load parameters from a case.py file"""
396+
case_path = EXAMPLES_DIR / case_dir / "case.py"
397+
if not case_path.exists():
398+
return {}
399+
400+
try:
401+
result = subprocess.run(
402+
["python3", str(case_path)],
403+
capture_output=True,
404+
text=True,
405+
timeout=10,
406+
check=True
407+
)
408+
params = json.loads(result.stdout)
409+
return params
410+
except (subprocess.CalledProcessError, json.JSONDecodeError, subprocess.TimeoutExpired):
411+
return {}
412+
413+
414+
def summarize_case_params(params: Dict[str, Any]) -> Dict[str, Any]:
415+
"""Extract key features from case parameters"""
416+
return {
417+
"model_eqns": params.get("model_eqns"),
418+
"num_fluids": params.get("num_fluids"),
419+
"surface_tension": params.get("surface_tension") == "T",
420+
"bubbles_euler": params.get("bubbles_euler") == "T",
421+
"bubbles_lagrange": params.get("bubbles_lagrange") == "T",
422+
"qbmm": params.get("qbmm") == "T",
423+
"polydisperse": params.get("polydisperse") == "T",
424+
"mhd": params.get("mhd") == "T",
425+
"relax": params.get("relax") == "T",
426+
"hypoelasticity": params.get("hypoelasticity") == "T",
427+
"viscous": params.get("viscous") == "T",
428+
"ib": params.get("ib") == "T",
429+
"igr": params.get("igr") == "T",
430+
"acoustic_source": params.get("acoustic_source") == "T",
431+
"cyl_coord": params.get("cyl_coord") == "T",
432+
"m": params.get("m"),
433+
"n": params.get("n", 0),
434+
"p": params.get("p", 0),
435+
"recon_type": params.get("recon_type", 1),
436+
"weno_order": params.get("weno_order"),
437+
"muscl_order": params.get("muscl_order"),
438+
"riemann_solver": params.get("riemann_solver"),
439+
"time_stepper": params.get("time_stepper"),
440+
}
441+
442+
443+
def get_model_name(model_eqns: int | None) -> str:
444+
"""Get human-friendly model name"""
445+
models = {
446+
1: "π-γ (Compressible Euler)",
447+
2: "5-Equation (Multiphase)",
448+
3: "6-Equation (Phase Change)",
449+
4: "4-Equation (Single Component)"
450+
}
451+
return models.get(model_eqns, "Not specified")
452+
453+
454+
def get_riemann_solver_name(solver: int | None) -> str:
455+
"""Get Riemann solver name"""
456+
solvers = {
457+
1: "HLL",
458+
2: "HLLC",
459+
3: "Exact",
460+
4: "HLLD",
461+
5: "Lax-Friedrichs"
462+
}
463+
return solvers.get(solver, "Not specified")
464+
465+
466+
def get_time_stepper_name(stepper: int | None) -> str:
467+
"""Get time stepper name"""
468+
steppers = {
469+
1: "RK1 (Forward Euler)",
470+
2: "RK2",
471+
3: "RK3 (SSP)"
472+
}
473+
return steppers.get(stepper, "Not specified")
474+
475+
476+
def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: # pylint: disable=too-many-branches,too-many-statements
477+
"""Render a single playbook entry as Markdown"""
478+
lines = []
479+
480+
tags_str = " · ".join(entry.tags)
481+
level_emoji = {"Beginner": "🟢", "Intermediate": "🟡", "Advanced": "🔴"}.get(entry.level, "")
482+
483+
lines.append("<details>")
484+
lines.append(f'<summary><b>{entry.title}</b> {level_emoji} <i>{entry.level}</i> · <code>{entry.case_dir}</code></summary>\n')
485+
lines.append(f"**{entry.description}**\n")
486+
lines.append(f"**Tags:** {tags_str}\n")
487+
488+
lines.append("**Physics Configuration:**\n")
489+
lines.append(f"- **Model:** {get_model_name(summary['model_eqns'])} (`model_eqns = {summary['model_eqns']}`)")
490+
491+
if summary['num_fluids'] is not None:
492+
lines.append(f"- **Number of fluids:** {summary['num_fluids']}")
493+
494+
# Dimensionality
495+
n, p = summary['n'], summary['p']
496+
dim_str = "3D" if p > 0 else ("2D" if n > 0 else "1D")
497+
lines.append(f"- **Dimensionality:** {dim_str}")
498+
499+
if summary['cyl_coord']:
500+
lines.append("- **Coordinates:** Cylindrical/Axisymmetric")
501+
502+
# Active features
503+
active_features = []
504+
if summary['bubbles_euler']:
505+
active_features.append("Euler-Euler bubbles")
506+
if summary['bubbles_lagrange']:
507+
active_features.append("Euler-Lagrange bubbles")
508+
if summary['qbmm']:
509+
active_features.append("QBMM")
510+
if summary['polydisperse']:
511+
active_features.append("Polydisperse")
512+
if summary['surface_tension']:
513+
active_features.append("Surface tension")
514+
if summary['mhd']:
515+
active_features.append("MHD")
516+
if summary['relax']:
517+
active_features.append("Phase change")
518+
if summary['hypoelasticity']:
519+
active_features.append("Hypoelasticity")
520+
if summary['viscous']:
521+
active_features.append("Viscous")
522+
if summary['ib']:
523+
active_features.append("Immersed boundaries")
524+
if summary['igr']:
525+
active_features.append("IGR solver")
526+
if summary['acoustic_source']:
527+
active_features.append("Acoustic sources")
528+
529+
if active_features:
530+
lines.append(f"- **Active features:** {', '.join(active_features)}")
531+
532+
# Numerics
533+
lines.append("\n**Numerical Methods:**\n")
534+
535+
if summary['recon_type'] == 1 and summary['weno_order']:
536+
lines.append(f"- **Reconstruction:** WENO-{summary['weno_order']}")
537+
elif summary['recon_type'] == 2 and summary['muscl_order']:
538+
lines.append(f"- **Reconstruction:** MUSCL (order {summary['muscl_order']})")
539+
540+
if summary['riemann_solver']:
541+
solver_name = get_riemann_solver_name(summary['riemann_solver'])
542+
lines.append(f"- **Riemann solver:** {solver_name} (`riemann_solver = {summary['riemann_solver']}`)")
543+
544+
if summary['time_stepper']:
545+
stepper_name = get_time_stepper_name(summary['time_stepper'])
546+
lines.append(f"- **Time stepping:** {stepper_name}")
547+
548+
# Links
549+
lines.append("\n**Related Documentation:**")
550+
lines.append(f"- [Model Equations (model_eqns = {summary['model_eqns']})](#-model-equations)")
551+
552+
if summary['riemann_solver']:
553+
lines.append("- [Riemann Solvers](#️-riemann-solvers)")
554+
555+
if summary['bubbles_euler'] or summary['bubbles_lagrange']:
556+
lines.append("- [Bubble Models](#-bubble-models)")
557+
558+
if summary['mhd']:
559+
lines.append("- [MHD](#magnetohydrodynamics-mhd-mhd)")
560+
561+
if summary['ib']:
562+
lines.append("- [Immersed Boundaries](#immersed-boundaries-ib)")
563+
564+
if summary['viscous']:
565+
lines.append("- [Viscosity](#viscosity-viscous)")
566+
567+
lines.append("\n</details>\n")
568+
return "\n".join(lines)
569+
570+
571+
def generate_playbook() -> str:
572+
"""Generate complete playbook from curated examples"""
573+
lines = []
574+
575+
lines.append("## 🧩 Case Design Playbook\n")
576+
lines.append(
577+
"> **Learn by example:** The cases below are curated from MFC's `examples/` "
578+
"directory and are validated, working configurations. "
579+
"Use them as blueprints for building your own simulations.\n"
580+
)
581+
582+
# Group by level
583+
for level in ["Beginner", "Intermediate", "Advanced"]:
584+
level_entries = [e for e in PLAYBOOK_EXAMPLES if e.level == level]
585+
if not level_entries:
586+
continue
587+
588+
level_emoji = {"Beginner": "🟢", "Intermediate": "🟡", "Advanced": "🔴"}.get(level, "")
589+
lines.append(f"\n### {level_emoji} {level} Examples\n")
590+
591+
for entry in level_entries:
592+
try:
593+
params = load_case_params(entry.case_dir)
594+
if not params:
595+
continue
596+
summary = summarize_case_params(params)
597+
card = render_playbook_card(entry, summary)
598+
lines.append(card)
599+
except Exception: # pylint: disable=broad-except
600+
continue
601+
602+
return "\n".join(lines)
603+
604+
306605
# ---------------------------------------------------------------------------
307606
# Markdown rendering
308607
# ---------------------------------------------------------------------------
@@ -321,15 +620,19 @@ def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-l
321620

322621
lines: List[str] = []
323622

324-
lines.append("# MFC Feature Compatibility Guide\n")
623+
lines.append("# Case Creator Guide\n")
325624
lines.append(
326-
"> **Quick reference** for understanding which MFC features work together "
625+
"> **Quick reference** for building MFC cases: working examples, compatibility rules, "
327626
"and configuration requirements.\n"
328627
)
329628
lines.append(
330-
"> Auto-generated from validation rules in `case_validator.py`.\n"
629+
"> Auto-generated from `case_validator.py` and `examples/`.\n"
331630
)
332631

632+
# Add playbook at the top
633+
playbook = generate_playbook()
634+
lines.append(playbook)
635+
333636
# Define major feature groups (excluding IGR)
334637
major_features = {
335638
"Physics Models": ["mhd", "surface_tension", "hypoelasticity", "hyperelasticity", "relax", "viscous", "acoustic_source"],

0 commit comments

Comments
 (0)