44
55Parses toolchain/mfc/case_validator.py, extracts all `self.prohibit(...)` rules,
66maps them to parameters and stages, and emits Markdown to stdout.
7+
8+ Also generates case design playbook from curated working examples.
79"""
810
911from __future__ import annotations
1012
1113import ast
14+ import json
15+ import subprocess
1216from dataclasses import dataclass , field
1317from pathlib import Path
14- from typing import Dict , List , Set , Iterable
18+ from typing import Dict , List , Set , Iterable , Any
1519from collections import defaultdict
1620
1721HERE = Path (__file__ ).resolve ().parent
1822CASE_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