From 8fed09a28c91fcf60b47761996363843b81f5741 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Tue, 27 Jan 2026 20:34:15 -0500 Subject: [PATCH 001/152] Quality of life improvements for MFC toolchain - Add validate command for pre-flight case validation without building - Add clean command implementation (was previously missing) - Show detailed CMake/compiler errors on build failure with formatted panels - Add build progress indicators (Configuring/Building/Installing status) - Display human-readable test names prominently in test output - Add real-time test failure feedback with helpful hints - Enhanced case validator error messages with suggestions - Add --debug-log flag for troubleshooting - Create troubleshooting documentation Co-Authored-By: Claude Opus 4.5 --- docs/documentation/troubleshooting.md | 149 ++++++++++++++++++++++++++ toolchain/main.py | 12 ++- toolchain/mfc/args.py | 11 +- toolchain/mfc/build.py | 72 ++++++++++++- toolchain/mfc/case_validator.py | 27 ++++- toolchain/mfc/clean.py | 22 ++++ toolchain/mfc/common.py | 27 ++++- toolchain/mfc/test/test.py | 38 ++++++- toolchain/mfc/validate.py | 58 ++++++++++ 9 files changed, 400 insertions(+), 16 deletions(-) create mode 100644 docs/documentation/troubleshooting.md create mode 100644 toolchain/mfc/clean.py create mode 100644 toolchain/mfc/validate.py diff --git a/docs/documentation/troubleshooting.md b/docs/documentation/troubleshooting.md new file mode 100644 index 0000000000..68fc18951f --- /dev/null +++ b/docs/documentation/troubleshooting.md @@ -0,0 +1,149 @@ +# Troubleshooting Guide + +This guide covers common issues you may encounter when building, running, or testing MFC. + +## Build Errors + +### "CMake could not find MPI" + +**Cause:** MPI is not installed or not in your PATH. + +**Fix:** +- **Ubuntu/Debian:** `sudo apt install libopenmpi-dev openmpi-bin` +- **macOS (Homebrew):** `brew install open-mpi` +- **HPC systems:** `module load openmpi` (or similar) + +After installing, verify with: `mpirun --version` + +### "CMake could not find a Fortran compiler" + +**Cause:** No Fortran compiler is installed or not in PATH. + +**Fix:** +- **Ubuntu/Debian:** `sudo apt install gfortran` +- **macOS (Homebrew):** `brew install gcc` +- **HPC systems:** `module load gcc` or `module load nvhpc` + +### "Fypp preprocessing failed" + +**Cause:** Usually a syntax error in `.fpp` files or missing Fypp. + +**Fix:** +1. Ensure Fypp is installed: `pip install fypp` +2. Check the specific error line mentioned in the output +3. If contributing code, run `./mfc.sh format` to check for issues + +### Build fails with OpenACC/GPU errors + +**Cause:** GPU compiler not properly configured. + +**Fix:** +1. For NVIDIA GPUs, ensure NVHPC is installed and loaded: `module load nvhpc` +2. Set `CUDA_CC` environment variable if needed: `export MFC_CUDA_CC=80` +3. Try building without GPU first: `./mfc.sh build --no-gpu` + +## Runtime Errors + +### "Case parameter constraint violations" + +**Cause:** Invalid combination of simulation parameters. + +**Fix:** +1. Run `./mfc.sh validate case.py` for detailed diagnostics +2. Check the specific constraint mentioned in the error +3. Refer to `docs/documentation/case.md` for parameter documentation +4. Look at similar examples in the `examples/` directory + +Common issues: +- `m`, `n`, `p` grid dimensions not matching the dimensionality +- `weno_order` not compatible with grid size +- Boundary conditions not matching the domain setup + +### "Golden file mismatch" (during tests) + +**Cause:** Numerical results differ from the expected reference values. + +**Fix:** +1. If you intentionally changed the physics/numerics, regenerate golden files: + ```bash + ./mfc.sh test --generate --from --to + ``` +2. If unexpected, check for: + - Compiler differences (different compilers may give slightly different results) + - Precision settings (single vs. double) + - Platform-specific numerical differences + +### "NaN detected in the case" + +**Cause:** Numerical instability in the simulation. + +**Fix:** +1. Reduce the time step (`dt`) +2. Check initial conditions for discontinuities +3. Verify boundary conditions are appropriate +4. Consider using a more diffusive scheme initially + +### Test timeout (exceeded 1 hour) + +**Cause:** Test is hanging or taking too long. + +**Fix:** +1. Check if the case is too large for the test environment +2. Verify MPI is working: `mpirun -n 2 hostname` +3. Check for deadlocks in parallel code +4. Review the case configuration for infinite loops + +## Common Issues + +### `./mfc.sh clean` doesn't work + +**Cause:** The command was not properly registered. + +**Fix:** This has been fixed in recent versions. If you have an older version: +```bash +rm -rf build/ +``` + +### Tests pass locally but fail in CI + +**Cause:** Environment differences between local and CI. + +**Fix:** +1. Check compiler versions match +2. Verify precision settings (single/double/mixed) +3. Look for platform-specific code paths +4. Run with the same flags as CI: check `.github/workflows/test.yml` + +### GPU not detected + +**Cause:** GPU drivers or compiler not properly configured. + +**Fix:** +1. Verify GPU is visible: `nvidia-smi` (NVIDIA) or `rocm-smi` (AMD) +2. Check compiler supports GPU: `nvfortran --version` or `ftn --version` +3. Ensure correct modules are loaded on HPC systems +4. Set `OMP_TARGET_OFFLOAD=MANDATORY` to force GPU usage + +### "Module not found" on HPC + +**Cause:** Required modules not loaded. + +**Fix:** +1. Check available modules: `module avail` +2. Load required modules (example for typical setup): + ```bash + module load gcc openmpi cmake python + ``` +3. Use `./mfc.sh load` if available for your system + +## Getting Help + +If you can't resolve an issue: + +1. Check existing [GitHub Issues](https://github.com/MFlowCode/MFC/issues) +2. Search the [documentation](https://mflowcode.github.io/) +3. Open a new issue with: + - Your OS and compiler versions + - The exact command you ran + - The complete error output + - Your case file (if applicable) diff --git a/toolchain/main.py b/toolchain/main.py index 4afc10cf3c..6bd8da781c 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -2,12 +2,12 @@ import signal, getpass, platform, itertools -from mfc import args, lock, build, bench, state, count +from mfc import args, lock, build, bench, state, count, clean, validate from mfc.state import ARG from mfc.run import run from mfc.test import test from mfc.packer import packer -from mfc.common import MFC_LOGO, MFCException, quit, format_list_to_string, does_command_exist +from mfc.common import MFC_LOGO, MFCException, quit, format_list_to_string, does_command_exist, setup_debug_logging from mfc.printer import cons def __print_greeting(): @@ -49,8 +49,9 @@ def __checks(): def __run(): {"test": test.test, "run": run.run, "build": build.build, - "bench": bench.bench, "count": count.count, - "packer": packer.packer, "count_diff": count.count_diff, "bench_diff": bench.diff + "bench": bench.bench, "count": count.count, "clean": clean.clean, + "packer": packer.packer, "count_diff": count.count_diff, "bench_diff": bench.diff, + "validate": validate.validate }[ARG("command")]() @@ -59,6 +60,9 @@ def __run(): lock.init() state.gARG = args.parse(state.gCFG) + # Setup debug logging if requested + setup_debug_logging(ARG("debug_log")) + lock.switch(state.MFCConfig.from_dict(state.gARG)) __print_greeting() diff --git a/toolchain/mfc/args.py b/toolchain/mfc/args.py index 50682d9f4e..09b74489a5 100644 --- a/toolchain/mfc/args.py +++ b/toolchain/mfc/args.py @@ -29,6 +29,7 @@ def parse(config: MFCConfig): count = parsers.add_parser(name="count", help="Count LOC in MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) count_diff = parsers.add_parser(name="count_diff", help="Count LOC in MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) packer = parsers.add_parser(name="packer", help="Packer utility (pack/unpack/compare).", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + validate = parsers.add_parser(name="validate", help="Validate a case file without running.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) # These parser arguments all call BASH scripts, and they only exist so that they show up in the help message parsers.add_parser(name="load", help="Loads the MFC environment with source.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -73,6 +74,9 @@ def add_common_arguments(p: argparse.ArgumentParser, mask = None): if "v" not in mask: p.add_argument("-v", "--verbose", action="store_true", help="Enables verbose compiler & linker output.") + if "d" not in mask: + p.add_argument("-d", "--debug-log", action="store_true", dest="debug_log", help="Enable debug logging for troubleshooting.") + if "g" not in mask: p.add_argument("-g", "--gpus", nargs="+", type=int, default=None, help="(Optional GPU override) List of GPU #s to use (environment default if unspecified).") @@ -145,6 +149,10 @@ def add_common_arguments(p: argparse.ArgumentParser, mask = None): # COUNT add_common_arguments(count_diff, "g") + # VALIDATE + add_common_arguments(validate, "tjmgv") # Only add debug-log flag + validate.add_argument("input", metavar="INPUT", type=str, help="Path to case file to validate.") + try: extra_index = sys.argv.index('--') except ValueError: @@ -155,7 +163,8 @@ def add_common_arguments(p: argparse.ArgumentParser, mask = None): # Add default arguments of other subparsers for name, parser in [("run", run), ("test", test), ("build", build), - ("clean", clean), ("count", count), ("count_diff", count_diff)]: + ("clean", clean), ("count", count), ("count_diff", count_diff), + ("validate", validate)]: if args["command"] == name: continue diff --git a/toolchain/mfc/build.py b/toolchain/mfc/build.py index 4fd18a2f10..ebb6c54f96 100644 --- a/toolchain/mfc/build.py +++ b/toolchain/mfc/build.py @@ -1,13 +1,42 @@ -import os, typing, hashlib, dataclasses +import os, typing, hashlib, dataclasses, subprocess + +from rich.panel import Panel from .case import Case from .printer import cons from .common import MFCException, system, delete_directory, create_directory, \ - format_list_to_string + format_list_to_string, debug from .state import ARG, CFG from .run import input from .state import gpuConfigOptions + +def _show_build_error(result: subprocess.CompletedProcess, stage: str, target_name: str): + """Display build error details from captured subprocess output.""" + cons.print() + cons.print(f"[bold red]{stage} Failed - Error Details:[/bold red]") + + # Show stdout if available (often contains the actual error for CMake) + if result.stdout: + stdout_text = result.stdout if isinstance(result.stdout, str) else result.stdout.decode('utf-8', errors='replace') + stdout_lines = stdout_text.strip().split('\n') + # Show last 40 lines to capture the relevant error + if len(stdout_lines) > 40: + stdout_lines = ['... (truncated) ...'] + stdout_lines[-40:] + if stdout_lines and stdout_lines != ['']: + cons.raw.print(Panel('\n'.join(stdout_lines), title="Output", border_style="yellow")) + + # Show stderr if available + if result.stderr: + stderr_text = result.stderr if isinstance(result.stderr, str) else result.stderr.decode('utf-8', errors='replace') + stderr_lines = stderr_text.strip().split('\n') + if len(stderr_lines) > 40: + stderr_lines = ['... (truncated) ...'] + stderr_lines[-40:] + if stderr_lines and stderr_lines != ['']: + cons.raw.print(Panel('\n'.join(stderr_lines), title="Errors", border_style="red")) + + cons.print() + @dataclasses.dataclass class MFCTarget: @dataclasses.dataclass @@ -161,8 +190,20 @@ def configure(self, case: Case): case.generate_fpp(self) - if system(command).returncode != 0: + debug(f"Configuring {self.name} in {build_dirpath}") + debug(f"CMake flags: {' '.join(flags)}") + + # Show progress indicator during configuration + cons.print(f" [bold blue]Configuring[/bold blue] [magenta]{self.name}[/magenta]...") + + # Capture output to show detailed errors on failure + result = system(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, print_cmd=False) + if result.returncode != 0: + cons.print(f" [bold red]✗[/bold red] Configuration failed for [magenta]{self.name}[/magenta]") + _show_build_error(result, "Configuration", self.name) raise MFCException(f"Failed to configure the [bold magenta]{self.name}[/bold magenta] target.") + else: + cons.print(f" [bold green]✓[/bold green] Configured [magenta]{self.name}[/magenta]") cons.print(no_indent=True) @@ -176,16 +217,37 @@ def build(self, case: input.MFCInputFile): if ARG('verbose'): command.append("--verbose") - if system(command).returncode != 0: + debug(f"Building {self.name} with {ARG('jobs')} parallel jobs") + debug(f"Build command: {' '.join(str(c) for c in command)}") + + # Show progress indicator during build (can take a long time) + cons.print(f" [bold blue]Building[/bold blue] [magenta]{self.name}[/magenta]...") + + # Capture output to show detailed errors on failure + result = system(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, print_cmd=False) + if result.returncode != 0: + cons.print(f" [bold red]✗[/bold red] Build failed for [magenta]{self.name}[/magenta]") + _show_build_error(result, "Build", self.name) raise MFCException(f"Failed to build the [bold magenta]{self.name}[/bold magenta] target.") + else: + cons.print(f" [bold green]✓[/bold green] Built [magenta]{self.name}[/magenta]") cons.print(no_indent=True) def install(self, case: input.MFCInputFile): command = ["cmake", "--install", self.get_staging_dirpath(case)] - if system(command).returncode != 0: + # Show progress indicator during install + cons.print(f" [bold blue]Installing[/bold blue] [magenta]{self.name}[/magenta]...") + + # Capture output to show detailed errors on failure + result = system(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, print_cmd=False) + if result.returncode != 0: + cons.print(f" [bold red]✗[/bold red] Install failed for [magenta]{self.name}[/magenta]") + _show_build_error(result, "Install", self.name) raise MFCException(f"Failed to install the [bold magenta]{self.name}[/bold magenta] target.") + else: + cons.print(f" [bold green]✓[/bold green] Installed [magenta]{self.name}[/magenta]") cons.print(no_indent=True) diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index d52cce412a..1574633f2e 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -1673,9 +1673,34 @@ def validate(self, stage: str = 'simulation'): return if self.errors: - error_msg = "Case parameter constraint violations:\n" + "\n".join(f" • {err}" for err in self.errors) + error_msg = self._format_errors() raise CaseConstraintError(error_msg) + def _format_errors(self) -> str: + """Format errors with enhanced context and suggestions.""" + lines = ["[bold red]Case parameter constraint violations:[/bold red]\n"] + + for i, err in enumerate(self.errors, 1): + lines.append(f" [bold]{i}.[/bold] {err}") + + # Add helpful hints for common errors + err_lower = err.lower() + if "must be positive" in err_lower or "must be set" in err_lower: + lines.append(f" [dim]Check that this required parameter is defined in your case file[/dim]") + elif "weno_order" in err_lower: + lines.append(f" [dim]Valid values: 1, 3, 5, or 7[/dim]") + elif "riemann_solver" in err_lower: + lines.append(f" [dim]Valid values: 1 (HLL), 2 (HLLC), 3 (Exact), etc.[/dim]") + elif "model_eqns" in err_lower: + lines.append(f" [dim]Valid values: 1, 2 (5-eq), 3 (6-eq), or 4[/dim]") + elif "boundary" in err_lower or "bc_" in err_lower: + lines.append(f" [dim]Common BC values: -1 (periodic), -2 (reflective), -3 (extrapolation)[/dim]") + + lines.append("") + lines.append("[dim]Tip: Run './mfc.sh validate case.py' for detailed validation[/dim]") + + return "\n".join(lines) + def validate_case_constraints(params: Dict[str, Any], stage: str = 'simulation'): """Convenience function to validate case parameters diff --git a/toolchain/mfc/clean.py b/toolchain/mfc/clean.py new file mode 100644 index 0000000000..3d9c2ff096 --- /dev/null +++ b/toolchain/mfc/clean.py @@ -0,0 +1,22 @@ +""" +MFC Clean Command - Remove build artifacts. +""" + +import os +import shutil + +from .printer import cons +from .common import MFC_BUILD_DIR + + +def clean(): + """Remove the build directory and all build artifacts.""" + if os.path.isdir(MFC_BUILD_DIR): + cons.print(f"Removing [bold magenta]{MFC_BUILD_DIR}[/bold magenta]...") + try: + shutil.rmtree(MFC_BUILD_DIR) + cons.print("[bold green]Build directory cleaned successfully.[/bold green]") + except OSError as e: + cons.print(f"[bold red]Error cleaning build directory:[/bold red] {e}") + else: + cons.print("[yellow]Build directory does not exist, nothing to clean.[/yellow]") diff --git a/toolchain/mfc/common.py b/toolchain/mfc/common.py index 6061f14b5b..eb11af3920 100644 --- a/toolchain/mfc/common.py +++ b/toolchain/mfc/common.py @@ -1,10 +1,35 @@ -import os, yaml, typing, shutil, subprocess +import os, yaml, typing, shutil, subprocess, logging from os.path import join, abspath, normpath, dirname, realpath from .printer import cons +# Debug logging infrastructure +_debug_logger = None + +def setup_debug_logging(enabled: bool = False): + """Setup debug logging for troubleshooting.""" + global _debug_logger + if enabled: + logging.basicConfig( + level=logging.DEBUG, + format='[DEBUG %(asctime)s] %(message)s', + datefmt='%H:%M:%S' + ) + _debug_logger = logging.getLogger('mfc') + _debug_logger.setLevel(logging.DEBUG) + cons.print("[dim]Debug logging enabled[/dim]") + else: + _debug_logger = None + +def debug(msg: str): + """Log a debug message if debug logging is enabled.""" + if _debug_logger: + _debug_logger.debug(msg) + cons.print(f"[dim][DEBUG][/dim] {msg}") + + MFC_ROOT_DIR = abspath(normpath(f"{dirname(realpath(__file__))}/../..")) MFC_TEST_DIR = abspath(join(MFC_ROOT_DIR, "tests")) MFC_BUILD_DIR = abspath(join(MFC_ROOT_DIR, "build")) diff --git a/toolchain/mfc/test/test.py b/toolchain/mfc/test/test.py index 78d40d9eba..d1c0708039 100644 --- a/toolchain/mfc/test/test.py +++ b/toolchain/mfc/test/test.py @@ -164,7 +164,7 @@ def test(): # Run cases with multiple threads (if available) cons.print() - cons.print(" Progress [bold magenta]UUID[/bold magenta] (s) Summary") + cons.print(" Progress Test Name Time(s) UUID") cons.print() # Select the correct number of threads to use to launch test cases @@ -259,7 +259,9 @@ def _handle_case(case: TestCase, devices: typing.Set[int]): case.create_directory() if ARG("dry_run"): - cons.print(f" [bold magenta]{case.get_uuid()}[/bold magenta] SKIP {case.trace}") + # Truncate long traces for readability + trace_display = case.trace if len(case.trace) <= 50 else case.trace[:47] + "..." + cons.print(f" (dry-run) {trace_display:50s} SKIP [magenta]{case.get_uuid()}[/magenta]") timeout_timer.cancel() return @@ -335,7 +337,9 @@ def _handle_case(case: TestCase, devices: typing.Set[int]): current_test_number += 1 progress_str = f"({current_test_number:3d}/{total_test_count:3d})" - cons.print(f" {progress_str} [bold magenta]{case.get_uuid()}[/bold magenta] {duration:6.2f} {case.trace}") + # Truncate long traces for readability, showing test name prominently + trace_display = case.trace if len(case.trace) <= 50 else case.trace[:47] + "..." + cons.print(f" {progress_str} {trace_display:50s} {duration:6.2f} [magenta]{case.get_uuid()}[/magenta]") except TestTimeoutError as exc: log_path = os.path.join(case.get_dirpath(), 'out_pre_sim.txt') @@ -382,7 +386,33 @@ def handle_case(case: TestCase, devices: typing.Set[int]): if nAttempts < max_attempts: continue nFAIL += 1 - cons.print(f"[bold red]Failed test {case} after {nAttempts} attempt(s).[/bold red]") + + # Enhanced real-time failure feedback + trace_display = case.trace if len(case.trace) <= 50 else case.trace[:47] + "..." + cons.print() + cons.print(f" [bold red]✗ FAILED:[/bold red] {trace_display}") + cons.print(f" UUID: [magenta]{case.get_uuid()}[/magenta]") + cons.print(f" Attempts: {nAttempts}") + + # Show truncated error message + exc_str = str(exc) + if len(exc_str) > 300: + exc_str = exc_str[:297] + "..." + cons.print(f" Error: {exc_str}") + + # Provide helpful hints based on error type + exc_lower = str(exc).lower() + if "tolerance" in exc_lower or "golden" in exc_lower or "mismatch" in exc_lower: + cons.print(f" [dim]Hint: Consider --generate to update golden files or check tolerances[/dim]") + elif "timeout" in exc_lower: + cons.print(f" [dim]Hint: Test may be hanging - check case configuration[/dim]") + elif "nan" in exc_lower: + cons.print(f" [dim]Hint: NaN detected - check numerical stability of the case[/dim]") + elif "failed to execute" in exc_lower: + cons.print(f" [dim]Hint: Check build logs and case parameters[/dim]") + cons.print() + + # Still collect for final summary errors.append(f"[bold red]Failed test {case} after {nAttempts} attempt(s).[/bold red]") errors.append(f"{exc}") diff --git a/toolchain/mfc/validate.py b/toolchain/mfc/validate.py new file mode 100644 index 0000000000..71216e7ef5 --- /dev/null +++ b/toolchain/mfc/validate.py @@ -0,0 +1,58 @@ +""" +MFC Validate Command - Validate a case file without building or running. +""" + +import os + +from .state import ARG +from .printer import cons +from .run import input as run_input +from .case_validator import CaseValidator, CaseConstraintError +from .common import MFCException + + +def validate(): + """Validate a case file without building or running.""" + input_file = ARG("input") + + if not os.path.isfile(input_file): + cons.print(f"[bold red]Error:[/bold red] File not found: {input_file}") + exit(1) + + cons.print(f"Validating [bold magenta]{input_file}[/bold magenta]...\n") + + try: + # Step 1: Load and parse case file (checks syntax) + case = run_input.load(input_file, do_print=False) + cons.print("[bold green]✓[/bold green] Syntax valid - case file parsed successfully") + cons.print(f" [dim]Loaded {len(case.params)} parameters[/dim]") + + # Step 2: Run constraint validation for each stage + stages = ['pre_process', 'simulation', 'post_process'] + all_passed = True + + for stage in stages: + try: + validator = CaseValidator(case.params) + validator.validate(stage) + cons.print(f"[bold green]✓[/bold green] {stage} constraints passed") + except CaseConstraintError as e: + all_passed = False + cons.print(f"[bold yellow]![/bold yellow] {stage} constraints: issues found") + # Show the constraint violations indented + for line in str(e).split('\n'): + if line.strip(): + cons.print(f" [dim]{line}[/dim]") + + # Step 3: Show summary + cons.print() + if all_passed: + cons.print("[bold green]Case validation complete - all checks passed![/bold green]") + else: + cons.print("[bold yellow]Case validation complete with warnings.[/bold yellow]") + cons.print("[dim]Note: Some constraint violations may be OK if you're not using that stage.[/dim]") + + except MFCException as e: + cons.print(f"\n[bold red]✗ Validation failed:[/bold red]") + cons.print(f"{e}") + exit(1) From 4827baf5355f938cb1459148312f386f22968fc0 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Tue, 27 Jan 2026 20:51:16 -0500 Subject: [PATCH 002/152] Add test summary report and case template generator Test Summary: - Rich panel with formatted pass/fail/skip counts - Total test duration display - Failed test details with UUIDs and error types - Helpful "Next Steps" suggestions for failures Case Template Generator (./mfc.sh init): - Create new cases from built-in templates (1D/2D/3D_minimal) - Copy from any example with --template example: - List available templates with --list - Well-documented case files with clear parameter sections Co-Authored-By: Claude Opus 4.5 --- toolchain/main.py | 4 +- toolchain/mfc/args.py | 11 +- toolchain/mfc/init.py | 500 +++++++++++++++++++++++++++++++++++++ toolchain/mfc/test/test.py | 114 ++++++++- 4 files changed, 612 insertions(+), 17 deletions(-) create mode 100644 toolchain/mfc/init.py diff --git a/toolchain/main.py b/toolchain/main.py index 6bd8da781c..829534ee56 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -2,7 +2,7 @@ import signal, getpass, platform, itertools -from mfc import args, lock, build, bench, state, count, clean, validate +from mfc import args, lock, build, bench, state, count, clean, validate, init from mfc.state import ARG from mfc.run import run from mfc.test import test @@ -51,7 +51,7 @@ def __run(): {"test": test.test, "run": run.run, "build": build.build, "bench": bench.bench, "count": count.count, "clean": clean.clean, "packer": packer.packer, "count_diff": count.count_diff, "bench_diff": bench.diff, - "validate": validate.validate + "validate": validate.validate, "init": init.init }[ARG("command")]() diff --git a/toolchain/mfc/args.py b/toolchain/mfc/args.py index 09b74489a5..6e12c3864f 100644 --- a/toolchain/mfc/args.py +++ b/toolchain/mfc/args.py @@ -30,6 +30,7 @@ def parse(config: MFCConfig): count_diff = parsers.add_parser(name="count_diff", help="Count LOC in MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) packer = parsers.add_parser(name="packer", help="Packer utility (pack/unpack/compare).", formatter_class=argparse.ArgumentDefaultsHelpFormatter) validate = parsers.add_parser(name="validate", help="Validate a case file without running.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + init = parsers.add_parser(name="init", help="Create a new case from a template.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) # These parser arguments all call BASH scripts, and they only exist so that they show up in the help message parsers.add_parser(name="load", help="Loads the MFC environment with source.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -153,6 +154,11 @@ def add_common_arguments(p: argparse.ArgumentParser, mask = None): add_common_arguments(validate, "tjmgv") # Only add debug-log flag validate.add_argument("input", metavar="INPUT", type=str, help="Path to case file to validate.") + # INIT + init.add_argument("name", metavar="NAME", type=str, nargs="?", default=None, help="Name/path for the new case directory.") + init.add_argument("-t", "--template", type=str, default="1D_minimal", help="Template to use (e.g., 1D_minimal, 2D_minimal, 3D_minimal, or example:).") + init.add_argument("-l", "--list", action="store_true", help="List available templates.") + try: extra_index = sys.argv.index('--') except ValueError: @@ -179,8 +185,9 @@ def add_common_arguments(p: argparse.ArgumentParser, mask = None): parser.print_help() exit(-1) - # "Slugify" the name of the job - args["name"] = re.sub(r'[\W_]+', '-', args["name"]) + # "Slugify" the name of the job (only for batch jobs, not for init command) + if args.get("name") is not None and isinstance(args["name"], str) and args["command"] != "init": + args["name"] = re.sub(r'[\W_]+', '-', args["name"]) # We need to check for some invalid combinations of arguments because of # the limitations of argparse. diff --git a/toolchain/mfc/init.py b/toolchain/mfc/init.py new file mode 100644 index 0000000000..653c2f5602 --- /dev/null +++ b/toolchain/mfc/init.py @@ -0,0 +1,500 @@ +"""MFC Case Template Generator - Create new case files from templates.""" + +import os +import shutil + +from .printer import cons +from .common import MFC_EXAMPLE_DIRPATH, MFCException +from .state import ARG + + +# Built-in minimal templates +BUILTIN_TEMPLATES = { + '1D_minimal': '''\ +#!/usr/bin/env python3 +""" +1D Minimal Case Template +------------------------ +A minimal 1D shock tube case to get started with MFC. + +Usage: + ./mfc.sh run case.py +""" +import math +import json + +# ============================================================================= +# SIMULATION PARAMETERS - Modify these for your case +# ============================================================================= + +# Grid resolution +Nx = 399 # Number of cells in x-direction + +# Domain size +x_start = 0.0 # Domain start +x_end = 1.0 # Domain end + +# Time stepping +t_end = 0.1 # End time +Nt = 1000 # Number of time steps + +# Initial conditions for left state (patch 1) +rho_L = 1.0 # Density +vel_L = 0.0 # Velocity +pres_L = 1.0 # Pressure + +# Initial conditions for right state (patch 2) +rho_R = 0.125 # Density +vel_R = 0.0 # Velocity +pres_R = 0.1 # Pressure + +# Fluid properties +gamma = 1.4 # Ratio of specific heats + +# ============================================================================= +# DERIVED QUANTITIES - Usually don't need to modify +# ============================================================================= +dx = (x_end - x_start) / (Nx + 1) +dt = t_end / Nt + +# ============================================================================= +# CASE DICTIONARY - MFC configuration +# ============================================================================= +print(json.dumps({ + # Logistics + "run_time_info": "T", + + # Computational Domain + "x_domain%beg": x_start, + "x_domain%end": x_end, + "m": Nx, + "n": 0, + "p": 0, + "dt": dt, + "t_step_start": 0, + "t_step_stop": Nt, + "t_step_save": max(1, Nt // 10), + + # Simulation Algorithm + "num_patches": 2, + "model_eqns": 2, # 5-equation model + "num_fluids": 1, + "time_stepper": 3, # TVD RK3 + "weno_order": 5, # WENO5 + "weno_eps": 1.0e-16, + "mapped_weno": "T", + "riemann_solver": 2, # HLLC + "wave_speeds": 1, + "avg_state": 2, + + # Boundary Conditions (-3 = extrapolation) + "bc_x%beg": -3, + "bc_x%end": -3, + + # Output + "format": 1, + "precision": 2, + "prim_vars_wrt": "T", + "parallel_io": "T", + + # Patch 1: Left state + "patch_icpp(1)%geometry": 1, + "patch_icpp(1)%x_centroid": (x_start + x_end) / 4, + "patch_icpp(1)%length_x": (x_end - x_start) / 2, + "patch_icpp(1)%vel(1)": vel_L, + "patch_icpp(1)%pres": pres_L, + "patch_icpp(1)%alpha_rho(1)": rho_L, + "patch_icpp(1)%alpha(1)": 1.0, + + # Patch 2: Right state + "patch_icpp(2)%geometry": 1, + "patch_icpp(2)%x_centroid": 3 * (x_start + x_end) / 4, + "patch_icpp(2)%length_x": (x_end - x_start) / 2, + "patch_icpp(2)%vel(1)": vel_R, + "patch_icpp(2)%pres": pres_R, + "patch_icpp(2)%alpha_rho(1)": rho_R, + "patch_icpp(2)%alpha(1)": 1.0, + + # Fluid Properties + "fluid_pp(1)%gamma": 1.0 / (gamma - 1.0), + "fluid_pp(1)%pi_inf": 0.0, +})) +''', + + '2D_minimal': '''\ +#!/usr/bin/env python3 +""" +2D Minimal Case Template +------------------------ +A minimal 2D case with a circular perturbation. + +Usage: + ./mfc.sh run case.py +""" +import math +import json + +# ============================================================================= +# SIMULATION PARAMETERS - Modify these for your case +# ============================================================================= + +# Grid resolution +Nx = 99 # Cells in x-direction +Ny = 99 # Cells in y-direction + +# Domain size +x_start, x_end = 0.0, 1.0 +y_start, y_end = 0.0, 1.0 + +# Time stepping +dt = 1.0e-6 +Nt = 1000 + +# Background state +rho_bg = 1.0 +vel_x_bg = 0.0 +vel_y_bg = 0.0 +pres_bg = 1.0e5 + +# Perturbation (circular region) +x_center = 0.5 +y_center = 0.5 +radius = 0.1 +rho_pert = 2.0 +pres_pert = 2.0e5 + +# Fluid properties +gamma = 1.4 + +# ============================================================================= +# CASE DICTIONARY - MFC configuration +# ============================================================================= +print(json.dumps({ + # Logistics + "run_time_info": "T", + + # Computational Domain + "x_domain%beg": x_start, + "x_domain%end": x_end, + "y_domain%beg": y_start, + "y_domain%end": y_end, + "m": Nx, + "n": Ny, + "p": 0, + "dt": dt, + "t_step_start": 0, + "t_step_stop": Nt, + "t_step_save": max(1, Nt // 10), + + # Simulation Algorithm + "num_patches": 2, + "model_eqns": 2, + "num_fluids": 1, + "time_stepper": 3, + "weno_order": 5, + "weno_eps": 1.0e-16, + "mapped_weno": "T", + "riemann_solver": 2, + "wave_speeds": 1, + "avg_state": 2, + + # Boundary Conditions + "bc_x%beg": -3, + "bc_x%end": -3, + "bc_y%beg": -3, + "bc_y%end": -3, + + # Output + "format": 1, + "precision": 2, + "prim_vars_wrt": "T", + "parallel_io": "T", + + # Patch 1: Background + "patch_icpp(1)%geometry": 3, # Rectangle + "patch_icpp(1)%x_centroid": (x_start + x_end) / 2, + "patch_icpp(1)%y_centroid": (y_start + y_end) / 2, + "patch_icpp(1)%length_x": x_end - x_start, + "patch_icpp(1)%length_y": y_end - y_start, + "patch_icpp(1)%vel(1)": vel_x_bg, + "patch_icpp(1)%vel(2)": vel_y_bg, + "patch_icpp(1)%pres": pres_bg, + "patch_icpp(1)%alpha_rho(1)": rho_bg, + "patch_icpp(1)%alpha(1)": 1.0, + + # Patch 2: Circular perturbation + "patch_icpp(2)%geometry": 2, # Circle + "patch_icpp(2)%x_centroid": x_center, + "patch_icpp(2)%y_centroid": y_center, + "patch_icpp(2)%radius": radius, + "patch_icpp(2)%alter_patch(1)": "T", + "patch_icpp(2)%vel(1)": vel_x_bg, + "patch_icpp(2)%vel(2)": vel_y_bg, + "patch_icpp(2)%pres": pres_pert, + "patch_icpp(2)%alpha_rho(1)": rho_pert, + "patch_icpp(2)%alpha(1)": 1.0, + + # Fluid Properties + "fluid_pp(1)%gamma": 1.0 / (gamma - 1.0), + "fluid_pp(1)%pi_inf": 0.0, +})) +''', + + '3D_minimal': '''\ +#!/usr/bin/env python3 +""" +3D Minimal Case Template +------------------------ +A minimal 3D case with a spherical perturbation. + +Usage: + ./mfc.sh run case.py -N 2 -n 4 # Run on 2 nodes with 4 tasks each +""" +import math +import json + +# ============================================================================= +# SIMULATION PARAMETERS - Modify these for your case +# ============================================================================= + +# Grid resolution (keep low for testing, increase for production) +Nx = 49 # Cells in x-direction +Ny = 49 # Cells in y-direction +Nz = 49 # Cells in z-direction + +# Domain size +x_start, x_end = 0.0, 1.0 +y_start, y_end = 0.0, 1.0 +z_start, z_end = 0.0, 1.0 + +# Time stepping +dt = 1.0e-6 +Nt = 100 # Keep low for testing + +# Background state +rho_bg = 1.0 +pres_bg = 1.0e5 + +# Spherical perturbation +x_center = 0.5 +y_center = 0.5 +z_center = 0.5 +radius = 0.1 +rho_pert = 2.0 +pres_pert = 2.0e5 + +# Fluid properties +gamma = 1.4 + +# ============================================================================= +# CASE DICTIONARY - MFC configuration +# ============================================================================= +print(json.dumps({ + # Logistics + "run_time_info": "T", + + # Computational Domain + "x_domain%beg": x_start, + "x_domain%end": x_end, + "y_domain%beg": y_start, + "y_domain%end": y_end, + "z_domain%beg": z_start, + "z_domain%end": z_end, + "m": Nx, + "n": Ny, + "p": Nz, + "dt": dt, + "t_step_start": 0, + "t_step_stop": Nt, + "t_step_save": max(1, Nt // 10), + + # Simulation Algorithm + "num_patches": 2, + "model_eqns": 2, + "num_fluids": 1, + "time_stepper": 3, + "weno_order": 5, + "weno_eps": 1.0e-16, + "mapped_weno": "T", + "riemann_solver": 2, + "wave_speeds": 1, + "avg_state": 2, + + # Boundary Conditions + "bc_x%beg": -3, + "bc_x%end": -3, + "bc_y%beg": -3, + "bc_y%end": -3, + "bc_z%beg": -3, + "bc_z%end": -3, + + # Output + "format": 1, + "precision": 2, + "prim_vars_wrt": "T", + "parallel_io": "T", + + # Patch 1: Background (cube) + "patch_icpp(1)%geometry": 9, + "patch_icpp(1)%x_centroid": (x_start + x_end) / 2, + "patch_icpp(1)%y_centroid": (y_start + y_end) / 2, + "patch_icpp(1)%z_centroid": (z_start + z_end) / 2, + "patch_icpp(1)%length_x": x_end - x_start, + "patch_icpp(1)%length_y": y_end - y_start, + "patch_icpp(1)%length_z": z_end - z_start, + "patch_icpp(1)%vel(1)": 0.0, + "patch_icpp(1)%vel(2)": 0.0, + "patch_icpp(1)%vel(3)": 0.0, + "patch_icpp(1)%pres": pres_bg, + "patch_icpp(1)%alpha_rho(1)": rho_bg, + "patch_icpp(1)%alpha(1)": 1.0, + + # Patch 2: Spherical perturbation + "patch_icpp(2)%geometry": 8, # Sphere + "patch_icpp(2)%x_centroid": x_center, + "patch_icpp(2)%y_centroid": y_center, + "patch_icpp(2)%z_centroid": z_center, + "patch_icpp(2)%radius": radius, + "patch_icpp(2)%alter_patch(1)": "T", + "patch_icpp(2)%vel(1)": 0.0, + "patch_icpp(2)%vel(2)": 0.0, + "patch_icpp(2)%vel(3)": 0.0, + "patch_icpp(2)%pres": pres_pert, + "patch_icpp(2)%alpha_rho(1)": rho_pert, + "patch_icpp(2)%alpha(1)": 1.0, + + # Fluid Properties + "fluid_pp(1)%gamma": 1.0 / (gamma - 1.0), + "fluid_pp(1)%pi_inf": 0.0, +})) +''', +} + + +def get_available_templates(): + """Get list of available templates (built-in + examples).""" + templates = list(BUILTIN_TEMPLATES.keys()) + + # Add examples as templates + if os.path.isdir(MFC_EXAMPLE_DIRPATH): + for name in sorted(os.listdir(MFC_EXAMPLE_DIRPATH)): + example_path = os.path.join(MFC_EXAMPLE_DIRPATH, name) + if os.path.isdir(example_path) and os.path.isfile(os.path.join(example_path, 'case.py')): + templates.append(f"example:{name}") + + return templates + + +def list_templates(): + """Print available templates.""" + cons.print("[bold]Available Templates[/bold]\n") + + cons.print(" [bold cyan]Built-in Templates:[/bold cyan]") + for name in sorted(BUILTIN_TEMPLATES.keys()): + desc = { + '1D_minimal': 'Minimal 1D shock tube case', + '2D_minimal': 'Minimal 2D case with circular perturbation', + '3D_minimal': 'Minimal 3D case with spherical perturbation', + }.get(name, '') + cons.print(f" [green]{name:20s}[/green] {desc}") + + cons.print() + cons.print(" [bold cyan]From Examples:[/bold cyan]") + + if os.path.isdir(MFC_EXAMPLE_DIRPATH): + examples = [] + for name in sorted(os.listdir(MFC_EXAMPLE_DIRPATH)): + example_path = os.path.join(MFC_EXAMPLE_DIRPATH, name) + if os.path.isdir(example_path) and os.path.isfile(os.path.join(example_path, 'case.py')): + examples.append(name) + + # Group by dimension + for dim in ['0D', '1D', '2D', '3D']: + dim_examples = [e for e in examples if e.startswith(dim)] + if dim_examples: + cons.print(f" [dim]{dim}:[/dim] {', '.join(dim_examples[:5])}", end='') + if len(dim_examples) > 5: + cons.print(f" [dim]... (+{len(dim_examples) - 5} more)[/dim]") + else: + cons.print() + + cons.print() + cons.print(" [bold]Usage:[/bold]") + cons.print(" ./mfc.sh init my_case # Use default 1D template") + cons.print(" ./mfc.sh init my_case --template 2D_minimal # Use 2D template") + cons.print(" ./mfc.sh init my_case --template example:1D_sodshocktube # Copy from example") + cons.print() + + +def create_case(name: str, template: str): + """Create a new case from a template.""" + # Determine output directory + output_dir = os.path.abspath(name) + + if os.path.exists(output_dir): + raise MFCException(f"Directory already exists: {output_dir}") + + # Check if it's a built-in template + if template in BUILTIN_TEMPLATES: + os.makedirs(output_dir, exist_ok=True) + case_path = os.path.join(output_dir, 'case.py') + + with open(case_path, 'w') as f: + f.write(BUILTIN_TEMPLATES[template]) + + os.chmod(case_path, 0o755) # Make executable + + cons.print(f"[bold green]Created[/bold green] {output_dir}/") + cons.print(f" Using template: [cyan]{template}[/cyan]") + cons.print() + cons.print(" [bold]Next steps:[/bold]") + cons.print(f" 1. Edit [cyan]{name}/case.py[/cyan] to configure your simulation") + cons.print(f" 2. Run: [cyan]./mfc.sh run {name}/case.py[/cyan]") + cons.print() + + # Check if it's an example template + elif template.startswith('example:'): + example_name = template[8:] # Remove 'example:' prefix + example_path = os.path.join(MFC_EXAMPLE_DIRPATH, example_name) + + if not os.path.isdir(example_path): + raise MFCException(f"Example not found: {example_name}") + + # Copy the example directory + shutil.copytree(example_path, output_dir) + + cons.print(f"[bold green]Created[/bold green] {output_dir}/") + cons.print(f" Copied from example: [cyan]{example_name}[/cyan]") + cons.print() + cons.print(" [bold]Next steps:[/bold]") + cons.print(f" 1. Review and modify [cyan]{name}/case.py[/cyan]") + cons.print(f" 2. Run: [cyan]./mfc.sh run {name}/case.py[/cyan]") + cons.print() + + else: + available = ', '.join(list(BUILTIN_TEMPLATES.keys())[:3]) + raise MFCException( + f"Unknown template: {template}\n" + f"Available built-in templates: {available}\n" + f"Or use 'example:' to copy from examples.\n" + f"Run './mfc.sh init --list' to see all available templates." + ) + + +def init(): + """Main entry point for the init command.""" + if ARG("list"): + list_templates() + return + + name = ARG("name") + template = ARG("template") + + if not name: + raise MFCException( + "Please specify a case name.\n" + "Usage: ./mfc.sh init [--template