diff --git a/CLAUDE.md b/CLAUDE.md index 47a188d100..627757fa56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ All commands run from the repo root via `./mfc.sh`. # Verification (pre-commit CI checks) ./mfc.sh precheck -j 8 # Run all 5 lint checks (same as CI gate) ./mfc.sh format -j 8 # Auto-format Fortran (.fpp/.f90) + Python -./mfc.sh lint # Pylint + Python unit tests +./mfc.sh lint # Ruff lint + Python unit tests ./mfc.sh spelling # Spell check # Module loading (HPC clusters only — must use `source`) diff --git a/benchmarks/5eq_rk3_weno3_hllc/case.py b/benchmarks/5eq_rk3_weno3_hllc/case.py index fa09426ffe..29bb6fca94 100644 --- a/benchmarks/5eq_rk3_weno3_hllc/case.py +++ b/benchmarks/5eq_rk3_weno3_hllc/case.py @@ -6,9 +6,9 @@ # - weno_order : 3 # - riemann_solver : 2 +import argparse import json import math -import argparse parser = argparse.ArgumentParser(prog="Benchmarking Case 1", description="This MFC case was created for the purposes of benchmarking MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/benchmarks/hypo_hll/case.py b/benchmarks/hypo_hll/case.py index f8d0928a01..6a92ee3a3d 100644 --- a/benchmarks/hypo_hll/case.py +++ b/benchmarks/hypo_hll/case.py @@ -4,9 +4,9 @@ # - hypoelasticity : T # - riemann_solver : 1 +import argparse import json import math -import argparse parser = argparse.ArgumentParser(prog="Benchmarkin Case 3", description="This MFC case was created for the purposes of benchmarking MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/benchmarks/ibm/case.py b/benchmarks/ibm/case.py index 303cf7fcaf..697973b8ed 100644 --- a/benchmarks/ibm/case.py +++ b/benchmarks/ibm/case.py @@ -3,9 +3,9 @@ # Additional Benchmarked Features # - ibm : T +import argparse import json import math -import argparse parser = argparse.ArgumentParser(prog="Benchmarking Case 4", description="This MFC case was created for the purposes of benchmarking MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/benchmarks/igr/case.py b/benchmarks/igr/case.py index 4ceed76257..52331297e2 100644 --- a/benchmarks/igr/case.py +++ b/benchmarks/igr/case.py @@ -5,9 +5,9 @@ # - viscous : T # - igr_order : 5 +import argparse import json import math -import argparse parser = argparse.ArgumentParser(prog="Benchmarking Case 5", description="This MFC case was created for the purposes of benchmarking MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/benchmarks/viscous_weno5_sgb_acoustic/case.py b/benchmarks/viscous_weno5_sgb_acoustic/case.py index 83bdc43e9c..ebc13e6161 100644 --- a/benchmarks/viscous_weno5_sgb_acoustic/case.py +++ b/benchmarks/viscous_weno5_sgb_acoustic/case.py @@ -8,9 +8,9 @@ # - bubble_model : 3 # - acoustic_source : T +import argparse import json import math -import argparse parser = argparse.ArgumentParser(prog="Benchmarking Case 2", description="This MFC case was created for the purposes of benchmarking MFC.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/docs/documentation/contributing.md b/docs/documentation/contributing.md index da97488dde..9811bcc3c3 100644 --- a/docs/documentation/contributing.md +++ b/docs/documentation/contributing.md @@ -715,14 +715,15 @@ Every push to a PR triggers CI. Understanding the pipeline helps you fix failure ### Lint Gate (runs first, blocks all other jobs) -All four checks must pass before any builds start: +All five checks must pass before any builds start: 1. **Formatting** — `./mfc.sh format` (auto-handled by pre-commit hook) 2. **Spelling** — `./mfc.sh spelling` -3. **Toolchain lint** — `./mfc.sh lint` (Python code quality) +3. **Toolchain lint** — `./mfc.sh lint` (ruff + Python unit tests) 4. **Source lint** — checks for: - Raw `!$acc` or `!$omp` directives (must use Fypp GPU macros) - Double-precision intrinsics (`dsqrt`, `dexp`, `dble`, etc.) +5. **Doc references** — validates documentation cross-references ### Build and Test Matrix diff --git a/examples/0D_bubblecollapse_adap/case.py b/examples/0D_bubblecollapse_adap/case.py index 70ffb11a81..ac79d69275 100644 --- a/examples/0D_bubblecollapse_adap/case.py +++ b/examples/0D_bubblecollapse_adap/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import math import json +import math # FLUID PROPERTIES # Water diff --git a/examples/1D_bubblescreen/case.py b/examples/1D_bubblescreen/case.py index fec53c55e1..f5d6011946 100755 --- a/examples/1D_bubblescreen/case.py +++ b/examples/1D_bubblescreen/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # FLUID PROPERTIES R_uni = 8314.0 # [J/kmol/K] @@ -53,7 +53,6 @@ u0 = math.sqrt(p0 / rho0) # [m/s] t0 = x0 / u0 # [s] -# cfl = 0.1 Nx = 100 Ldomain = 20.0e-03 diff --git a/examples/1D_convergence/case.py b/examples/1D_convergence/case.py index fe1ed3c976..7c5858bed4 100755 --- a/examples/1D_convergence/case.py +++ b/examples/1D_convergence/case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import math -import json import argparse +import json +import math # Parsing command line arguments parser = argparse.ArgumentParser(description="Generate JSON case configuration for two-fluid convergence simulation.") @@ -67,10 +67,10 @@ "patch_icpp(1)%length_x": 1.0, "patch_icpp(1)%vel(1)": 1.0, "patch_icpp(1)%pres": 1.0, - "patch_icpp(1)%alpha_rho(1)": f"0.5 - 0.5*sin(2*pi*x)", - "patch_icpp(1)%alpha(1)": f"0.5 - 0.5*sin(2*pi*x)", - "patch_icpp(1)%alpha_rho(2)": f"0.5 + 0.5*sin(2*pi*x)", - "patch_icpp(1)%alpha(2)": f"0.5 + 0.5*sin(2*pi*x)", + "patch_icpp(1)%alpha_rho(1)": "0.5 - 0.5*sin(2*pi*x)", + "patch_icpp(1)%alpha(1)": "0.5 - 0.5*sin(2*pi*x)", + "patch_icpp(1)%alpha_rho(2)": "0.5 + 0.5*sin(2*pi*x)", + "patch_icpp(1)%alpha(2)": "0.5 + 0.5*sin(2*pi*x)", # Fluids Physical Parameters "fluid_pp(1)%gamma": 1.0e00 / (1.4 - 1.0e00), "fluid_pp(1)%pi_inf": 0.0, diff --git a/examples/1D_exp_bubscreen/case.py b/examples/1D_exp_bubscreen/case.py index d8590436c6..212e18611d 100755 --- a/examples/1D_exp_bubscreen/case.py +++ b/examples/1D_exp_bubscreen/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # FLUID PROPERTIES R_uni = 8314.0 # [J/kmol/K] diff --git a/examples/1D_exp_tube_phasechange/case.py b/examples/1D_exp_tube_phasechange/case.py index 18bd4c72d9..03808cd78d 100644 --- a/examples/1D_exp_tube_phasechange/case.py +++ b/examples/1D_exp_tube_phasechange/case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import math -import json import argparse +import json +import math parser = argparse.ArgumentParser(prog="phasechange", description="phase change considering both 5 and 6 equation models.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--mfc", type=json.loads, default="{}", metavar="DICT", help="MFC's toolchain's internal state.") diff --git a/examples/1D_hypo_2materials/case.py b/examples/1D_hypo_2materials/case.py index 137fb8824c..cd4316fc41 100755 --- a/examples/1D_hypo_2materials/case.py +++ b/examples/1D_hypo_2materials/case.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import math import json +import math # Numerical setup Nx = 399 diff --git a/examples/1D_impact/case.py b/examples/1D_impact/case.py index f83793ea14..7f434741f8 100755 --- a/examples/1D_impact/case.py +++ b/examples/1D_impact/case.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import math import json +import math # Numerical setup Nx = 399 diff --git a/examples/1D_inert_shocktube/case.py b/examples/1D_inert_shocktube/case.py index c9a1a8f9d6..26f45f1658 100644 --- a/examples/1D_inert_shocktube/case.py +++ b/examples/1D_inert_shocktube/case.py @@ -2,8 +2,8 @@ # References: # + https://doi.org/10.1016/j.compfluid.2013.10.014: 4.3. Multi-component inert shock tube -import json import argparse +import json import cantera as ct @@ -105,8 +105,8 @@ if args.chemistry: for i in range(len(sol_L.Y)): - case[f"patch_icpp(1)%Y({i+1})"] = sol_L.Y[i] - case[f"patch_icpp(2)%Y({i+1})"] = sol_R.Y[i] + case[f"patch_icpp(1)%Y({i + 1})"] = sol_L.Y[i] + case[f"patch_icpp(2)%Y({i + 1})"] = sol_R.Y[i] if __name__ == "__main__": print(json.dumps(case)) diff --git a/examples/1D_laxshocktube/case.py b/examples/1D_laxshocktube/case.py index 6426e2fe05..15445f792c 100644 --- a/examples/1D_laxshocktube/case.py +++ b/examples/1D_laxshocktube/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 200 diff --git a/examples/1D_multispecies_diffusion/case.py b/examples/1D_multispecies_diffusion/case.py index ecacbd965c..b4f52ae438 100644 --- a/examples/1D_multispecies_diffusion/case.py +++ b/examples/1D_multispecies_diffusion/case.py @@ -2,9 +2,10 @@ # References: # + https://doi.org/10.1016/j.compfluid.2013.10.014: 4.4. Multicomponent diffusion test case -import json import argparse +import json import math + import cantera as ct ctfile = "gri30.yaml" @@ -73,6 +74,6 @@ for i in range(len(sol_L.Y)): case[f"chem_wrt_Y({i + 1})"] = "T" - case[f"patch_icpp(1)%Y({i+1})"] = 0.0 + case[f"patch_icpp(1)%Y({i + 1})"] = 0.0 if __name__ == "__main__": print(json.dumps(case)) diff --git a/examples/1D_poly_bubscreen/case.py b/examples/1D_poly_bubscreen/case.py index b5ebf8e43d..9006f72f31 100644 --- a/examples/1D_poly_bubscreen/case.py +++ b/examples/1D_poly_bubscreen/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # FLUID PROPERTIES R_uni = 8314.0 # [J/kmol/K] @@ -46,7 +46,6 @@ u0 = math.sqrt(p0 / rho0) # [m/s] t0 = x0 / u0 # [s] -# cact = 1475.0 cfl = 0.4 Nx = 20 diff --git a/examples/1D_qbmm/case.py b/examples/1D_qbmm/case.py index 2c75a6454d..426e1460b5 100644 --- a/examples/1D_qbmm/case.py +++ b/examples/1D_qbmm/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python2 -import math import json +import math # FLUID PROPERTIES R_uni = 8314.0 # [J/kmol/K] @@ -48,7 +48,6 @@ u0 = math.sqrt(p0 / rho0) # [m/s] t0 = x0 / u0 # [s] -# cact = 1475.0 cfl = 0.1 Nx = 400 diff --git a/examples/1D_reactive_shocktube/case.py b/examples/1D_reactive_shocktube/case.py index c9ce96b804..d14c20c2ea 100644 --- a/examples/1D_reactive_shocktube/case.py +++ b/examples/1D_reactive_shocktube/case.py @@ -3,8 +3,9 @@ # + https://doi.org/10.1016/j.ijhydene.2023.03.190: Verification of numerical method # + https://doi.org/10.1016/j.compfluid.2013.10.014: 4.7. Multi-species reactive shock tube -import json import argparse +import json + import cantera as ct parser = argparse.ArgumentParser(prog="1D_reactive_shocktube", formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -111,8 +112,8 @@ if args.chemistry: for i in range(len(sol_L.Y)): case[f"chem_wrt_Y({i + 1})"] = "T" - case[f"patch_icpp(1)%Y({i+1})"] = sol_L.Y[i] - case[f"patch_icpp(2)%Y({i+1})"] = sol_R.Y[i] + case[f"patch_icpp(1)%Y({i + 1})"] = sol_L.Y[i] + case[f"patch_icpp(2)%Y({i + 1})"] = sol_R.Y[i] if __name__ == "__main__": print(json.dumps(case)) diff --git a/examples/1D_shuosher_analytical/case.py b/examples/1D_shuosher_analytical/case.py index 8126714ff4..4f9ceaff56 100644 --- a/examples/1D_shuosher_analytical/case.py +++ b/examples/1D_shuosher_analytical/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_old/case.py b/examples/1D_shuosher_old/case.py index 7ad71e733a..7169f664b2 100644 --- a/examples/1D_shuosher_old/case.py +++ b/examples/1D_shuosher_old/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_teno5/case.py b/examples/1D_shuosher_teno5/case.py index 2477ba6a87..8132ed2067 100644 --- a/examples/1D_shuosher_teno5/case.py +++ b/examples/1D_shuosher_teno5/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_teno7/case.py b/examples/1D_shuosher_teno7/case.py index 9bac5d82a0..6c7963ced1 100644 --- a/examples/1D_shuosher_teno7/case.py +++ b/examples/1D_shuosher_teno7/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_wenojs5/case.py b/examples/1D_shuosher_wenojs5/case.py index 52763938fd..94a9564552 100644 --- a/examples/1D_shuosher_wenojs5/case.py +++ b/examples/1D_shuosher_wenojs5/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_wenom5/case.py b/examples/1D_shuosher_wenom5/case.py index c3dde3a589..fd479db63c 100644 --- a/examples/1D_shuosher_wenom5/case.py +++ b/examples/1D_shuosher_wenom5/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_shuosher_wenoz5/case.py b/examples/1D_shuosher_wenoz5/case.py index f959f363ae..4bc4789a6d 100644 --- a/examples/1D_shuosher_wenoz5/case.py +++ b/examples/1D_shuosher_wenoz5/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 1000 diff --git a/examples/1D_sodHypo/case.py b/examples/1D_sodHypo/case.py index 73c7a3f88e..2339e02026 100755 --- a/examples/1D_sodHypo/case.py +++ b/examples/1D_sodHypo/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 201 diff --git a/examples/1D_sodshocktube/case.py b/examples/1D_sodshocktube/case.py index 0cb23dbb92..ea0be00820 100755 --- a/examples/1D_sodshocktube/case.py +++ b/examples/1D_sodshocktube/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 399 diff --git a/examples/1D_sodshocktube_muscl/case.py b/examples/1D_sodshocktube_muscl/case.py index ec0d2cb3f8..df63c55535 100755 --- a/examples/1D_sodshocktube_muscl/case.py +++ b/examples/1D_sodshocktube_muscl/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 399 diff --git a/examples/1D_titarevtorro/case.py b/examples/1D_titarevtorro/case.py index 63dfe39b79..2d058d1746 100644 --- a/examples/1D_titarevtorro/case.py +++ b/examples/1D_titarevtorro/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 999 diff --git a/examples/1D_titarevtorro_analytical/case.py b/examples/1D_titarevtorro_analytical/case.py index 3a51bd86b0..73f012225f 100644 --- a/examples/1D_titarevtorro_analytical/case.py +++ b/examples/1D_titarevtorro_analytical/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Numerical setup Nx = 999 diff --git a/examples/2D_5wave_quasi1D/case.py b/examples/2D_5wave_quasi1D/case.py index 3a971977ba..0f5d6ba4a7 100755 --- a/examples/2D_5wave_quasi1D/case.py +++ b/examples/2D_5wave_quasi1D/case.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import math import json +import math # Numerical setup Nx = 399 diff --git a/examples/2D_GreshoVortex/case.py b/examples/2D_GreshoVortex/case.py index 96f3638333..ada31b2dd7 100644 --- a/examples/2D_GreshoVortex/case.py +++ b/examples/2D_GreshoVortex/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math gam = 1.4 Ma0 = 1e-3 diff --git a/examples/2D_IGR_2fluid/case.py b/examples/2D_IGR_2fluid/case.py index be59b206e3..3b7f520d9d 100644 --- a/examples/2D_IGR_2fluid/case.py +++ b/examples/2D_IGR_2fluid/case.py @@ -1,12 +1,11 @@ - #!/usr/bin/env python3 # This case file demonstrates the Laplace pressure jump of a water droplet in air. The laplace pressure jump # in 2D is given by delta = sigma / r where delta is the pressure jump, sigma is the surface tension coefficient, # and r is the radius of the droplet. The results of this simulation agree with theory to well within 1% # relative error. -import math import json +import math l = 1 eps = 1e-6 diff --git a/examples/2D_IGR_triple_point/case.py b/examples/2D_IGR_triple_point/case.py index 0af966e364..cf0708419c 100755 --- a/examples/2D_IGR_triple_point/case.py +++ b/examples/2D_IGR_triple_point/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math eps = 1e-8 Nx = 699 diff --git a/examples/2D_TaylorGreenVortex/case.py b/examples/2D_TaylorGreenVortex/case.py index 3157b23a3d..4a419d38e6 100644 --- a/examples/2D_TaylorGreenVortex/case.py +++ b/examples/2D_TaylorGreenVortex/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math gam_a = 1.4 Mu = 10000 # Define the fluid's dynamic viscosity diff --git a/examples/2D_acoustic_pulse/case.py b/examples/2D_acoustic_pulse/case.py index 899242ca48..be11755d9b 100644 --- a/examples/2D_acoustic_pulse/case.py +++ b/examples/2D_acoustic_pulse/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Numerical setup Nx = 99 diff --git a/examples/2D_acoustic_pulse_analytical/case.py b/examples/2D_acoustic_pulse_analytical/case.py index 73cec89a66..76192fd0e4 100644 --- a/examples/2D_acoustic_pulse_analytical/case.py +++ b/examples/2D_acoustic_pulse_analytical/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Numerical setup Nx = 99 diff --git a/examples/2D_axisym_shockbubble/case.py b/examples/2D_axisym_shockbubble/case.py index 3dcdb8ba3a..86bacd5a83 100644 --- a/examples/2D_axisym_shockbubble/case.py +++ b/examples/2D_axisym_shockbubble/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math ps = 248758.567 gam = 1.4 diff --git a/examples/2D_axisym_shockwatercavity/case.py b/examples/2D_axisym_shockwatercavity/case.py index bded43fd7a..b331322e76 100644 --- a/examples/2D_axisym_shockwatercavity/case.py +++ b/examples/2D_axisym_shockwatercavity/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # athmospheric pressure - Pa (used as reference value) patm = 101325 diff --git a/examples/2D_isentropicvortex/case.py b/examples/2D_isentropicvortex/case.py index a3b2b63e78..6bbffcd45c 100644 --- a/examples/2D_isentropicvortex/case.py +++ b/examples/2D_isentropicvortex/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Parameters epsilon = "5d0" @@ -20,14 +20,14 @@ f"{alpha_rho1_i}*(1d0 - ({alpha_rho1_i}/{pres_i})*({epsilon}/(2d0*pi))*" + f"({epsilon}/(8d0*{alpha}*({gamma} + 1d0)*pi))*" + f"exp(2d0*{alpha}*(1d0 - (x - xc)**2d0" - + f"- (y - yc)**2d0))" + + "- (y - yc)**2d0))" + f")**{gamma}" ) pres = ( f"{pres_i}*(1d0 - ({alpha_rho1_i}/{pres_i})*({epsilon}/(2d0*pi))*" + f"({epsilon}/(8d0*{alpha}*({gamma} + 1d0)*pi))*" + f"exp(2d0*{alpha}*(1d0 - (x - xc)**2d0" - + f"- (y - yc)**2d0))" + + "- (y - yc)**2d0))" + f")**({gamma} + 1d0)" ) diff --git a/examples/2D_isentropicvortex_analytical/case.py b/examples/2D_isentropicvortex_analytical/case.py index f25773e1a5..ba31fb491c 100644 --- a/examples/2D_isentropicvortex_analytical/case.py +++ b/examples/2D_isentropicvortex_analytical/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Parameters epsilon = "5d0" @@ -20,14 +20,14 @@ f"{alpha_rho1_i}*(1d0 - ({alpha_rho1_i}/{pres_i})*({epsilon}/(2d0*pi))*" + f"({epsilon}/(8d0*{alpha}*({gamma} + 1d0)*pi))*" + f"exp(2d0*{alpha}*(1d0 - (x - xc)**2d0" - + f"- (y - yc)**2d0))" + + "- (y - yc)**2d0))" + f")**{gamma}" ) pres = ( f"{pres_i}*(1d0 - ({alpha_rho1_i}/{pres_i})*({epsilon}/(2d0*pi))*" + f"({epsilon}/(8d0*{alpha}*({gamma} + 1d0)*pi))*" + f"exp(2d0*{alpha}*(1d0 - (x - xc)**2d0" - + f"- (y - yc)**2d0))" + + "- (y - yc)**2d0))" + f")**({gamma} + 1d0)" ) diff --git a/examples/2D_jet/case.py b/examples/2D_jet/case.py index c4a9374851..7da489c055 100644 --- a/examples/2D_jet/case.py +++ b/examples/2D_jet/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math pA = 101325 rhoA = 1.29 diff --git a/examples/2D_lagrange_bubblescreen/case.py b/examples/2D_lagrange_bubblescreen/case.py index 74a4540ddf..eba8edb699 100644 --- a/examples/2D_lagrange_bubblescreen/case.py +++ b/examples/2D_lagrange_bubblescreen/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Bubble screen # Description: A planar acoustic wave interacts with a bubble cloud diff --git a/examples/2D_laplace_pressure_jump/case.py b/examples/2D_laplace_pressure_jump/case.py index e66b63645d..ec1b2a32a8 100644 --- a/examples/2D_laplace_pressure_jump/case.py +++ b/examples/2D_laplace_pressure_jump/case.py @@ -4,8 +4,8 @@ # and r is the radius of the droplet. The results of this simulation agree with theory to well within 1% # relative error. -import math import json +import math l = 0.375 diff --git a/examples/2D_lungwave/case.py b/examples/2D_lungwave/case.py index 85b0387919..badea226f7 100644 --- a/examples/2D_lungwave/case.py +++ b/examples/2D_lungwave/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math pi = 3.141592653589 # material parameters diff --git a/examples/2D_lungwave_horizontal/case.py b/examples/2D_lungwave_horizontal/case.py index 8fea0a0285..0c9d2632a3 100644 --- a/examples/2D_lungwave_horizontal/case.py +++ b/examples/2D_lungwave_horizontal/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math pi = 3.141592653589 # material parameters diff --git a/examples/2D_mixing_artificial_Ma/case.py b/examples/2D_mixing_artificial_Ma/case.py index e9d926df71..c14b7b8f16 100644 --- a/examples/2D_mixing_artificial_Ma/case.py +++ b/examples/2D_mixing_artificial_Ma/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # FLUID PROPERTIES (WATER, VAPOR & AIR) # Water diff --git a/examples/2D_patch_modal_shape/case.py b/examples/2D_patch_modal_shape/case.py index 8eb3bf58ce..c1be303320 100644 --- a/examples/2D_patch_modal_shape/case.py +++ b/examples/2D_patch_modal_shape/case.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Minimal 2D acoustic case with geometry 13 (2D modal Fourier shape). Additive form.""" -import math + import json +import math Nx, Ny = 64, 64 Lx, Ly = 8.0, 8.0 diff --git a/examples/2D_patch_modal_shape_exp/case.py b/examples/2D_patch_modal_shape_exp/case.py index abf98c620b..9d1873e4d5 100644 --- a/examples/2D_patch_modal_shape_exp/case.py +++ b/examples/2D_patch_modal_shape_exp/case.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Minimal 2D acoustic case with geometry 13 in exponential form (modal_use_exp_form).""" -import math + import json +import math Nx, Ny = 64, 64 Lx = 8.0 diff --git a/examples/2D_phasechange_bubble/case.py b/examples/2D_phasechange_bubble/case.py index 012afe3ad3..c7b782daab 100644 --- a/examples/2D_phasechange_bubble/case.py +++ b/examples/2D_phasechange_bubble/case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import math -import json import argparse +import json +import math parser = argparse.ArgumentParser(prog="phasechange", description="phase change considering both 5 and 6 equation models.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--mfc", type=json.loads, default={}, metavar="DICT", help="MFC's toolchain's internal state.") diff --git a/examples/2D_rayleigh_taylor/case.py b/examples/2D_rayleigh_taylor/case.py index 029178732a..ab7d9267db 100644 --- a/examples/2D_rayleigh_taylor/case.py +++ b/examples/2D_rayleigh_taylor/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math lam = 0.2 h = 1.2 diff --git a/examples/2D_shockbubble/case.py b/examples/2D_shockbubble/case.py index 9d66dafab8..6a195c7a2f 100644 --- a/examples/2D_shockbubble/case.py +++ b/examples/2D_shockbubble/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math ps = 248758.567 gam = 1.4 diff --git a/examples/2D_shockdroplet/case.py b/examples/2D_shockdroplet/case.py index 4167f0f6a4..910d1e42aa 100755 --- a/examples/2D_shockdroplet/case.py +++ b/examples/2D_shockdroplet/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math Ma = 1.4 ps = 238558 diff --git a/examples/2D_shockdroplet_muscl/case.py b/examples/2D_shockdroplet_muscl/case.py index 5f8f2fbce2..20915f1577 100755 --- a/examples/2D_shockdroplet_muscl/case.py +++ b/examples/2D_shockdroplet_muscl/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math Ma = 1.4 ps = 238558 diff --git a/examples/2D_shocktube_phasechange/case.py b/examples/2D_shocktube_phasechange/case.py index 48277019a8..fe4614316e 100644 --- a/examples/2D_shocktube_phasechange/case.py +++ b/examples/2D_shocktube_phasechange/case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import math -import json import argparse +import json +import math parser = argparse.ArgumentParser(prog="phasechange", description="phase change considering both 5 and 6 equation models.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--mfc", type=json.loads, default="{}", metavar="DICT", help="MFC's toolchain's internal state.") diff --git a/examples/2D_triple_point/case.py b/examples/2D_triple_point/case.py index 89a0795834..a67ebe8cce 100755 --- a/examples/2D_triple_point/case.py +++ b/examples/2D_triple_point/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math eps = 1e-8 Nx = 699 diff --git a/examples/2D_viscous/case.py b/examples/2D_viscous/case.py index de0fa1a7cf..f525067b29 100644 --- a/examples/2D_viscous/case.py +++ b/examples/2D_viscous/case.py @@ -3,8 +3,8 @@ # Dependencies and Logistics # Command to navigate between directories -import math import json +import math myeps = 1.4 / 150.0 diff --git a/examples/2D_whale_bubble_annulus/case.py b/examples/2D_whale_bubble_annulus/case.py index ecb9215767..e6de0dca20 100755 --- a/examples/2D_whale_bubble_annulus/case.py +++ b/examples/2D_whale_bubble_annulus/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python2 -import math import json +import math # x0 = 10.E-06 x0 = 1.0 diff --git a/examples/2D_zero_circ_vortex/case.py b/examples/2D_zero_circ_vortex/case.py index 2eb8b24643..a400cb02e5 100644 --- a/examples/2D_zero_circ_vortex/case.py +++ b/examples/2D_zero_circ_vortex/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Numerical setup Nx = 250 diff --git a/examples/2D_zero_circ_vortex_analytical/case.py b/examples/2D_zero_circ_vortex_analytical/case.py index 793f265b7c..da28a8c43e 100644 --- a/examples/2D_zero_circ_vortex_analytical/case.py +++ b/examples/2D_zero_circ_vortex_analytical/case.py @@ -1,5 +1,5 @@ -import math import json +import math # Numerical setup Nx = 250 diff --git a/examples/3D_IGR_33jet/case.py b/examples/3D_IGR_33jet/case.py index 236812f8b2..d7b32157a6 100644 --- a/examples/3D_IGR_33jet/case.py +++ b/examples/3D_IGR_33jet/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Domain parameters D = 2.5 # Jet diameter diff --git a/examples/3D_IGR_TaylorGreenVortex/case.py b/examples/3D_IGR_TaylorGreenVortex/case.py index 20372828e2..f10c503746 100644 --- a/examples/3D_IGR_TaylorGreenVortex/case.py +++ b/examples/3D_IGR_TaylorGreenVortex/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math N = 99 diff --git a/examples/3D_IGR_TaylorGreenVortex_nvidia/case.py b/examples/3D_IGR_TaylorGreenVortex_nvidia/case.py index e2b22e8017..daf00de189 100644 --- a/examples/3D_IGR_TaylorGreenVortex_nvidia/case.py +++ b/examples/3D_IGR_TaylorGreenVortex_nvidia/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math N = 799 Nx = N diff --git a/examples/3D_IGR_jet/case.py b/examples/3D_IGR_jet/case.py index e26abdacd7..e0062f0991 100644 --- a/examples/3D_IGR_jet/case.py +++ b/examples/3D_IGR_jet/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Domain parameters diff --git a/examples/3D_TaylorGreenVortex/case.py b/examples/3D_TaylorGreenVortex/case.py index 8b48c2990f..e6df484570 100644 --- a/examples/3D_TaylorGreenVortex/case.py +++ b/examples/3D_TaylorGreenVortex/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math N = 256 diff --git a/examples/3D_TaylorGreenVortex_analytical/case.py b/examples/3D_TaylorGreenVortex_analytical/case.py index de440980cb..11cab03bdb 100644 --- a/examples/3D_TaylorGreenVortex_analytical/case.py +++ b/examples/3D_TaylorGreenVortex_analytical/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math N = 256 diff --git a/examples/3D_lagrange_bubblescreen/case.py b/examples/3D_lagrange_bubblescreen/case.py index b44799dcf6..ab61ffbe42 100644 --- a/examples/3D_lagrange_bubblescreen/case.py +++ b/examples/3D_lagrange_bubblescreen/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Bubble screen # Description: A planar acoustic wave interacts with a bubble cloud diff --git a/examples/3D_lagrange_shbubcollapse/case.py b/examples/3D_lagrange_shbubcollapse/case.py index 58240fea99..73e0b88f69 100644 --- a/examples/3D_lagrange_shbubcollapse/case.py +++ b/examples/3D_lagrange_shbubcollapse/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # Single bubble collapse # Description: A planar acoustic wave interacts with a single bubble diff --git a/examples/3D_patch_spherical_harmonic/case.py b/examples/3D_patch_spherical_harmonic/case.py index d2cb2423ab..dc0314a869 100644 --- a/examples/3D_patch_spherical_harmonic/case.py +++ b/examples/3D_patch_spherical_harmonic/case.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Minimal 3D acoustic case with geometry 14 (spherical harmonic surface).""" -import math + import json +import math L = 8.0 N = 50 diff --git a/examples/3D_phasechange_bubble/case.py b/examples/3D_phasechange_bubble/case.py index 4e3c5f446f..6c71aa1936 100644 --- a/examples/3D_phasechange_bubble/case.py +++ b/examples/3D_phasechange_bubble/case.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import math -import json import argparse +import json +import math parser = argparse.ArgumentParser(prog="phasechange", description="phase change considering both 5 and 6 equation models.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--mfc", type=json.loads, default="{}", metavar="DICT", help="MFC's toolchain's internal state.") diff --git a/examples/3D_rayleigh_taylor/case.py b/examples/3D_rayleigh_taylor/case.py index 3f3ec8a7e3..647127426d 100644 --- a/examples/3D_rayleigh_taylor/case.py +++ b/examples/3D_rayleigh_taylor/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math lam = 0.2 h = 1.2 diff --git a/examples/3D_rayleigh_taylor_muscl/case.py b/examples/3D_rayleigh_taylor_muscl/case.py index b72ea84236..9962b24864 100644 --- a/examples/3D_rayleigh_taylor_muscl/case.py +++ b/examples/3D_rayleigh_taylor_muscl/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math lam = 0.2 h = 1.2 diff --git a/examples/3D_recovering_sphere/case.py b/examples/3D_recovering_sphere/case.py index c16ac81b55..9a92c654a2 100644 --- a/examples/3D_recovering_sphere/case.py +++ b/examples/3D_recovering_sphere/case.py @@ -2,8 +2,8 @@ # This simulation shows the early stages of a cubic droplet recovering a spherical shape due to capillary # forces. While the relaxation is not complete, it demonstrates the expecteed symmetric behavior. -import math import json +import math l = 0.375 diff --git a/examples/3D_shockdroplet/case.py b/examples/3D_shockdroplet/case.py index e935861cc1..10741bc82e 100644 --- a/examples/3D_shockdroplet/case.py +++ b/examples/3D_shockdroplet/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # athmospheric pressure - Pa (used as reference value) patm = 101325 diff --git a/examples/3D_shockdroplet_muscl/case.py b/examples/3D_shockdroplet_muscl/case.py index 8d4337b0da..699ace9379 100644 --- a/examples/3D_shockdroplet_muscl/case.py +++ b/examples/3D_shockdroplet_muscl/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # athmospheric pressure - Pa (used as reference value) patm = 101325 diff --git a/examples/3D_turb_mixing/case.py b/examples/3D_turb_mixing/case.py index 5ff57c0e34..f4b8a0c486 100644 --- a/examples/3D_turb_mixing/case.py +++ b/examples/3D_turb_mixing/case.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import math import json +import math # SURROUNDING FLOW # Nondimensional parameters diff --git a/examples/nD_perfect_reactor/analyze.py b/examples/nD_perfect_reactor/analyze.py index a7f212fd3f..738e9114a6 100644 --- a/examples/nD_perfect_reactor/analyze.py +++ b/examples/nD_perfect_reactor/analyze.py @@ -15,48 +15,51 @@ MFC writes species as Y_{name} (e.g. Y_OH) and density as alpha_rho1. Run `./mfc.sh viz . --list-vars` to verify variable names in your output. """ -from case import dt, Tend, SAVE_COUNT, sol -from mfc.viz import assemble_silo, discover_timesteps -from tqdm import tqdm -import cantera as ct -import matplotlib.pyplot as plt + import sys +import cantera as ct import matplotlib -matplotlib.use('Agg') +import matplotlib.pyplot as plt +from case import SAVE_COUNT, Tend, dt, sol +from tqdm import tqdm + +from mfc.viz import assemble_silo, discover_timesteps + +matplotlib.use("Agg") # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- -CASE_DIR = '.' -Y_MAJORS = {'H', 'O', 'OH', 'HO2'} -Y_MINORS = {'H2O', 'H2O2'} +CASE_DIR = "." +Y_MAJORS = {"H", "O", "OH", "HO2"} +Y_MINORS = {"H2O", "H2O2"} Y_VARS = Y_MAJORS | Y_MINORS -oh_idx = sol.species_index('OH') -skinner_induction_time = 0.052e-3 # Skinner & Ringrose (1965) +oh_idx = sol.species_index("OH") +skinner_induction_time = 0.052e-3 # Skinner & Ringrose (1965) # --------------------------------------------------------------------------- # Load MFC output # --------------------------------------------------------------------------- -steps = discover_timesteps(CASE_DIR, 'silo') +steps = discover_timesteps(CASE_DIR, "silo") if not steps: - sys.exit('No silo timesteps found — did you run post_process?') + sys.exit("No silo timesteps found — did you run post_process?") mfc_times = [] mfc_rhos = [] mfc_Ys = {y: [] for y in Y_VARS} -for step in tqdm(steps, desc='Loading MFC output'): +for step in tqdm(steps, desc="Loading MFC output"): assembled = assemble_silo(CASE_DIR, step) # Perfectly stirred reactor: spatially uniform — take the midpoint cell. mid = assembled.x_cc.size // 2 mfc_times.append(step * dt) # alpha_rho1 = partial density of fluid 1; equals total density for single-fluid cases. - mfc_rhos.append(float(assembled.variables['alpha_rho1'][mid])) + mfc_rhos.append(float(assembled.variables["alpha_rho1"][mid])) for y in Y_VARS: - mfc_Ys[y].append(float(assembled.variables[f'Y_{y}'][mid])) + mfc_Ys[y].append(float(assembled.variables[f"Y_{y}"][mid])) # --------------------------------------------------------------------------- # Cantera 0-D reference @@ -96,38 +99,35 @@ def find_induction_time(ts, Ys_OH, rhos): ct_induction = find_induction_time(ct_ts, [Y[oh_idx] for Y in ct_Ys], ct_rhos) -mfc_induction = find_induction_time(mfc_times, mfc_Ys['OH'], mfc_rhos) +mfc_induction = find_induction_time(mfc_times, mfc_Ys["OH"], mfc_rhos) -print('Induction Times ([OH] >= 1e-6 mol/m^3):') -print(f' Skinner et al.: {skinner_induction_time:.3e} s') -print(f' Cantera: {ct_induction:.3e} s' - if ct_induction is not None else ' Cantera: not reached') -print(f' (Che)MFC: {mfc_induction:.3e} s' - if mfc_induction is not None else ' (Che)MFC: not reached') +print("Induction Times ([OH] >= 1e-6 mol/m^3):") +print(f" Skinner et al.: {skinner_induction_time:.3e} s") +print(f" Cantera: {ct_induction:.3e} s" if ct_induction is not None else " Cantera: not reached") +print(f" (Che)MFC: {mfc_induction:.3e} s" if mfc_induction is not None else " (Che)MFC: not reached") # --------------------------------------------------------------------------- # Plot # --------------------------------------------------------------------------- fig, axes = plt.subplots(1, 2, figsize=(12, 6)) -_colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] +_colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] _color = {y: _colors[i % len(_colors)] for i, y in enumerate(sorted(Y_VARS))} for ax, group in zip(axes, [sorted(Y_MAJORS), sorted(Y_MINORS)]): for y in group: - ax.plot(mfc_times, mfc_Ys[y], color=_color[y], label=f'${y}$') - ax.plot(ct_ts, [Y[sol.species_index(y)] for Y in ct_Ys], - linestyle=':', color=_color[y], alpha=0.6, label=f'{y} (Cantera)') - ax.set_xlabel('Time (s)') - ax.set_ylabel('$Y_k$') - ax.set_xscale('log') - ax.set_yscale('log') - ax.legend(title='Species', ncol=2) + ax.plot(mfc_times, mfc_Ys[y], color=_color[y], label=f"${y}$") + ax.plot(ct_ts, [Y[sol.species_index(y)] for Y in ct_Ys], linestyle=":", color=_color[y], alpha=0.6, label=f"{y} (Cantera)") + ax.set_xlabel("Time (s)") + ax.set_ylabel("$Y_k$") + ax.set_xscale("log") + ax.set_yscale("log") + ax.legend(title="Species", ncol=2) # Mark induction times on both panels induction_lines = [ - (skinner_induction_time, 'r', '-', 'Skinner et al.'), - (mfc_induction, 'b', '-.', '(Che)MFC'), - (ct_induction, 'g', ':', 'Cantera'), + (skinner_induction_time, "r", "-", "Skinner et al."), + (mfc_induction, "b", "-.", "(Che)MFC"), + (ct_induction, "g", ":", "Cantera"), ] for ax in axes: for t, c, ls, _ in induction_lines: @@ -135,14 +135,13 @@ def find_induction_time(ts, Ys_OH, rhos): ax.axvline(t, color=c, linestyle=ls) axes[0].legend( - handles=[plt.Line2D([0], [0], color=c, linestyle=ls) - for t, c, ls, _lbl in induction_lines if t is not None], + handles=[plt.Line2D([0], [0], color=c, linestyle=ls) for t, c, ls, _lbl in induction_lines if t is not None], labels=[lbl for t, _c, _ls, lbl in induction_lines if t is not None], - title='Induction Times', - loc='lower right', + title="Induction Times", + loc="lower right", ) plt.tight_layout() -plt.savefig('plots.png', dpi=300) +plt.savefig("plots.png", dpi=300) plt.close() -print('Saved: plots.png') +print("Saved: plots.png") diff --git a/examples/nD_perfect_reactor/case.py b/examples/nD_perfect_reactor/case.py index 680978dfbc..d0000deb89 100644 --- a/examples/nD_perfect_reactor/case.py +++ b/examples/nD_perfect_reactor/case.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # Reference: # + https://doi.org/10.1063/1.1696266 -import json import argparse +import json + import cantera as ct from mfc.case_utils import * @@ -119,7 +120,7 @@ for i in range(len(sol.Y)): case[f"chem_wrt_Y({i + 1})"] = "T" - case[f"patch_icpp(1)%Y({i+1})"] = sol.Y[i] + case[f"patch_icpp(1)%Y({i + 1})"] = sol.Y[i] case = remove_higher_dimensional_keys(case, args.ndim) diff --git a/examples/scaling/analyze.py b/examples/scaling/analyze.py index 0f358417c3..ce9831e12e 100644 --- a/examples/scaling/analyze.py +++ b/examples/scaling/analyze.py @@ -1,8 +1,9 @@ import os import re -import pandas as pd from io import StringIO +import pandas as pd + def parse_time_avg(path): last_val = None @@ -172,7 +173,7 @@ def parse_reference_file(filename): subset["grind_time"] = times subset["rel_perf"] = subset["grind_time"] / ref["grind_time"].values - print(f"Grind Time - Single Device") + print("Grind Time - Single Device") print(subset[["memory", "grind_time", "rel_perf"]].to_string(index=False)) print() diff --git a/examples/scaling/benchmark.py b/examples/scaling/benchmark.py index 3efc5615df..a3d2620555 100644 --- a/examples/scaling/benchmark.py +++ b/examples/scaling/benchmark.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -import sys +import argparse import json import math +import sys import typing -import argparse parser = argparse.ArgumentParser( prog="scaling_and_perf", diff --git a/examples/scaling/export.py b/examples/scaling/export.py index 3208cf8541..c8b389b4a6 100644 --- a/examples/scaling/export.py +++ b/examples/scaling/export.py @@ -1,7 +1,7 @@ -import re -import os import csv import glob +import os +import re import statistics from dataclasses import dataclass, fields diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000000..1131d80b5c --- /dev/null +++ b/ruff.toml @@ -0,0 +1,25 @@ +line-length = 200 +target-version = "py39" +exclude = ["_version.py"] + +[lint] +select = ["E", "F", "W", "I", "PL"] +ignore = [ + # Complexity thresholds (project style) + "PLR0911", # too-many-return-statements + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0915", # too-many-statements + "PLR2004", # magic-value-comparison + # Import patterns (project style) + "PLC0415", # import-outside-toplevel + # Global variable reads (module-level singleton pattern) + "PLW0602", # global-variable-not-assigned +] + +[lint.per-file-ignores] +"examples/**/*.py" = ["F401", "F403", "F405", "F811", "F821", "E402", "E722", "E741"] +"benchmarks/*/case.py" = ["F401", "F403", "F405", "F811", "F821", "E402", "E741"] + +[lint.isort] +known-first-party = ["mfc"] diff --git a/toolchain/bootstrap/format.sh b/toolchain/bootstrap/format.sh index 8dc3841036..424d07e562 100644 --- a/toolchain/bootstrap/format.sh +++ b/toolchain/bootstrap/format.sh @@ -53,14 +53,17 @@ if [[ ${#PATHS[@]} -gt 0 ]]; then exit 1 fi - # Format Python files - if ! find $SEARCH_PATHS -type f 2>/dev/null | grep -E '\.(py)$' \ - | xargs --no-run-if-empty -L 1 -P ${JOBS:-1} $SHELL toolchain/bootstrap/format_python.sh; then + # Format Python files with ruff (auto-fix lint issues, then format) + if ! ruff check --fix-only $SEARCH_PATHS; then + error "Auto-fixing Python lint issues failed." + exit 1 + fi + if ! ruff format $SEARCH_PATHS; then error "Formatting Python files failed." exit 1 fi else - # Default: format src/, examples/, and benchmarks/ + # Default: format src/ (Fortran), toolchain/ examples/ benchmarks/ (Python) # Format Fortran files (.f90, .fpp) in src/ if ! find src -type f 2>/dev/null | grep -Ev 'autogen' | grep -E '\.(f90|fpp)$' \ @@ -69,17 +72,13 @@ else exit 1 fi - # Format Python files in examples/ - if ! find examples -type f 2>/dev/null | grep -E '\.(py)$' \ - | xargs --no-run-if-empty -L 1 -P ${JOBS:-1} $SHELL toolchain/bootstrap/format_python.sh; then - error "Formatting MFC examples failed." + # Format Python files with ruff (auto-fix lint issues, then format) + if ! ruff check --fix-only toolchain/ examples/ benchmarks/; then + error "Auto-fixing Python lint issues failed." exit 1 fi - - # Format Python files in benchmarks/ - if ! find benchmarks -type f 2>/dev/null | grep -E '\.(py)$' \ - | xargs --no-run-if-empty -L 1 -P ${JOBS:-1} $SHELL toolchain/bootstrap/format_python.sh; then - error "Formatting MFC benchmarks failed." + if ! ruff format toolchain/ examples/ benchmarks/; then + error "Formatting Python files failed." exit 1 fi fi diff --git a/toolchain/bootstrap/format_python.sh b/toolchain/bootstrap/format_python.sh deleted file mode 100644 index 3d95c7aadc..0000000000 --- a/toolchain/bootstrap/format_python.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -. toolchain/util.sh - -echo "> $1" - -# Use autopep8 for Python formatting -# (black has issues with Python 3.12.5 which is common on HPC systems) -if ! autopep8 --in-place --max-line-length 200 "$1" 2>&1; then - error "Failed to format $1 with autopep8" - exit 1 -fi - diff --git a/toolchain/bootstrap/lint.sh b/toolchain/bootstrap/lint.sh index 89649cd5aa..6ff9ca88eb 100644 --- a/toolchain/bootstrap/lint.sh +++ b/toolchain/bootstrap/lint.sh @@ -12,24 +12,28 @@ for arg in "$@"; do esac done -log "(venv) Running$MAGENTA pylint$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""toolchain$COLOR_RESET." +log "(venv) Auto-fixing safe lint issues with$MAGENTA ruff$COLOR_RESET..." -pylint -d R1722,W0718,C0301,C0116,C0115,C0114,C0410,W0622,W0640,C0103,W1309,C0411,W1514,R0401,W0511,C0321,C3001,R0801,R0911,R0912 "$(pwd)/toolchain/" +ruff check --fix-only toolchain/ examples/*/case.py benchmarks/*/case.py -log "(venv) Running$MAGENTA pylint$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""examples$COLOR_RESET." +log "(venv) Running$MAGENTA ruff$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""toolchain$COLOR_RESET." -pylint -d C0103,C0114,C0301,R0801,C0410,W0611,W1514,E0401,C0115,C0116,C0200,W1309,W0401,E0602,R1720,W0614,E1101 $(pwd)/examples/*/case.py +ruff check toolchain/ -log "(venv) Running$MAGENTA pylint$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""benchmarks$COLOR_RESET." +log "(venv) Running$MAGENTA ruff$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""examples$COLOR_RESET." -pylint -d C0103,C0114,C0301,R0801,C0410,W0611,W1514,E0401,C0115,C0116,C0200,W1309,W0401,E0602,R1720,W0614,E1101 $(pwd)/benchmarks/*/case.py +ruff check examples/*/case.py + +log "(venv) Running$MAGENTA ruff$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""benchmarks$COLOR_RESET." + +ruff check benchmarks/*/case.py # Run toolchain unit tests unless --no-test is specified if [ "$RUN_TESTS" = true ]; then log "(venv) Running$MAGENTA unit tests$COLOR_RESET on$MAGENTA MFC$COLOR_RESET's $MAGENTA""toolchain$COLOR_RESET." # Run tests as modules from the toolchain directory to resolve relative imports - cd "$(pwd)/toolchain" + cd toolchain python3 -m unittest mfc.params_tests.test_registry mfc.params_tests.test_definitions mfc.params_tests.test_validate mfc.params_tests.test_integration -v python3 -m unittest mfc.cli.test_cli -v python3 -m unittest mfc.viz.test_viz -v diff --git a/toolchain/indenter.py b/toolchain/indenter.py index bea485c181..4abfad7b4c 100644 --- a/toolchain/indenter.py +++ b/toolchain/indenter.py @@ -1,38 +1,37 @@ #!/usr/bin/env python3 -import os, argparse +import argparse +import os + def main(): - parser = argparse.ArgumentParser( - prog='indenter.py', - description='Adjust indentation of OpenACC directives in a Fortran file') - parser.add_argument('filepath', metavar='input_file', type=str, help='File to format') + parser = argparse.ArgumentParser(prog="indenter.py", description="Adjust indentation of OpenACC directives in a Fortran file") + parser.add_argument("filepath", metavar="input_file", type=str, help="File to format") args = vars(parser.parse_args()) - filepath = args['filepath'] + filepath = args["filepath"] temp_filepath = f"{filepath}.new" adjust_indentation(filepath, temp_filepath) os.replace(temp_filepath, filepath) -BLOCK_STARTERS = ('if', 'do', '#:if', '#:else', "#ifdef", "#else") -BLOCK_ENDERS = ('end', 'contains', 'else', '#:end', '#:else', '#else', '#endif') -LOOP_DIRECTIVES = ('!$acc loop', '!$acc parallel loop') -INDENTERS = ('!DIR', '!$acc') -# pylint: disable=too-many-branches +BLOCK_STARTERS = ("if", "do", "#:if", "#:else", "#ifdef", "#else") +BLOCK_ENDERS = ("end", "contains", "else", "#:end", "#:else", "#else", "#endif") +LOOP_DIRECTIVES = ("!$acc loop", "!$acc parallel loop") +INDENTERS = ("!DIR", "!$acc") + + def adjust_indentation(input_file, output_file): - max_empty_lines=4 - indent_len=4 + max_empty_lines = 4 + indent_len = 4 - with open(input_file, 'r') as file_in, open(output_file, 'w') as file_out: + with open(input_file, "r") as file_in, open(output_file, "w") as file_out: lines = file_in.readlines() # this makes sure !$acc lines that have line continuations get indented at proper level - # pylint: disable=too-many-nested-blocks for _ in range(10): # loop through file - # pylint: disable=consider-using-enumerate for i in range(len(lines)): if lines[i].lstrip().startswith(INDENTERS) and i + 1 < len(lines): j = i + 1 @@ -43,12 +42,12 @@ def adjust_indentation(input_file, output_file): if lines[j].lstrip().startswith(BLOCK_ENDERS): empty_lines = max_empty_lines # skip empty lines - elif lines[j].strip() == '': + elif lines[j].strip() == "": empty_lines += 1 # indent acc lines elif not lines[j].lstrip().startswith(INDENTERS): indent = len(lines[j]) - len(lines[j].lstrip()) - lines[i] = ' ' * indent + lines[i].lstrip() + lines[i] = " " * indent + lines[i].lstrip() break j += 1 # if looking down just finds empty lines, start looking up for indentation level @@ -56,30 +55,30 @@ def adjust_indentation(input_file, output_file): k = i - 1 while k >= 0: # if line above is not empty - if lines[k].strip() != '': + if lines[k].strip() != "": # if line 2 above ends with line continuation, indent at that level - if lines[k-1].strip().endswith('&'): - indent = len(lines[k-1]) - len(lines[k-1].lstrip()) + if lines[k - 1].strip().endswith("&"): + indent = len(lines[k - 1]) - len(lines[k - 1].lstrip()) # if line above starts a loop or branch, indent elif lines[k].lstrip().startswith(BLOCK_STARTERS): indent = indent_len + (len(lines[k]) - len(lines[k].lstrip())) # else indent at level of line above else: indent = len(lines[k]) - len(lines[k].lstrip()) - lines[i] = ' ' * indent + lines[i].lstrip() + lines[i] = " " * indent + lines[i].lstrip() break k -= 1 # remove empty lines following an acc loop directive i = 0 while i < len(lines): - if lines[i].lstrip().startswith(LOOP_DIRECTIVES) and \ - i+1 < len(lines) and lines[i+1].strip() == '': + if lines[i].lstrip().startswith(LOOP_DIRECTIVES) and i + 1 < len(lines) and lines[i + 1].strip() == "": file_out.write(lines[i]) i += 2 else: file_out.write(lines[i]) i += 1 + if __name__ == "__main__": main() diff --git a/toolchain/main.py b/toolchain/main.py index d024fb46d9..5efd9ff5ec 100644 --- a/toolchain/main.py +++ b/toolchain/main.py @@ -1,21 +1,26 @@ #!/usr/bin/env python3 -import os, signal, getpass, platform, itertools +import getpass +import itertools +import os +import platform +import signal # Only import what's needed for startup - other modules are loaded lazily -from mfc import args, lock, state -from mfc.state import ARG -from mfc.common import MFC_LOGO, MFC_ROOT_DIR, MFCException, quit, format_list_to_string, does_command_exist, setup_debug_logging +from mfc import args, lock, state +from mfc.common import MFC_LOGO, MFC_ROOT_DIR, MFCException, does_command_exist, format_list_to_string, quit, setup_debug_logging from mfc.printer import cons +from mfc.state import ARG def __do_regenerate(toolchain: str): """Perform the actual regeneration of completion scripts and schema.""" - import json # pylint: disable=import-outside-toplevel - from pathlib import Path # pylint: disable=import-outside-toplevel - from mfc.cli.commands import MFC_CLI_SCHEMA # pylint: disable=import-outside-toplevel - from mfc.cli.completion_gen import generate_bash_completion, generate_zsh_completion # pylint: disable=import-outside-toplevel - from mfc.params.generators.json_schema_gen import generate_json_schema # pylint: disable=import-outside-toplevel + import json + from pathlib import Path + + from mfc.cli.commands import MFC_CLI_SCHEMA + from mfc.cli.completion_gen import generate_bash_completion, generate_zsh_completion + from mfc.params.generators.json_schema_gen import generate_json_schema cons.print("[dim]Auto-regenerating completion scripts...[/dim]") @@ -27,14 +32,14 @@ def __do_regenerate(toolchain: str): (completions_dir / "_mfc").write_text(generate_zsh_completion(MFC_CLI_SCHEMA)) # Generate JSON schema - with open(Path(toolchain) / "mfc-case-schema.json", 'w', encoding='utf-8') as f: + with open(Path(toolchain) / "mfc-case-schema.json", "w", encoding="utf-8") as f: json.dump(generate_json_schema(include_descriptions=True), f, indent=2) def __update_installed_completions(toolchain: str): """Update installed shell completions if they're older than generated ones.""" - import shutil # pylint: disable=import-outside-toplevel - from pathlib import Path # pylint: disable=import-outside-toplevel + import shutil + from pathlib import Path src_dir = Path(toolchain) / "completions" dst_dir = Path.home() / ".local" / "share" / "mfc" / "completions" @@ -81,10 +86,7 @@ def __ensure_generated_files(): return # No source files found, skip check # Check if any generated file is missing or older than sources - needs_regen = any( - not os.path.exists(g) or os.path.getmtime(g) < source_mtime - for g in generated - ) + needs_regen = any(not os.path.exists(g) or os.path.getmtime(g) < source_mtime for g in generated) if needs_regen: __do_regenerate(toolchain) @@ -92,34 +94,31 @@ def __ensure_generated_files(): # Always check if completions need to be installed or updated __update_installed_completions(toolchain) + def __print_greeting(): - MFC_LOGO_LINES = MFC_LOGO.splitlines() + MFC_LOGO_LINES = MFC_LOGO.splitlines() max_logo_line_length = max(len(line) for line in MFC_LOGO_LINES) - host_line = f"{getpass.getuser()}@{platform.node()} [{platform.system()}]" + host_line = f"{getpass.getuser()}@{platform.node()} [{platform.system()}]" targets_line = f"[bold]--targets {format_list_to_string(ARG('targets'), 'magenta', 'None')}[/bold]" - help_line = "$ ./mfc.sh (build, run, test, clean, new, validate, params) --help" + help_line = "$ ./mfc.sh (build, run, test, clean, new, validate, params) --help" MFC_SIDEBAR_LINES = [ f"[bold]{host_line}[/bold]", - '-' * len(host_line), - '', + "-" * len(host_line), + "", f"[bold]--jobs [magenta]{ARG('jobs')}[/magenta][/bold]", f"[bold]{' '.join(state.gCFG.make_options())}[/bold]", targets_line if ARG("command") != "test" else "", - '', - '-' * len(help_line), + "", + "-" * len(help_line), f"[yellow]{help_line}[/yellow]", ] for a, b in itertools.zip_longest(MFC_LOGO_LINES, MFC_SIDEBAR_LINES): - a = a or '' - lhs = a.ljust(max_logo_line_length) - rhs = b or '' - cons.print( - f"[bold]{lhs}[/bold] | {rhs}", - highlight=False - ) + lhs = (a or "").ljust(max_logo_line_length) + rhs = b or "" + cons.print(f"[bold]{lhs}[/bold] | {rhs}", highlight=False) cons.print() @@ -131,57 +130,73 @@ def __checks(): raise MFCException("CMake is required to build MFC but couldn't be located on your system. Please ensure it installed and discoverable (e.g in your system's $PATH).") -def __run(): # pylint: disable=too-many-branches +def __run(): # Lazy import modules only when needed for the specific command cmd = ARG("command") if cmd == "test": - from mfc.test import test # pylint: disable=import-outside-toplevel + from mfc.test import test + test.test() elif cmd == "run": - from mfc.run import run # pylint: disable=import-outside-toplevel + from mfc.run import run + run.run() elif cmd == "build": - from mfc import build # pylint: disable=import-outside-toplevel + from mfc import build + build.build() elif cmd == "bench": - from mfc import bench # pylint: disable=import-outside-toplevel + from mfc import bench + bench.bench() elif cmd == "bench_diff": - from mfc import bench # pylint: disable=import-outside-toplevel + from mfc import bench + bench.diff() elif cmd == "count": - from mfc import count # pylint: disable=import-outside-toplevel + from mfc import count + count.count() elif cmd == "count_diff": - from mfc import count # pylint: disable=import-outside-toplevel + from mfc import count + count.count_diff() elif cmd == "clean": - from mfc import clean # pylint: disable=import-outside-toplevel + from mfc import clean + clean.clean() elif cmd == "packer": - from mfc.packer import packer # pylint: disable=import-outside-toplevel + from mfc.packer import packer + packer.packer() elif cmd == "validate": - from mfc import validate # pylint: disable=import-outside-toplevel + from mfc import validate + validate.validate() elif cmd == "new": - from mfc import init # pylint: disable=import-outside-toplevel + from mfc import init + init.init() elif cmd == "interactive": - from mfc.user_guide import interactive_mode # pylint: disable=import-outside-toplevel + from mfc.user_guide import interactive_mode + interactive_mode() elif cmd == "completion": - from mfc import completion # pylint: disable=import-outside-toplevel + from mfc import completion + completion.completion() elif cmd == "generate": - from mfc import generate # pylint: disable=import-outside-toplevel + from mfc import generate + generate.generate() elif cmd == "viz": - from mfc.viz import viz # pylint: disable=import-outside-toplevel + from mfc.viz import viz + viz.viz() elif cmd == "params": - from mfc import params_cmd # pylint: disable=import-outside-toplevel + from mfc import params_cmd + params_cmd.params() @@ -196,7 +211,8 @@ def __run(): # pylint: disable=too-many-branches lock.switch(state.MFCConfig.from_dict(state.gARG)) # Ensure IDE configuration is up to date (lightweight check) - from mfc.ide import ensure_vscode_settings # pylint: disable=import-outside-toplevel + from mfc.ide import ensure_vscode_settings + ensure_vscode_settings() # Auto-regenerate completion scripts if source files changed @@ -214,7 +230,7 @@ def __run(): # pylint: disable=too-many-branches [bold red]Error[/bold red]: {str(exc)} """) quit(signal.SIGTERM) - except KeyboardInterrupt as exc: + except KeyboardInterrupt: quit(signal.SIGTERM) except Exception as exc: cons.reset() diff --git a/toolchain/mfc/args.py b/toolchain/mfc/args.py index 80e6e0888c..9a3d82f83f 100644 --- a/toolchain/mfc/args.py +++ b/toolchain/mfc/args.py @@ -5,17 +5,21 @@ from the central CLI schema in cli/commands.py. """ +import os.path import re import sys -import os.path +from .cli.argparse_gen import generate_parser +from .cli.commands import COMMAND_ALIASES, MFC_CLI_SCHEMA from .common import MFCException from .state import MFCConfig -from .cli.commands import MFC_CLI_SCHEMA, COMMAND_ALIASES -from .cli.argparse_gen import generate_parser from .user_guide import ( - print_help, is_first_time_user, print_welcome, - print_command_help, print_topic_help, print_help_topics, + is_first_time_user, + print_command_help, + print_help, + print_help_topics, + print_topic_help, + print_welcome, ) @@ -27,7 +31,7 @@ def _get_command_from_args(args_list): """ # Skip the program name and any leading options (starting with '-') for token in args_list[1:]: - if not token.startswith('-'): + if not token.startswith("-"): return COMMAND_ALIASES.get(token, token) return None @@ -51,7 +55,6 @@ def _handle_enhanced_help(args_list): return None -# pylint: disable=too-many-locals, too-many-branches, too-many-statements def parse(config: MFCConfig): """Parse command line arguments using the CLI schema.""" # Handle enhanced help before argparse @@ -66,7 +69,7 @@ def parse(config: MFCConfig): sys.exit(0) try: - extra_index = sys.argv.index('--') + extra_index = sys.argv.index("--") except ValueError: extra_index = len(sys.argv) @@ -80,13 +83,13 @@ def custom_error(message): print_command_help(attempted_command, show_argparse=False) subparser.print_help() sys.stdout.flush() # Ensure help prints before error - sys.stderr.write(f'\n{subparser.prog}: error: {message}\n') + sys.stderr.write(f"\n{subparser.prog}: error: {message}\n") sys.exit(2) subparser.error = custom_error args: dict = vars(parser.parse_args(sys.argv[1:extra_index])) - args["--"] = sys.argv[extra_index + 1:] + args["--"] = sys.argv[extra_index + 1 :] # Handle --help at top level if args.get("help") and args["command"] is None: @@ -145,7 +148,7 @@ def custom_error(message): # "Slugify" the name of the job (only for batch jobs, not for new command) if args.get("name") is not None and isinstance(args["name"], str) and args["command"] != "new": - args["name"] = re.sub(r'[\W_]+', '-', args["name"]) + args["name"] = re.sub(r"[\W_]+", "-", args["name"]) # We need to check for some invalid combinations of arguments because of # the limitations of argparse. @@ -158,7 +161,8 @@ def custom_error(message): # Resolve test case defaults (deferred to avoid slow startup for non-test commands) if args["command"] == "test": - from .test.cases import list_cases # pylint: disable=import-outside-toplevel + from .test.cases import list_cases + test_cases = list_cases() if args.get("from") is None: args["from"] = test_cases[0].get_uuid() diff --git a/toolchain/mfc/bench.py b/toolchain/mfc/bench.py index 58b90e965b..a6a04e6f8e 100644 --- a/toolchain/mfc/bench.py +++ b/toolchain/mfc/bench.py @@ -1,13 +1,19 @@ -import os, sys, uuid, subprocess, dataclasses, typing, math, traceback, time +import dataclasses +import math +import os +import subprocess +import sys +import time +import traceback +import typing +import uuid import rich.table +from .build import DEFAULT_TARGETS, SIMULATION, get_targets +from .common import MFC_BENCH_FILEPATH, MFC_BUILD_DIR, MFCException, create_directory, file_dump_yaml, file_load_yaml, format_list_to_string, system from .printer import cons -from .state import ARG, CFG -from .build import get_targets, DEFAULT_TARGETS, SIMULATION -from .common import system, MFC_BENCH_FILEPATH, MFC_BUILD_DIR, format_list_to_string -from .common import file_load_yaml, file_dump_yaml, create_directory -from .common import MFCException +from .state import ARG, CFG @dataclasses.dataclass @@ -16,8 +22,8 @@ class BenchCase: path: str args: typing.List[str] -# pylint: disable=too-many-locals, too-many-branches, too-many-statements, too-many-nested-blocks -def bench(targets = None): + +def bench(targets=None): if targets is None: targets = ARG("targets") @@ -33,7 +39,7 @@ def bench(targets = None): try: cons.print() - CASES = [ BenchCase(**case) for case in file_load_yaml(MFC_BENCH_FILEPATH) ] + CASES = [BenchCase(**case) for case in file_load_yaml(MFC_BENCH_FILEPATH)] for case in CASES: case.args = case.args + ARG("--") @@ -44,10 +50,7 @@ def bench(targets = None): raise MFCException(f"Benchmark case file not found: {case.path}") results = { - "metadata": { - "invocation": sys.argv[1:], - "lock": dataclasses.asdict(CFG()) - }, + "metadata": {"invocation": sys.argv[1:], "lock": dataclasses.asdict(CFG())}, "cases": {}, } @@ -57,9 +60,9 @@ def bench(targets = None): for i, case in enumerate(CASES): summary_filepath = os.path.join(bench_dirpath, f"{case.slug}.yaml") - log_filepath = os.path.join(bench_dirpath, f"{case.slug}.out") + log_filepath = os.path.join(bench_dirpath, f"{case.slug}.out") - cons.print(f"{str(i+1).zfill(len(CASES) // 10 + 1)}/{len(CASES)}: {case.slug} @ [bold]{os.path.relpath(case.path)}[/bold]") + cons.print(f"{str(i + 1).zfill(len(CASES) // 10 + 1)}/{len(CASES)}: {case.slug} @ [bold]{os.path.relpath(case.path)}[/bold]") cons.indent() cons.print() cons.print(f"> Log: [bold]{os.path.relpath(log_filepath)}[/bold]") @@ -70,20 +73,17 @@ def bench(targets = None): try: with open(log_filepath, "w") as log_file: result = system( - ["./mfc.sh", "run", case.path] + - ["--targets"] + [t.name for t in targets] + - ["--output-summary", summary_filepath] + - case.args + - ["--", "--gbpp", str(ARG('mem'))], + ["./mfc.sh", "run", case.path] + ["--targets"] + [t.name for t in targets] + ["--output-summary", summary_filepath] + case.args + ["--", "--gbpp", str(ARG("mem"))], stdout=log_file, - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT, + ) # Check return code (handle CompletedProcess or int defensively) rc = result.returncode if hasattr(result, "returncode") else result if rc != 0: if attempt < max_attempts: cons.print(f"[bold yellow]WARNING[/bold yellow]: Case {case.slug} failed with exit code {rc} (attempt {attempt}/{max_attempts})") - cons.print(f"Retrying in 5s...") + cons.print("Retrying in 5s...") time.sleep(5) continue cons.print(f"[bold red]ERROR[/bold red]: Case {case.slug} failed with exit code {rc}") @@ -95,7 +95,7 @@ def bench(targets = None): if not os.path.exists(summary_filepath): if attempt < max_attempts: cons.print(f"[bold yellow]WARNING[/bold yellow]: Summary file not created for {case.slug} (attempt {attempt}/{max_attempts})") - cons.print(f"Retrying in 5s...") + cons.print("Retrying in 5s...") time.sleep(5) continue cons.print(f"[bold red]ERROR[/bold red]: Summary file not created for {case.slug}") @@ -130,7 +130,7 @@ def bench(targets = None): # Add to results results["cases"][case.slug] = { - "description": dataclasses.asdict(case), + "description": dataclasses.asdict(case), "output_summary": summary, } cons.print(f"[bold green]✓[/bold green] Case {case.slug} completed successfully") @@ -139,7 +139,7 @@ def bench(targets = None): except Exception as e: if attempt < max_attempts: cons.print(f"[bold yellow]WARNING[/bold yellow]: Unexpected error running {case.slug} (attempt {attempt}/{max_attempts}): {e}") - cons.print(f"Retrying in 5s...") + cons.print("Retrying in 5s...") time.sleep(5) continue cons.print(f"[bold red]ERROR[/bold red]: Unexpected error running {case.slug}: {e}") @@ -168,36 +168,41 @@ def bench(targets = None): # TODO: This function is too long and not nicely written at all. Someone should # refactor it... -# pylint: disable=too-many-branches def diff(): lhs, rhs = file_load_yaml(ARG("lhs")), file_load_yaml(ARG("rhs")) - cons.print(f"[bold]Comparing Benchmarks: Speedups from [magenta]{os.path.relpath(ARG('lhs'))}[/magenta] to [magenta]{os.path.relpath(ARG('rhs'))}[/magenta] are displayed below. Thus, numbers > 1 represent increases in performance.[/bold]") + lhs_path = os.path.relpath(ARG("lhs")) + rhs_path = os.path.relpath(ARG("rhs")) + cons.print( + f"[bold]Comparing Benchmarks: Speedups from [magenta]{lhs_path}[/magenta] to [magenta]{rhs_path}[/magenta] are displayed below. Thus, numbers > 1 represent increases in performance.[/bold]" + ) if lhs["metadata"] != rhs["metadata"]: - _lock_to_str = lambda lock: ' '.join([f"{k}={v}" for k, v in lock.items()]) + + def _lock_to_str(lock): + return " ".join([f"{k}={v}" for k, v in lock.items()]) cons.print(f"""\ [bold yellow]Warning[/bold yellow]: Metadata in lhs and rhs are not equal. This could mean that the benchmarks are not comparable (e.g. one was run on CPUs and the other on GPUs). lhs: - * Invocation: [magenta]{' '.join(lhs['metadata']['invocation'])}[/magenta] - * Modes: {_lock_to_str(lhs['metadata']['lock'])} + * Invocation: [magenta]{" ".join(lhs["metadata"]["invocation"])}[/magenta] + * Modes: {_lock_to_str(lhs["metadata"]["lock"])} rhs: - * Invocation: {' '.join(rhs['metadata']['invocation'])} - * Modes: [magenta]{_lock_to_str(rhs['metadata']['lock'])}[/magenta] + * Invocation: {" ".join(rhs["metadata"]["invocation"])} + * Modes: [magenta]{_lock_to_str(rhs["metadata"]["lock"])}[/magenta] """) slugs = set(lhs["cases"].keys()) & set(rhs["cases"].keys()) if len(slugs) not in [len(lhs["cases"]), len(rhs["cases"])]: cons.print(f"""\ [bold yellow]Warning[/bold yellow]: Cases in lhs and rhs are not equal. - * rhs cases: {', '.join(set(rhs['cases'].keys()) - slugs)}. - * lhs cases: {', '.join(set(lhs['cases'].keys()) - slugs)}. + * rhs cases: {", ".join(set(rhs["cases"].keys()) - slugs)}. + * lhs cases: {", ".join(set(lhs["cases"].keys()) - slugs)}. Using intersection: {slugs} with {len(slugs)} elements. """) table = rich.table.Table(show_header=True, box=rich.table.box.SIMPLE) - table.add_column("[bold]Case[/bold]", justify="left") + table.add_column("[bold]Case[/bold]", justify="left") table.add_column("[bold]Pre Process[/bold]", justify="right") table.add_column("[bold]Simulation[/bold]", justify="right") table.add_column("[bold]Post Process[/bold]", justify="right") @@ -205,12 +210,13 @@ def diff(): err = 0 for slug in slugs: lhs_summary, rhs_summary = lhs["cases"][slug]["output_summary"], rhs["cases"][slug]["output_summary"] - speedups = ['N/A', 'N/A', 'N/A'] + speedups = ["N/A", "N/A", "N/A"] for i, target in enumerate(sorted(DEFAULT_TARGETS, key=lambda t: t.runOrder)): if (target.name not in lhs_summary) or (target.name not in rhs_summary): cons.print(f"{target.name} not present in lhs_summary or rhs_summary - Case: {slug}") - err = 1; continue + err = 1 + continue if not math.isfinite(lhs_summary[target.name]["exec"]) or not math.isfinite(rhs_summary[target.name]["exec"]): err = 1 @@ -230,10 +236,7 @@ def diff(): if grind_time_value < 0.95: cons.print(f"[bold yellow]Warning[/bold yellow]: Grind time speedup for {target.name} below threshold (<0.95) - Case: {slug}") except Exception as e: - cons.print( - f"[bold red]ERROR[/bold red]: Failed to compute speedup for {target.name} in {slug}: {e}\n" - f"{traceback.format_exc()}" - ) + cons.print(f"[bold red]ERROR[/bold red]: Failed to compute speedup for {target.name} in {slug}: {e}\n{traceback.format_exc()}") err = 1 table.add_row(f"[magenta]{slug}[/magenta]", *speedups) diff --git a/toolchain/mfc/build.py b/toolchain/mfc/build.py index 08ff6d7510..d6daf97bb6 100644 --- a/toolchain/mfc/build.py +++ b/toolchain/mfc/build.py @@ -1,27 +1,32 @@ -import os, typing, hashlib, dataclasses, subprocess, re, time, sys, threading, queue +import dataclasses +import hashlib +import os +import queue +import re +import subprocess +import sys +import threading +import time +import typing from rich.panel import Panel -from rich.text import Text -from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TaskProgressColumn +from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn +from rich.text import Text -from .case import Case +from .case import Case +from .common import MFCException, create_directory, debug, delete_directory, format_list_to_string, system from .printer import cons -from .common import MFCException, system, delete_directory, create_directory, \ - format_list_to_string, debug -from .state import ARG, CFG -from .run import input -from .state import gpuConfigOptions +from .run import input +from .state import ARG, CFG, gpuConfigOptions from .user_guide import Tips - # Regex to parse build progress # Ninja format: [42/156] Building Fortran object ... -_NINJA_PROGRESS_RE = re.compile(r'^\[(\d+)/(\d+)\]\s+(.*)$') +_NINJA_PROGRESS_RE = re.compile(r"^\[(\d+)/(\d+)\]\s+(.*)$") # Make format: [ 16%] Building Fortran object ... or [100%] Linking ... -_MAKE_PROGRESS_RE = re.compile(r'^\[\s*(\d+)%\]\s+(.*)$') +_MAKE_PROGRESS_RE = re.compile(r"^\[\s*(\d+)%\]\s+(.*)$") -# pylint: disable=too-many-locals,too-many-branches,too-many-statements,too-many-nested-blocks def _run_build_with_progress(command: typing.List[str], target_name: str, streaming: bool = False) -> subprocess.CompletedProcess: """ Run a build command with a progress bar that parses ninja output. @@ -49,19 +54,19 @@ def _run_build_with_progress(command: typing.List[str], target_name: str, stream if streaming: # Streaming mode (-v): merge stderr into stdout to avoid pipe deadlock - process = subprocess.Popen( # pylint: disable=consider-using-with + process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 # Line buffered + bufsize=1, # Line buffered ) cons.print(f" [bold blue]Building[/bold blue] [magenta]{target_name}[/magenta] [dim](-v)[/dim]...") start_time = time.time() # Read merged stdout+stderr and print matching lines - for line in iter(process.stdout.readline, ''): + for line in iter(process.stdout.readline, ""): all_stdout.append(line) stripped = line.strip() @@ -74,7 +79,7 @@ def _run_build_with_progress(command: typing.List[str], target_name: str, stream # Extract filename from action parts = action.split() if len(parts) >= 3: - filename = os.path.basename(parts[-1]).replace('.o', '').replace('.obj', '') + filename = os.path.basename(parts[-1]).replace(".o", "").replace(".obj", "") if len(filename) > 40: filename = filename[:37] + "..." cons.print(f" [dim][{completed}/{total}][/dim] {filename}") @@ -90,7 +95,7 @@ def _run_build_with_progress(command: typing.List[str], target_name: str, stream if len(parts) >= 3: # Get the last part which is usually the file path obj_path = parts[-1] - filename = os.path.basename(obj_path).replace('.o', '').replace('.obj', '') + filename = os.path.basename(obj_path).replace(".o", "").replace(".obj", "") if len(filename) > 40: filename = filename[:37] + "..." cons.print(f" [dim][{percent:>3}%][/dim] {filename}") @@ -101,15 +106,15 @@ def _run_build_with_progress(command: typing.List[str], target_name: str, stream if elapsed > 5: cons.print(f" [dim](build took {elapsed:.1f}s)[/dim]") - return subprocess.CompletedProcess(cmd, process.returncode, ''.join(all_stdout), '') + return subprocess.CompletedProcess(cmd, process.returncode, "".join(all_stdout), "") # Start the process for non-streaming modes - process = subprocess.Popen( # pylint: disable=consider-using-with + process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - bufsize=1 # Line buffered + bufsize=1, # Line buffered ) if not is_tty: @@ -142,24 +147,19 @@ def _run_build_with_progress(command: typing.List[str], target_name: str, stream refresh_per_second=4, ) as progress: # Start with indeterminate progress (total=None shows spinner behavior) - task = progress.add_task( - "build", - total=None, - target=target_name, - current_file="" - ) + task = progress.add_task("build", total=None, target=target_name, current_file="") # Use threads to read stdout and stderr concurrently stdout_queue = queue.Queue() stderr_queue = queue.Queue() def read_stdout(): - for line in iter(process.stdout.readline, ''): + for line in iter(process.stdout.readline, ""): stdout_queue.put(line) stdout_queue.put(None) # Signal EOF def read_stderr(): - for line in iter(process.stderr.readline, ''): + for line in iter(process.stderr.readline, ""): stderr_queue.put(line) stderr_queue.put(None) # Signal EOF @@ -193,7 +193,7 @@ def read_stderr(): parts = action.split() if len(parts) >= 3: obj_path = parts[-1] - current_file = os.path.basename(obj_path).replace('.o', '').replace('.obj', '') + current_file = os.path.basename(obj_path).replace(".o", "").replace(".obj", "") if len(current_file) > 30: current_file = current_file[:27] + "..." @@ -201,11 +201,7 @@ def read_stderr(): progress_detected = True progress.update(task, total=total_files) - progress.update( - task, - completed=completed_files, - current_file=current_file - ) + progress.update(task, completed=completed_files, current_file=current_file) else: # Try make format: [ 16%] Action make_match = _MAKE_PROGRESS_RE.match(stripped) @@ -218,7 +214,7 @@ def read_stderr(): parts = action.split() if len(parts) >= 3: obj_path = parts[-1] - current_file = os.path.basename(obj_path).replace('.o', '').replace('.obj', '') + current_file = os.path.basename(obj_path).replace(".o", "").replace(".obj", "") if len(current_file) > 30: current_file = current_file[:27] + "..." @@ -227,11 +223,7 @@ def read_stderr(): # Make uses percentage, so set total to 100 progress.update(task, total=100) - progress.update( - task, - completed=percent, - current_file=current_file - ) + progress.update(task, completed=percent, current_file=current_file) except queue.Empty: pass @@ -256,12 +248,7 @@ def read_stderr(): stdout_thread.join(timeout=1) stderr_thread.join(timeout=1) - return subprocess.CompletedProcess( - cmd, - process.returncode, - ''.join(all_stdout), - ''.join(all_stderr) - ) + return subprocess.CompletedProcess(cmd, process.returncode, "".join(all_stdout), "".join(all_stderr)) def _show_build_error(result: subprocess.CompletedProcess, stage: str): @@ -271,20 +258,21 @@ def _show_build_error(result: subprocess.CompletedProcess, stage: str): # 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_text = result.stdout if isinstance(result.stdout, str) else result.stdout.decode("utf-8", errors="replace") stdout_text = stdout_text.strip() if stdout_text: cons.raw.print(Panel(Text(stdout_text), 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_text = result.stderr if isinstance(result.stderr, str) else result.stderr.decode("utf-8", errors="replace") stderr_text = stderr_text.strip() if stderr_text: cons.raw.print(Panel(Text(stderr_text), title="Errors", border_style="red")) cons.print() + @dataclasses.dataclass class MFCTarget: @dataclasses.dataclass @@ -294,23 +282,23 @@ class Dependencies: gpu: typing.List def compute(self) -> typing.Set: - r = self.all[:] + r = self.all[:] r += self.gpu[:] if (ARG("gpu") != gpuConfigOptions.NONE.value) else self.cpu[:] return r - name: str # Name of the target - flags: typing.List[str] # Extra flags to pass to CMakeMFCTarget - isDependency: bool # Is it a dependency of an MFC target? - isDefault: bool # Should it be built by default? (unspecified -t | --targets) - isRequired: bool # Should it always be built? (no matter what -t | --targets is) - requires: Dependencies # Build dependencies of the target - runOrder: int # For MFC Targets: Order in which targets should logically run + name: str # Name of the target + flags: typing.List[str] # Extra flags to pass to CMakeMFCTarget + isDependency: bool # Is it a dependency of an MFC target? + isDefault: bool # Should it be built by default? (unspecified -t | --targets) + isRequired: bool # Should it always be built? (no matter what -t | --targets is) + requires: Dependencies # Build dependencies of the target + runOrder: int # For MFC Targets: Order in which targets should logically run def __hash__(self) -> int: return hash(self.name) - def get_slug(self, case: Case ) -> str: + def get_slug(self, case: Case) -> str: if self.isDependency: return self.name @@ -319,44 +307,44 @@ def get_slug(self, case: Case ) -> str: m.update(CFG().make_slug().encode()) m.update(case.get_fpp(self, False).encode()) - if case.params.get('chemistry', 'F') == 'T': + if case.params.get("chemistry", "F") == "T": m.update(case.get_cantera_solution().name.encode()) return m.hexdigest()[:10] # Get path to directory that will store the build files - def get_staging_dirpath(self, case: Case ) -> str: - return os.sep.join([os.getcwd(), "build", "staging", self.get_slug(case) ]) + def get_staging_dirpath(self, case: Case) -> str: + return os.sep.join([os.getcwd(), "build", "staging", self.get_slug(case)]) # Get the directory that contains the target's CMakeLists.txt def get_cmake_dirpath(self) -> str: # The CMakeLists.txt file is located: # * Regular: /CMakelists.txt # * Dependency: /toolchain/dependencies/CMakelists.txt - return os.sep.join([ - os.getcwd(), - os.sep.join(["toolchain", "dependencies"]) if self.isDependency else "", - ]) + return os.sep.join( + [ + os.getcwd(), + os.sep.join(["toolchain", "dependencies"]) if self.isDependency else "", + ] + ) - def get_install_dirpath(self, case: Case ) -> str: + def get_install_dirpath(self, case: Case) -> str: # The install directory is located /build/install/ return os.sep.join([os.getcwd(), "build", "install", self.get_slug(case)]) def get_home_dirpath(self) -> str: return os.sep.join([os.getcwd()]) - def get_install_binpath(self, case: Case ) -> str: + def get_install_binpath(self, case: Case) -> str: # /install//bin/ return os.sep.join([self.get_install_dirpath(case), "bin", self.name]) - def is_configured(self, case: Case ) -> bool: + def is_configured(self, case: Case) -> bool: # We assume that if the CMakeCache.txt file exists, then the target is # configured. (this isn't perfect, but it's good enough for now) - return os.path.isfile( - os.sep.join([self.get_staging_dirpath(case), "CMakeCache.txt"]) - ) + return os.path.isfile(os.sep.join([self.get_staging_dirpath(case), "CMakeCache.txt"])) - def get_configuration_txt(self, case: Case ) -> typing.Optional[dict]: + def get_configuration_txt(self, case: Case) -> typing.Optional[dict]: if not self.is_configured(case): return None @@ -377,13 +365,11 @@ def is_buildable(self) -> bool: return True def configure(self, case: Case): - build_dirpath = self.get_staging_dirpath(case) - cmake_dirpath = self.get_cmake_dirpath() + build_dirpath = self.get_staging_dirpath(case) + cmake_dirpath = self.get_cmake_dirpath() install_dirpath = self.get_install_dirpath(case) - install_prefixes = ';'.join([ - t.get_install_dirpath(case) for t in self.requires.compute() - ]) + install_prefixes = ";".join([t.get_install_dirpath(case) for t in self.requires.compute()]) flags: list = self.flags.copy() + [ # Disable CMake warnings intended for developers (us). @@ -415,20 +401,20 @@ def configure(self, case: Case): # See: https://cmake.org/cmake/help/latest/command/install.html. f"-DCMAKE_INSTALL_PREFIX={install_dirpath}", f"-DMFC_SINGLE_PRECISION={'ON' if (ARG('single') or ARG('mixed')) else 'OFF'}", - f"-DMFC_MIXED_PRECISION={'ON' if ARG('mixed') else 'OFF'}" + f"-DMFC_MIXED_PRECISION={'ON' if ARG('mixed') else 'OFF'}", ] # Verbosity level 3 (-vvv): add cmake debug flags if ARG("verbose") >= 3: - flags.append('--debug-find') + flags.append("--debug-find") if not self.isDependency: - flags.append(f"-DMFC_MPI={ 'ON' if ARG('mpi') else 'OFF'}") + flags.append(f"-DMFC_MPI={'ON' if ARG('mpi') else 'OFF'}") # flags.append(f"-DMFC_OpenACC={'ON' if ARG('acc') else 'OFF'}") # flags.append(f"-DMFC_OpenMP={'ON' if ARG('mp') else 'OFF'}") flags.append(f"-DMFC_OpenACC={'ON' if (ARG('gpu') == gpuConfigOptions.ACC.value) else 'OFF'}") flags.append(f"-DMFC_OpenMP={'ON' if (ARG('gpu') == gpuConfigOptions.MP.value) else 'OFF'}") - flags.append(f"-DMFC_GCov={ 'ON' if ARG('gcov') else 'OFF'}") + flags.append(f"-DMFC_GCov={'ON' if ARG('gcov') else 'OFF'}") flags.append(f"-DMFC_Unified={'ON' if ARG('unified') else 'OFF'}") flags.append(f"-DMFC_Fastmath={'ON' if ARG('fastmath') else 'OFF'}") @@ -442,7 +428,7 @@ def configure(self, case: Case): debug(f"Configuring {self.name} in {build_dirpath}") debug(f"CMake flags: {' '.join(flags)}") - verbosity = ARG('verbose') + verbosity = ARG("verbose") if verbosity >= 2: # -vv or higher: show raw cmake output level_str = "vv" + "v" * (verbosity - 2) if verbosity > 2 else "vv" @@ -469,12 +455,9 @@ def configure(self, case: Case): def build(self, case: input.MFCInputFile): case.generate_fpp(self) - command = ["cmake", "--build", self.get_staging_dirpath(case), - "--target", self.name, - "--parallel", ARG("jobs"), - "--config", 'Debug' if ARG('debug') else 'Release'] + command = ["cmake", "--build", self.get_staging_dirpath(case), "--target", self.name, "--parallel", ARG("jobs"), "--config", "Debug" if ARG("debug") else "Release"] - verbosity = ARG('verbose') + verbosity = ARG("verbose") # -vv or higher: add cmake --verbose flag for full compiler commands if verbosity >= 2: command.append("--verbose") @@ -522,25 +505,27 @@ def install(self, case: input.MFCInputFile): cons.print(f" [bold green]✓[/bold green] Installed [magenta]{self.name}[/magenta]") cons.print(no_indent=True) + # name flags isDep isDef isReq dependencies run order -FFTW = MFCTarget('fftw', ['-DMFC_FFTW=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) -HDF5 = MFCTarget('hdf5', ['-DMFC_HDF5=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) -SILO = MFCTarget('silo', ['-DMFC_SILO=ON'], True, False, False, MFCTarget.Dependencies([HDF5], [], []), -1) -LAPACK = MFCTarget('lapack', ['-DMFC_LAPACK=ON'], True, False, False, MFCTarget.Dependencies([],[],[]), -1) -HIPFORT = MFCTarget('hipfort', ['-DMFC_HIPFORT=ON'], True, False, False, MFCTarget.Dependencies([], [], []), -1) -PRE_PROCESS = MFCTarget('pre_process', ['-DMFC_PRE_PROCESS=ON'], False, True, False, MFCTarget.Dependencies([], [], []), 0) -SIMULATION = MFCTarget('simulation', ['-DMFC_SIMULATION=ON'], False, True, False, MFCTarget.Dependencies([], [FFTW], [HIPFORT]), 1) -POST_PROCESS = MFCTarget('post_process', ['-DMFC_POST_PROCESS=ON'], False, True, False, MFCTarget.Dependencies([FFTW, HDF5, SILO, LAPACK], [], []), 2) -SYSCHECK = MFCTarget('syscheck', ['-DMFC_SYSCHECK=ON'], False, False, True, MFCTarget.Dependencies([], [], [HIPFORT]), -1) -DOCUMENTATION = MFCTarget('documentation', ['-DMFC_DOCUMENTATION=ON'], False, False, False, MFCTarget.Dependencies([], [], []), -1) +FFTW = MFCTarget("fftw", ["-DMFC_FFTW=ON"], True, False, False, MFCTarget.Dependencies([], [], []), -1) +HDF5 = MFCTarget("hdf5", ["-DMFC_HDF5=ON"], True, False, False, MFCTarget.Dependencies([], [], []), -1) +SILO = MFCTarget("silo", ["-DMFC_SILO=ON"], True, False, False, MFCTarget.Dependencies([HDF5], [], []), -1) +LAPACK = MFCTarget("lapack", ["-DMFC_LAPACK=ON"], True, False, False, MFCTarget.Dependencies([], [], []), -1) +HIPFORT = MFCTarget("hipfort", ["-DMFC_HIPFORT=ON"], True, False, False, MFCTarget.Dependencies([], [], []), -1) +PRE_PROCESS = MFCTarget("pre_process", ["-DMFC_PRE_PROCESS=ON"], False, True, False, MFCTarget.Dependencies([], [], []), 0) +SIMULATION = MFCTarget("simulation", ["-DMFC_SIMULATION=ON"], False, True, False, MFCTarget.Dependencies([], [FFTW], [HIPFORT]), 1) +POST_PROCESS = MFCTarget("post_process", ["-DMFC_POST_PROCESS=ON"], False, True, False, MFCTarget.Dependencies([FFTW, HDF5, SILO, LAPACK], [], []), 2) +SYSCHECK = MFCTarget("syscheck", ["-DMFC_SYSCHECK=ON"], False, False, True, MFCTarget.Dependencies([], [], [HIPFORT]), -1) +DOCUMENTATION = MFCTarget("documentation", ["-DMFC_DOCUMENTATION=ON"], False, False, False, MFCTarget.Dependencies([], [], []), -1) + +TARGETS = {FFTW, HDF5, SILO, LAPACK, HIPFORT, PRE_PROCESS, SIMULATION, POST_PROCESS, SYSCHECK, DOCUMENTATION} -TARGETS = { FFTW, HDF5, SILO, LAPACK, HIPFORT, PRE_PROCESS, SIMULATION, POST_PROCESS, SYSCHECK, DOCUMENTATION } +DEFAULT_TARGETS = {target for target in TARGETS if target.isDefault} +REQUIRED_TARGETS = {target for target in TARGETS if target.isRequired} +DEPENDENCY_TARGETS = {target for target in TARGETS if target.isDependency} -DEFAULT_TARGETS = { target for target in TARGETS if target.isDefault } -REQUIRED_TARGETS = { target for target in TARGETS if target.isRequired } -DEPENDENCY_TARGETS = { target for target in TARGETS if target.isDependency } +TARGET_MAP = {target.name: target for target in TARGETS} -TARGET_MAP = { target.name: target for target in TARGETS } def get_target(target: typing.Union[str, MFCTarget]) -> MFCTarget: if isinstance(target, MFCTarget): @@ -553,7 +538,7 @@ def get_target(target: typing.Union[str, MFCTarget]) -> MFCTarget: def get_targets(targets: typing.List[typing.Union[str, MFCTarget]]) -> typing.List[MFCTarget]: - return [ get_target(t) for t in targets ] + return [get_target(t) for t in targets] def __build_target(target: typing.Union[MFCTarget, str], case: input.MFCInputFile, history: typing.Set[str] = None): @@ -584,32 +569,29 @@ def __build_target(target: typing.Union[MFCTarget, str], case: input.MFCInputFil def get_configured_targets(case: input.MFCInputFile) -> typing.List[MFCTarget]: - return [ target for target in TARGETS if target.is_configured(case) ] + return [target for target in TARGETS if target.is_configured(case)] def __generate_header(case: input.MFCInputFile, targets: typing.List): - feature_flags = [ - 'Build', - format_list_to_string([ t.name for t in get_targets(targets) ], 'magenta') - ] + feature_flags = ["Build", format_list_to_string([t.name for t in get_targets(targets)], "magenta")] if ARG("case_optimization"): feature_flags.append(f"Case Optimized: [magenta]{ARG('input')}[/magenta]") - if case.params.get('chemistry', 'F') == 'T': + if case.params.get("chemistry", "F") == "T": feature_flags.append(f"Chemistry: [magenta]{case.get_cantera_solution().source}[/magenta]") return f"[bold]{' | '.join(feature_flags or ['Generic'])}[/bold]" -def build(targets = None, case: input.MFCInputFile = None, history: typing.Set[str] = None): +def build(targets=None, case: input.MFCInputFile = None, history: typing.Set[str] = None): if history is None: history = set() if isinstance(targets, (MFCTarget, str)): - targets = [ targets ] + targets = [targets] if targets is None: targets = ARG("targets") targets = get_targets(list(REQUIRED_TARGETS) + targets) - case = case or input.load(ARG("input"), ARG("--"), {}) + case = case or input.load(ARG("input"), ARG("--"), {}) case.validate_params() if len(history) == 0: diff --git a/toolchain/mfc/case.py b/toolchain/mfc/case.py index 445a111c57..4388ac97c7 100644 --- a/toolchain/mfc/case.py +++ b/toolchain/mfc/case.py @@ -1,28 +1,40 @@ -# pylint: disable=import-outside-toplevel -import re, json, math, copy, dataclasses, difflib, fastjsonschema +import copy +import dataclasses +import difflib +import json +import math +import re + +import fastjsonschema from . import common from .printer import cons - +from .run import case_dicts from .state import ARG -from .run import case_dicts def _suggest_similar_params(unknown_key: str, valid_keys: list, n: int = 3) -> list: """Find similar parameter names for typo suggestions.""" return difflib.get_close_matches(unknown_key, valid_keys, n=n, cutoff=0.6) + QPVF_IDX_VARS = { - 'alpha_rho': 'contxb', 'vel' : 'momxb', 'pres': 'E_idx', - 'alpha': 'advxb', 'tau_e': 'stress_idx%beg', 'Y': 'chemxb', - 'cf_val': 'c_idx', 'Bx': 'B_idx%beg', 'By': 'B_idx%end-1', 'Bz': 'B_idx%end', + "alpha_rho": "contxb", + "vel": "momxb", + "pres": "E_idx", + "alpha": "advxb", + "tau_e": "stress_idx%beg", + "Y": "chemxb", + "cf_val": "c_idx", + "Bx": "B_idx%beg", + "By": "B_idx%end-1", + "Bz": "B_idx%end", } -MIBM_ANALYTIC_VARS = [ - 'vel(1)', 'vel(2)', 'vel(3)', 'angular_vel(1)', 'angular_vel(2)', 'angular_vel(3)' -] +MIBM_ANALYTIC_VARS = ["vel(1)", "vel(2)", "vel(3)", "angular_vel(1)", "angular_vel(2)", "angular_vel(3)"] # "B_idx%end - 1" not "B_idx%beg + 1" must be used because 1D does not have Bx + @dataclasses.dataclass(init=False) class Case: params: dict @@ -36,14 +48,15 @@ def get_parameters(self) -> dict: def get_cell_count(self) -> int: return math.prod([max(1, int(self.params.get(dir, 0))) for dir in ["m", "n", "p"]]) - def has_parameter(self, key: str)-> bool: + def has_parameter(self, key: str) -> bool: return key in self.params.keys() def gen_json_dict_str(self) -> str: return json.dumps(self.params, indent=4) def get_inp(self, _target) -> str: - from . import build # pylint: disable=import-outside-toplevel + from . import build + target = build.get_target(_target) cons.print(f"Generating [magenta]{target.name}.inp[/magenta]:") @@ -74,17 +87,17 @@ def get_inp(self, _target) -> str: hint = f" Did you mean: {', '.join(suggestions)}?" if suggestions else "" raise common.MFCException(f"Unknown parameter '{key}'.{hint}") - cons.print(f"[yellow]INFO:[/yellow] Forwarded {len(self.params)-len(ignored)}/{len(self.params)} parameters.") + cons.print(f"[yellow]INFO:[/yellow] Forwarded {len(self.params) - len(ignored)}/{len(self.params)} parameters.") cons.unindent() return f"&user_inputs\n{dict_str}&end/\n" def validate_params(self, origin_txt: str = None): - '''Validates parameters read from case file: + """Validates parameters read from case file: 1. Type checking via JSON schema 2. Constraint validation (valid values, ranges) 3. Dependency checking (required/recommended params) - ''' + """ # Type checking try: case_dicts.get_validator()(self.params) @@ -115,37 +128,36 @@ def __get_ndims(self) -> int: return 1 + min(int(self.params.get("n", 0)), 1) + min(int(self.params.get("p", 0)), 1) def __is_ic_analytical(self, key: str, val: str) -> bool: - '''Is this initial condition analytical? - More precisely, is this an arbitrary expression or a string representing a number?''' + """Is this initial condition analytical? + More precisely, is this an arbitrary expression or a string representing a number?""" if common.is_number(val) or not isinstance(val, str): return False for array in QPVF_IDX_VARS: - if re.match(fr'^patch_icpp\([0-9]+\)%{array}', key): + if re.match(rf"^patch_icpp\([0-9]+\)%{array}", key): return True return False def __is_mib_analytical(self, key: str, val: str) -> bool: - '''Is this initial condition analytical? - More precisely, is this an arbitrary expression or a string representing a number?''' + """Is this initial condition analytical? + More precisely, is this an arbitrary expression or a string representing a number?""" if common.is_number(val) or not isinstance(val, str): return False for variable in MIBM_ANALYTIC_VARS: - if re.match(fr'^patch_ib\([0-9]+\)%{re.escape(variable)}', key): + if re.match(rf"^patch_ib\([0-9]+\)%{re.escape(variable)}", key): return True return False - # pylint: disable=too-many-locals def __get_analytic_ic_fpp(self, print: bool) -> str: # generates the content of an FFP file that will hold the functions for # some initial condition DATA = { - 1: {'ptypes': [1, 15, 16], 'sf_idx': 'i, 0, 0'}, - 2: {'ptypes': [2, 3, 4, 5, 6, 7, 13, 17, 18, 21], 'sf_idx': 'i, j, 0'}, - 3: {'ptypes': [8, 9, 10, 11, 12, 14, 19, 21], 'sf_idx': 'i, j, k'} + 1: {"ptypes": [1, 15, 16], "sf_idx": "i, 0, 0"}, + 2: {"ptypes": [2, 3, 4, 5, 6, 7, 13, 17, 18, 21], "sf_idx": "i, j, 0"}, + 3: {"ptypes": [8, 9, 10, 11, 12, 14, 19, 21], "sf_idx": "i, j, k"}, }[self.__get_ndims()] patches = {} @@ -156,7 +168,7 @@ def __get_analytic_ic_fpp(self, print: bool) -> str: if not self.__is_ic_analytical(key, val): continue - patch_id = re.search(r'[0-9]+', key).group(0) + patch_id = re.search(r"[0-9]+", key).group(0) if patch_id not in patches: patches[patch_id] = [] @@ -170,22 +182,28 @@ def __get_analytic_ic_fpp(self, print: bool) -> str: for pid, items in patches.items(): ptype = self.params[f"patch_icpp({pid})%geometry"] - if ptype not in DATA['ptypes']: + if ptype not in DATA["ptypes"]: raise common.MFCException(f"Patch #{pid} of type {ptype} cannot be analytically defined.") # function that defines how we will replace variable names with # values from the case file def rhs_replace(match): return { - 'x': 'x_cc(i)', 'y': 'y_cc(j)', 'z': 'z_cc(k)', - - 'xc': f'patch_icpp({pid})%x_centroid', 'yc': f'patch_icpp({pid})%y_centroid', 'zc': f'patch_icpp({pid})%z_centroid', - 'lx': f'patch_icpp({pid})%length_x', 'ly': f'patch_icpp({pid})%length_y', 'lz': f'patch_icpp({pid})%length_z', - - 'r': f'patch_icpp({pid})%radius', 'eps': f'patch_icpp({pid})%epsilon', 'beta': f'patch_icpp({pid})%beta', - 'tau_e': f'patch_icpp({pid})%tau_e', 'radii': f'patch_icpp({pid})%radii', - - 'e' : f'{math.e}', + "x": "x_cc(i)", + "y": "y_cc(j)", + "z": "z_cc(k)", + "xc": f"patch_icpp({pid})%x_centroid", + "yc": f"patch_icpp({pid})%y_centroid", + "zc": f"patch_icpp({pid})%z_centroid", + "lx": f"patch_icpp({pid})%length_x", + "ly": f"patch_icpp({pid})%length_y", + "lz": f"patch_icpp({pid})%length_z", + "r": f"patch_icpp({pid})%radius", + "eps": f"patch_icpp({pid})%epsilon", + "beta": f"patch_icpp({pid})%beta", + "tau_e": f"patch_icpp({pid})%tau_e", + "radii": f"patch_icpp({pid})%radii", + "e": f"{math.e}", }.get(match.group(), match.group()) lines = [] @@ -195,11 +213,11 @@ def rhs_replace(match): if print: cons.print(f"* Codegen: {attribute} = {expr}") - varname = re.findall(r"[a-zA-Z][a-zA-Z0-9_]*", attribute)[1] + varname = re.findall(r"[a-zA-Z][a-zA-Z0-9_]*", attribute)[1] qpvf_idx = QPVF_IDX_VARS[varname][:] if len(re.findall(r"[0-9]+", attribute)) == 2: - idx = int(re.findall(r'[0-9]+', attribute)[1]) - 1 + idx = int(re.findall(r"[0-9]+", attribute)[1]) - 1 qpvf_idx = f"{qpvf_idx} + {idx}" lhs = f"q_prim_vf({qpvf_idx})%sf({DATA['sf_idx']})" @@ -212,7 +230,7 @@ def rhs_replace(match): # new lines as a fully concatenated string with fortran syntax srcs.append(f"""\ if (patch_id == {pid}) then -{f'{chr(10)}'.join(lines)} +{f"{chr(10)}".join(lines)} end if\ """) @@ -222,7 +240,7 @@ def rhs_replace(match): ! expressions that are evaluated at runtime from the input file. #:def analytical() -{f'{chr(10)}{chr(10)}'.join(srcs)} +{f"{chr(10)}{chr(10)}".join(srcs)} #:enddef """ return content @@ -237,7 +255,7 @@ def __get_analytic_mib_fpp(self, print: bool) -> str: if not self.__is_mib_analytical(key, val): continue - patch_id = re.search(r'[0-9]+', key).group(0) + patch_id = re.search(r"[0-9]+", key).group(0) if patch_id not in ib_patches: ib_patches[patch_id] = [] @@ -249,17 +267,16 @@ def __get_analytic_mib_fpp(self, print: bool) -> str: # for each analytical patch that is required to be added, generate # the string that contains that function. for pid, items in ib_patches.items(): - # function that defines how we will replace variable names with # values from the case file def rhs_replace(match): return { - 'x': 'x_cc(i)', 'y': 'y_cc(j)', 'z': 'z_cc(k)', - 't': 'mytime', - - 'r': f'patch_ib({pid})%radius', - - 'e' : f'{math.e}', + "x": "x_cc(i)", + "y": "y_cc(j)", + "z": "z_cc(k)", + "t": "mytime", + "r": f"patch_ib({pid})%radius", + "e": f"{math.e}", }.get(match.group(), match.group()) lines = [] @@ -279,7 +296,7 @@ def rhs_replace(match): # new lines as a fully concatenated string with fortran syntax srcs.append(f"""\ if (i == {pid}) then -{f'{chr(10)}'.join(lines)} +{f"{chr(10)}".join(lines)} end if\ """) @@ -288,7 +305,7 @@ def rhs_replace(match): ! parameterize the velocity and rotation rate of a moving IB. #:def mib_analytical() -{f'{chr(10)}{chr(10)}'.join(srcs)} +{f"{chr(10)}{chr(10)}".join(srcs)} #:enddef """ return content @@ -307,11 +324,11 @@ def __get_sim_fpp(self, print: bool) -> str: elif bubble_model == 3: nterms = 7 - mapped_weno = 1 if self.params.get("mapped_weno", 'F') == 'T' else 0 - wenoz = 1 if self.params.get("wenoz", 'F') == 'T' else 0 - teno = 1 if self.params.get("teno", 'F') == 'T' else 0 + mapped_weno = 1 if self.params.get("mapped_weno", "F") == "T" else 0 + wenoz = 1 if self.params.get("wenoz", "F") == "T" else 0 + teno = 1 if self.params.get("teno", "F") == "T" else 0 wenojs = 0 if (mapped_weno or wenoz or teno) else 1 - igr = 1 if self.params.get("igr", 'F') == 'T' else 0 + igr = 1 if self.params.get("igr", "F") == "T" else 0 recon_type = self.params.get("recon_type", 1) @@ -322,7 +339,7 @@ def __get_sim_fpp(self, print: bool) -> str: else: weno_polyn = 1 - if self.params.get("igr", "F") == 'T': + if self.params.get("igr", "F") == "T": weno_order = 5 weno_polyn = 3 @@ -332,16 +349,16 @@ def __get_sim_fpp(self, print: bool) -> str: weno_num_stencils = weno_polyn num_dims = 1 + min(int(self.params.get("n", 0)), 1) + min(int(self.params.get("p", 0)), 1) - if self.params.get("mhd", 'F') == 'T': + if self.params.get("mhd", "F") == "T": num_vels = 3 else: num_vels = num_dims - mhd = 1 if self.params.get("mhd", 'F') == 'T' else 0 - relativity = 1 if self.params.get("relativity", 'F') == 'T' else 0 - viscous = 1 if self.params.get("viscous", 'F') == 'T' else 0 - igr = 1 if self.params.get("igr", 'F') == 'T' else 0 - igr_pres_lim = 1 if self.params.get("igr_pres_lim", 'F') == 'T' else 0 + mhd = 1 if self.params.get("mhd", "F") == "T" else 0 + relativity = 1 if self.params.get("relativity", "F") == "T" else 0 + viscous = 1 if self.params.get("viscous", "F") == "T" else 0 + igr = 1 if self.params.get("igr", "F") == "T" else 0 + igr_pres_lim = 1 if self.params.get("igr_pres_lim", "F") == "T" else 0 # Throw error if wenoz_q is required but not set out = f"""\ @@ -380,25 +397,24 @@ def __get_sim_fpp(self, print: bool) -> str: # We need to also include the pre_processing includes so that common subroutines have access to the @:analytical function return out + f"\n{self.__get_pre_fpp(print)}" - def __get_pre_fpp(self, print: bool) -> str: out = self.__get_analytic_ic_fpp(print) return out - def get_fpp(self, target, print = True) -> str: - from . import build # pylint: disable=import-outside-toplevel + def get_fpp(self, target, print=True) -> str: + from . import build def _prepend() -> str: return f"""\ -#:set chemistry = {self.params.get("chemistry", 'F') == 'T'} +#:set chemistry = {self.params.get("chemistry", "F") == "T"} """ def _default(_) -> str: return "! This file is purposefully empty." result = { - "pre_process" : self.__get_pre_fpp, - "simulation" : self.__get_sim_fpp, + "pre_process": self.__get_pre_fpp, + "simulation": self.__get_sim_fpp, }.get(build.get_target(target).name, _default)(print) return _prepend() + result diff --git a/toolchain/mfc/case_utils.py b/toolchain/mfc/case_utils.py index a4c2f9caab..3bcd9ef5e2 100644 --- a/toolchain/mfc/case_utils.py +++ b/toolchain/mfc/case_utils.py @@ -1,19 +1,17 @@ import re + def remove_higher_dimensional_keys(case: dict, ndims: int) -> dict: assert 1 <= ndims <= 3 - rm_dims = [set(), set(['y', 'z']), set(['z']), set()][ndims] - dim_ids = {dim: i + 1 for i, dim in enumerate(['x', 'y', 'z'])} - dim_mnp = {'x': 'm', 'y': 'n', 'z': 'p'} + rm_dims = [set(), set(["y", "z"]), set(["z"]), set()][ndims] + dim_ids = {dim: i + 1 for i, dim in enumerate(["x", "y", "z"])} + dim_mnp = {"x": "m", "y": "n", "z": "p"} rm_keys = set() for key in case.keys(): for dim in rm_dims: - if any([ - re.match(f'.+_{dim}', key), re.match(f'{dim}_.+', key), - re.match(f'%{dim}', key), f'%vel({dim_ids[dim]})' in key - ]): + if any([re.match(f".+_{dim}", key), re.match(f"{dim}_.+", key), re.match(f"%{dim}", key), f"%vel({dim_ids[dim]})" in key]): rm_keys.add(key) break diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index cc6fba0c00..db7a3b8d72 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -11,17 +11,16 @@ - src/simulation/m_checker.fpp - src/post_process/m_checker.fpp """ -# pylint: disable=too-many-lines # Justification: Comprehensive validator covering all MFC parameter constraints import re -from typing import Dict, Any, List, Set from functools import lru_cache +from typing import Any, Dict, List, Set + from .common import MFCException from .params.definitions import CONSTRAINTS from .state import CFG - # Physics documentation for check methods. # Each entry maps a check method name to metadata used by gen_physics_docs.py # to auto-generate docs/documentation/physics_constraints.md. @@ -39,22 +38,14 @@ "title": "EOS Parameter Sanity (Transformed Gamma)", "category": "Thermodynamic Constraints", "math": r"\Gamma = \frac{1}{\gamma - 1}", - "explanation": ( - "MFC uses the transformed stiffened gas parameter. " - "A common mistake is entering the physical gamma (e.g., 1.4 for air) " - "instead of the transformed value 1/(gamma-1) = 2.5." - ), + "explanation": ("MFC uses the transformed stiffened gas parameter. A common mistake is entering the physical gamma (e.g., 1.4 for air) instead of the transformed value 1/(gamma-1) = 2.5."), "references": ["Wilfong26", "Allaire02"], }, "check_patch_physics": { "title": "Patch Initial Condition Constraints", "category": "Thermodynamic Constraints", "math": r"p > 0, \quad \alpha_i \rho_i \geq 0, \quad 0 \leq \alpha_i \leq 1", - "explanation": ( - "All initial patch pressures must be strictly positive. " - "Partial densities must be non-negative. " - "Volume fractions must be in [0,1]." - ), + "explanation": ("All initial patch pressures must be strictly positive. Partial densities must be non-negative. Volume fractions must be in [0,1]."), }, # --- Mixture Constraints --- "check_volume_fraction_sum": { @@ -74,10 +65,7 @@ "title": "Alpha-Rho Consistency", "category": "Mixture Constraints", "math": r"\alpha_j = 0 \Rightarrow \alpha_j \rho_j = 0, \quad \alpha_j > 0 \Rightarrow \alpha_j \rho_j > 0", - "explanation": ( - "Warns about physically inconsistent combinations: " - "density assigned to an absent phase, or a present phase with zero density." - ), + "explanation": ("Warns about physically inconsistent combinations: density assigned to an absent phase, or a present phase with zero density."), }, # --- Domain and Geometry --- "check_domain_bounds": { @@ -90,18 +78,12 @@ "title": "Dimensionality", "category": "Domain and Geometry", "math": r"m > 0, \quad n \geq 0, \quad p \geq 0", - "explanation": ( - "The x-direction must have cells. Cannot have z without y. " - "Cylindrical coordinates require odd p." - ), + "explanation": ("The x-direction must have cells. Cannot have z without y. Cylindrical coordinates require odd p."), }, "check_patch_within_domain": { "title": "Patch Within Domain", "category": "Domain and Geometry", - "explanation": ( - "For patches with centroid + length geometry, the bounding box must not be " - "entirely outside the computational domain. Skipped when grid stretching is active." - ), + "explanation": ("For patches with centroid + length geometry, the bounding box must not be entirely outside the computational domain. Skipped when grid stretching is active."), }, # --- Velocity and Dimensional Consistency --- "check_velocity_components": { @@ -115,40 +97,26 @@ "check_model_eqns_and_num_fluids": { "title": "Model Equation Selection", "category": "Model Equations", - "explanation": ( - "Model 1: gamma-law single-fluid. " - "Model 2: five-equation (Allaire). " - "Model 3: six-equation (Saurel). " - "Model 4: four-equation (single-component with bubbles)." - ), + "explanation": ("Model 1: gamma-law single-fluid. Model 2: five-equation (Allaire). Model 3: six-equation (Saurel). Model 4: four-equation (single-component with bubbles)."), "references": ["Wilfong26", "Allaire02", "Saurel09"], }, # --- Boundary Conditions --- "check_boundary_conditions": { "title": "Boundary Condition Compatibility", "category": "Boundary Conditions", - "explanation": ( - "Periodicity must match on both ends. Valid BC values range from -1 to -17. " - "Cylindrical coordinates have specific BC requirements at the axis." - ), + "explanation": ("Periodicity must match on both ends. Valid BC values range from -1 to -17. Cylindrical coordinates have specific BC requirements at the axis."), }, # --- Bubble Physics --- "check_bubbles_euler": { "title": "Euler-Euler Bubble Model", "category": "Bubble Physics", - "explanation": ( - "Requires nb >= 1, positive reference quantities. " - "Polydisperse requires odd nb > 1 and poly_sigma > 0. QBMM requires nnode = 4." - ), + "explanation": ("Requires nb >= 1, positive reference quantities. Polydisperse requires odd nb > 1 and poly_sigma > 0. QBMM requires nnode = 4."), "references": ["Bryngelson21"], }, "check_bubbles_euler_simulation": { "title": "Bubble Simulation Constraints", "category": "Bubble Physics", - "explanation": ( - "Requires HLLC Riemann solver and arithmetic average. " - "Five-equation model does not support Gilmore bubble_model." - ), + "explanation": ("Requires HLLC Riemann solver and arithmetic average. Five-equation model does not support Gilmore bubble_model."), }, "check_bubbles_lagrange": { "title": "Euler-Lagrange Bubble Model", @@ -159,10 +127,7 @@ "check_weno": { "title": "WENO Reconstruction", "category": "Numerical Schemes", - "explanation": ( - "weno_order must be 1, 3, 5, or 7. Grid must have enough cells. " - "Only one of mapped_weno, wenoz, teno can be active." - ), + "explanation": ("weno_order must be 1, 3, 5, or 7. Grid must have enough cells. Only one of mapped_weno, wenoz, teno can be active."), }, "check_muscl": { "title": "MUSCL Reconstruction", @@ -172,10 +137,7 @@ "check_time_stepping": { "title": "Time Stepping", "category": "Numerical Schemes", - "explanation": ( - "time_stepper in {1,2,3}. Fixed dt must be positive. " - "CFL-based modes require cfl_target in (0,1]." - ), + "explanation": ("time_stepper in {1,2,3}. Fixed dt must be positive. CFL-based modes require cfl_target in (0,1]."), }, "check_viscosity": { "title": "Viscosity", @@ -187,10 +149,7 @@ "check_mhd": { "title": "Magnetohydrodynamics (MHD)", "category": "Feature Compatibility", - "explanation": ( - "Requires model_eqns = 2, num_fluids = 1, HLL or HLLD Riemann solver. " - "No relativity with HLLD." - ), + "explanation": ("Requires model_eqns = 2, num_fluids = 1, HLL or HLLD Riemann solver. No relativity with HLLD."), }, "check_surface_tension": { "title": "Surface Tension", @@ -215,19 +174,13 @@ "check_igr": { "title": "Iterative Generalized Riemann (IGR)", "category": "Feature Compatibility", - "explanation": ( - "Requires model_eqns = 2. Incompatible with characteristic BCs, " - "bubbles, MHD, and elastic models." - ), + "explanation": ("Requires model_eqns = 2. Incompatible with characteristic BCs, bubbles, MHD, and elastic models."), }, # --- Acoustic Sources --- "check_acoustic_source": { "title": "Acoustic Sources", "category": "Acoustic Sources", - "explanation": ( - "Dimension-specific support types. Pulse type in {1,2,3,4}. " - "Non-planar sources require foc_length and aperture." - ), + "explanation": ("Dimension-specific support types. Pulse type in {1,2,3,4}. Non-planar sources require foc_length and aperture."), }, # --- Post-Processing --- "check_vorticity": { @@ -238,10 +191,7 @@ "check_fft": { "title": "FFT Output", "category": "Post-Processing", - "explanation": ( - "Requires 3D with all periodic boundaries. " - "Global dimensions must be even. Incompatible with cylindrical coordinates." - ), + "explanation": ("Requires 3D with all periodic boundaries. Global dimensions must be even. Incompatible with cylindrical coordinates."), }, } @@ -257,20 +207,17 @@ def _get_logical_params_from_registry() -> Set[str]: Returns: Set of parameter names that have LOG type. """ - from .params import REGISTRY # pylint: disable=import-outside-toplevel - from .params.schema import ParamType # pylint: disable=import-outside-toplevel + from .params import REGISTRY + from .params.schema import ParamType - return { - name for name, param in REGISTRY.all_params.items() - if param.param_type == ParamType.LOG - } + return {name for name, param in REGISTRY.all_params.items() if param.param_type == ParamType.LOG} class CaseConstraintError(MFCException): """Exception raised when case parameters violate constraints""" -class CaseValidator: # pylint: disable=too-many-public-methods +class CaseValidator: """Validates MFC case parameter constraints""" def __init__(self, params: Dict[str, Any]): @@ -299,10 +246,8 @@ def warn(self, condition: bool, message: str): def _validate_logical(self, key: str): """Validate that a parameter is a valid Fortran logical ('T' or 'F').""" val = self.get(key) - if val is not None and val not in ('T', 'F'): - self.errors.append( - f"{key} must be 'T' or 'F', got '{val}'" - ) + if val is not None and val not in ("T", "F"): + self.errors.append(f"{key} must be 'T' or 'F', got '{val}'") def check_parameter_types(self): """Validate parameter types before other checks. @@ -320,12 +265,10 @@ def check_parameter_types(self): self._validate_logical(param) # Required domain parameters when m > 0 - m = self.get('m') + m = self.get("m") if m is not None and m > 0: - self.prohibit(not self.is_set('x_domain%beg'), - "x_domain%beg must be set when m > 0") - self.prohibit(not self.is_set('x_domain%end'), - "x_domain%end must be set when m > 0") + self.prohibit(not self.is_set("x_domain%beg"), "x_domain%beg must be set when m > 0") + self.prohibit(not self.is_set("x_domain%end"), "x_domain%end must be set when m > 0") # =================================================================== # Common Checks (All Stages) @@ -333,262 +276,209 @@ def check_parameter_types(self): def check_simulation_domain(self): """Checks constraints on dimensionality and number of cells""" - m = self.get('m') - n = self.get('n', 0) - p = self.get('p', 0) - cyl_coord = self.get('cyl_coord', 'F') == 'T' + m = self.get("m") + n = self.get("n", 0) + p = self.get("p", 0) + cyl_coord = self.get("cyl_coord", "F") == "T" self.prohibit(m is None, "m must be set") self.prohibit(m is not None and m <= 0, "m must be positive") self.prohibit(n is not None and n < 0, "n must be non-negative") self.prohibit(p is not None and p < 0, "p must be non-negative") - self.prohibit(cyl_coord and p is not None and p > 0 and p % 2 == 0, - "p must be odd for cylindrical coordinates") - self.prohibit(n is not None and p is not None and n == 0 and p > 0, - "p must be 0 if n = 0") + self.prohibit(cyl_coord and p is not None and p > 0 and p % 2 == 0, "p must be odd for cylindrical coordinates") + self.prohibit(n is not None and p is not None and n == 0 and p > 0, "p must be 0 if n = 0") def check_model_eqns_and_num_fluids(self): """Checks constraints on model equations and number of fluids""" - model_eqns = self.get('model_eqns') - num_fluids = self.get('num_fluids') - mpp_lim = self.get('mpp_lim', 'F') == 'T' - cyl_coord = self.get('cyl_coord', 'F') == 'T' - p = self.get('p', 0) - - self.prohibit(model_eqns is not None and model_eqns not in [1, 2, 3, 4], - "model_eqns must be 1, 2, 3, or 4") - self.prohibit(num_fluids is not None and num_fluids < 1, - "num_fluids must be positive") - self.prohibit(model_eqns == 1 and num_fluids is not None, - "num_fluids is not supported for model_eqns = 1") - self.prohibit(model_eqns == 2 and num_fluids is None, - "5-equation model (model_eqns = 2) requires num_fluids to be set") - self.prohibit(model_eqns == 3 and num_fluids is None, - "6-equation model (model_eqns = 3) requires num_fluids to be set") - self.prohibit(model_eqns == 4 and num_fluids is None, - "4-equation model (model_eqns = 4) requires num_fluids to be set") - self.prohibit(model_eqns == 1 and mpp_lim, - "model_eqns = 1 does not support mpp_lim") - self.prohibit(num_fluids == 1 and mpp_lim, - "num_fluids = 1 does not support mpp_lim") - self.prohibit(model_eqns == 3 and cyl_coord and p != 0, - "6-equation model (model_eqns = 3) does not support cylindrical coordinates (cyl_coord = T and p != 0)") + model_eqns = self.get("model_eqns") + num_fluids = self.get("num_fluids") + mpp_lim = self.get("mpp_lim", "F") == "T" + cyl_coord = self.get("cyl_coord", "F") == "T" + p = self.get("p", 0) + + self.prohibit(model_eqns is not None and model_eqns not in [1, 2, 3, 4], "model_eqns must be 1, 2, 3, or 4") + self.prohibit(num_fluids is not None and num_fluids < 1, "num_fluids must be positive") + self.prohibit(model_eqns == 1 and num_fluids is not None, "num_fluids is not supported for model_eqns = 1") + self.prohibit(model_eqns == 2 and num_fluids is None, "5-equation model (model_eqns = 2) requires num_fluids to be set") + self.prohibit(model_eqns == 3 and num_fluids is None, "6-equation model (model_eqns = 3) requires num_fluids to be set") + self.prohibit(model_eqns == 4 and num_fluids is None, "4-equation model (model_eqns = 4) requires num_fluids to be set") + self.prohibit(model_eqns == 1 and mpp_lim, "model_eqns = 1 does not support mpp_lim") + self.prohibit(num_fluids == 1 and mpp_lim, "num_fluids = 1 does not support mpp_lim") + self.prohibit(model_eqns == 3 and cyl_coord and p != 0, "6-equation model (model_eqns = 3) does not support cylindrical coordinates (cyl_coord = T and p != 0)") def check_igr(self): """Checks constraints regarding IGR order""" - igr = self.get('igr', 'F') == 'T' - igr_pres_lim = self.get('igr_pres_lim', 'F') == 'T' + igr = self.get("igr", "F") == "T" + igr_pres_lim = self.get("igr_pres_lim", "F") == "T" - self.prohibit(igr_pres_lim and not igr, - "igr_pres_lim requires igr to be enabled") + self.prohibit(igr_pres_lim and not igr, "igr_pres_lim requires igr to be enabled") if not igr: return - igr_order = self.get('igr_order') - m = self.get('m', 0) - n = self.get('n', 0) - p = self.get('p', 0) - self.prohibit(igr_order not in [None, 3, 5], - "igr_order must be 3 or 5") + igr_order = self.get("igr_order") + m = self.get("m", 0) + n = self.get("n", 0) + p = self.get("p", 0) + self.prohibit(igr_order not in [None, 3, 5], "igr_order must be 3 or 5") if igr_order: - self.prohibit(m + 1 < igr_order, - f"m must be at least igr_order - 1 (= {igr_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < igr_order, - f"n must be at least igr_order - 1 (= {igr_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < igr_order, - f"p must be at least igr_order - 1 (= {igr_order - 1})") + self.prohibit(m + 1 < igr_order, f"m must be at least igr_order - 1 (= {igr_order - 1})") + self.prohibit(n is not None and n > 0 and n + 1 < igr_order, f"n must be at least igr_order - 1 (= {igr_order - 1})") + self.prohibit(p is not None and p > 0 and p + 1 < igr_order, f"p must be at least igr_order - 1 (= {igr_order - 1})") def check_weno(self): """Checks constraints regarding WENO order""" - recon_type = self.get('recon_type', 1) + recon_type = self.get("recon_type", 1) # WENO_TYPE = 1 if recon_type != 1: return - weno_order = self.get('weno_order') - m = self.get('m', 0) - n = self.get('n', 0) - p = self.get('p', 0) + weno_order = self.get("weno_order") + m = self.get("m", 0) + n = self.get("n", 0) + p = self.get("p", 0) if weno_order is None: return - self.prohibit(weno_order not in [1, 3, 5, 7], - "weno_order must be 1, 3, 5, or 7") - self.prohibit(m + 1 < weno_order, - f"m must be at least weno_order - 1 (= {weno_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < weno_order, - f"For 2D simulation, n must be at least weno_order - 1 (= {weno_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < weno_order, - f"For 3D simulation, p must be at least weno_order - 1 (= {weno_order - 1})") + self.prohibit(weno_order not in [1, 3, 5, 7], "weno_order must be 1, 3, 5, or 7") + self.prohibit(m + 1 < weno_order, f"m must be at least weno_order - 1 (= {weno_order - 1})") + self.prohibit(n is not None and n > 0 and n + 1 < weno_order, f"For 2D simulation, n must be at least weno_order - 1 (= {weno_order - 1})") + self.prohibit(p is not None and p > 0 and p + 1 < weno_order, f"For 3D simulation, p must be at least weno_order - 1 (= {weno_order - 1})") def check_muscl(self): """Check constraints regarding MUSCL order""" - recon_type = self.get('recon_type', 1) - int_comp = self.get('int_comp', 'F') == 'T' + recon_type = self.get("recon_type", 1) + int_comp = self.get("int_comp", "F") == "T" - self.prohibit(int_comp and recon_type != 2, - "int_comp (THINC interface compression) requires recon_type = 2 (MUSCL)") + self.prohibit(int_comp and recon_type != 2, "int_comp (THINC interface compression) requires recon_type = 2 (MUSCL)") # MUSCL_TYPE = 2 if recon_type != 2: return - muscl_order = self.get('muscl_order') - m = self.get('m', 0) - n = self.get('n', 0) - p = self.get('p', 0) + muscl_order = self.get("muscl_order") + m = self.get("m", 0) + n = self.get("n", 0) + p = self.get("p", 0) if muscl_order is None: return - self.prohibit(muscl_order not in [1, 2], - "muscl_order must be 1 or 2") - self.prohibit(m + 1 < muscl_order, - f"m must be at least muscl_order - 1 (= {muscl_order - 1})") - self.prohibit(n is not None and n > 0 and n + 1 < muscl_order, - f"For 2D simulation, n must be at least muscl_order - 1 (= {muscl_order - 1})") - self.prohibit(p is not None and p > 0 and p + 1 < muscl_order, - f"For 3D simulation, p must be at least muscl_order - 1 (= {muscl_order - 1})") + self.prohibit(muscl_order not in [1, 2], "muscl_order must be 1 or 2") + self.prohibit(m + 1 < muscl_order, f"m must be at least muscl_order - 1 (= {muscl_order - 1})") + self.prohibit(n is not None and n > 0 and n + 1 < muscl_order, f"For 2D simulation, n must be at least muscl_order - 1 (= {muscl_order - 1})") + self.prohibit(p is not None and p > 0 and p + 1 < muscl_order, f"For 3D simulation, p must be at least muscl_order - 1 (= {muscl_order - 1})") - def check_boundary_conditions(self): # pylint: disable=too-many-locals + def check_boundary_conditions(self): """Checks constraints on boundary conditions""" - cyl_coord = self.get('cyl_coord', 'F') == 'T' - m = self.get('m', 0) - n = self.get('n', 0) - p = self.get('p', 0) + cyl_coord = self.get("cyl_coord", "F") == "T" + m = self.get("m", 0) + n = self.get("n", 0) + p = self.get("p", 0) - for dir, var in [('x', 'm'), ('y', 'n'), ('z', 'p')]: - var_val = {'m': m, 'n': n, 'p': p}[var] + for dir, var in [("x", "m"), ("y", "n"), ("z", "p")]: + var_val = {"m": m, "n": n, "p": p}[var] - for bound in ['beg', 'end']: - bc_key = f'bc_{dir}%{bound}' + for bound in ["beg", "end"]: + bc_key = f"bc_{dir}%{bound}" bc_val = self.get(bc_key) - self.prohibit(var_val is not None and var_val == 0 and bc_val is not None, - f"{bc_key} is not supported for {var} = 0") - self.prohibit(var_val is not None and var_val > 0 and bc_val is None, - f"{var} != 0 but {bc_key} is not set") + self.prohibit(var_val is not None and var_val == 0 and bc_val is not None, f"{bc_key} is not supported for {var} = 0") + self.prohibit(var_val is not None and var_val > 0 and bc_val is None, f"{var} != 0 but {bc_key} is not set") # Check periodicity matches - beg_bc = self.get(f'bc_{dir}%beg') - end_bc = self.get(f'bc_{dir}%end') + beg_bc = self.get(f"bc_{dir}%beg") + end_bc = self.get(f"bc_{dir}%end") if beg_bc is not None and end_bc is not None: - self.prohibit((beg_bc == -1 and end_bc != -1) or (end_bc == -1 and beg_bc != -1), - f"bc_{dir}%beg and bc_{dir}%end must be both periodic (= -1) or both non-periodic") + self.prohibit((beg_bc == -1 and end_bc != -1) or (end_bc == -1 and beg_bc != -1), f"bc_{dir}%beg and bc_{dir}%end must be both periodic (= -1) or both non-periodic") # Range check (skip for cylindrical y/z) - skip_check = cyl_coord and dir in ['y', 'z'] - for bound in ['beg', 'end']: - bc_key = f'bc_{dir}%{bound}' + skip_check = cyl_coord and dir in ["y", "z"] + for bound in ["beg", "end"]: + bc_key = f"bc_{dir}%{bound}" bc_val = self.get(bc_key) if not skip_check and bc_val is not None: - self.prohibit(bc_val > -1 or bc_val < -17, - f"{bc_key} must be between -1 and -17") - self.prohibit(bc_val == -14 and not cyl_coord, - f"{bc_key} must not be -14 (BC_AXIS) for non-cylindrical coordinates") + self.prohibit(bc_val > -1 or bc_val < -17, f"{bc_key} must be between -1 and -17") + self.prohibit(bc_val == -14 and not cyl_coord, f"{bc_key} must not be -14 (BC_AXIS) for non-cylindrical coordinates") # Check BC_NULL is not used - for dir in ['x', 'y', 'z']: - for bound in ['beg', 'end']: - bc_val = self.get(f'bc_{dir}%{bound}') - self.prohibit(bc_val == -13, - "Boundary condition -13 (BC_NULL) is not supported") + for dir in ["x", "y", "z"]: + for bound in ["beg", "end"]: + bc_val = self.get(f"bc_{dir}%{bound}") + self.prohibit(bc_val == -13, "Boundary condition -13 (BC_NULL) is not supported") # Cylindrical specific checks if cyl_coord: self.prohibit(n is not None and n == 0, "n must be positive (2D or 3D) for cylindrical coordinates") - bc_y_beg = self.get('bc_y%beg') - bc_y_end = self.get('bc_y%end') - bc_z_beg = self.get('bc_z%beg') - bc_z_end = self.get('bc_z%end') + bc_y_beg = self.get("bc_y%beg") + bc_y_end = self.get("bc_y%end") + bc_z_beg = self.get("bc_z%beg") + bc_z_end = self.get("bc_z%end") - self.prohibit(p is not None and p == 0 and bc_y_beg != -2, - "bc_y%beg must be -2 (BC_REFLECTIVE) for 2D cylindrical coordinates (p = 0)") - self.prohibit(p is not None and p > 0 and bc_y_beg != -14, - "bc_y%beg must be -14 (BC_AXIS) for 3D cylindrical coordinates (p > 0)") + self.prohibit(p is not None and p == 0 and bc_y_beg != -2, "bc_y%beg must be -2 (BC_REFLECTIVE) for 2D cylindrical coordinates (p = 0)") + self.prohibit(p is not None and p > 0 and bc_y_beg != -14, "bc_y%beg must be -14 (BC_AXIS) for 3D cylindrical coordinates (p > 0)") if bc_y_end is not None: - self.prohibit(bc_y_end > -1 or bc_y_end < -17, - "bc_y%end must be between -1 and -17") - self.prohibit(bc_y_end == -14, - "bc_y%end must not be -14 (BC_AXIS)") + self.prohibit(bc_y_end > -1 or bc_y_end < -17, "bc_y%end must be between -1 and -17") + self.prohibit(bc_y_end == -14, "bc_y%end must not be -14 (BC_AXIS)") # 3D cylindrical if p is not None and p > 0: - self.prohibit(bc_z_beg is not None and bc_z_beg not in [-1, -2], - "bc_z%beg must be -1 (periodic) or -2 (reflective) for 3D cylindrical coordinates") - self.prohibit(bc_z_end is not None and bc_z_end not in [-1, -2], - "bc_z%end must be -1 (periodic) or -2 (reflective) for 3D cylindrical coordinates") + self.prohibit(bc_z_beg is not None and bc_z_beg not in [-1, -2], "bc_z%beg must be -1 (periodic) or -2 (reflective) for 3D cylindrical coordinates") + self.prohibit(bc_z_end is not None and bc_z_end not in [-1, -2], "bc_z%end must be -1 (periodic) or -2 (reflective) for 3D cylindrical coordinates") - def check_bubbles_euler(self): # pylint: disable=too-many-locals + def check_bubbles_euler(self): """Checks constraints on bubble parameters""" - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' + bubbles_euler = self.get("bubbles_euler", "F") == "T" if not bubbles_euler: return - nb = self.get('nb') - polydisperse = self.get('polydisperse', 'F') == 'T' - thermal = self.get('thermal') - model_eqns = self.get('model_eqns') - cyl_coord = self.get('cyl_coord', 'F') == 'T' - rhoref = self.get('rhoref') - pref = self.get('pref') - num_fluids = self.get('num_fluids') - - self.prohibit(nb is None or nb < 1, - "The Ensemble-Averaged Bubble Model requires nb >= 1") - self.prohibit(polydisperse and nb == 1, - "Polydisperse bubble dynamics requires nb > 1") - self.prohibit(polydisperse and nb is not None and nb % 2 == 0, - "nb must be odd for polydisperse bubbles") - self.prohibit(thermal is not None and thermal > 3, - "thermal must be <= 3") - self.prohibit(model_eqns == 3, - "Bubble models untested with 6-equation model (model_eqns = 3)") - self.prohibit(model_eqns == 1, - "Bubble models untested with pi-gamma model (model_eqns = 1)") - self.prohibit(model_eqns == 4 and rhoref is None, - "rhoref must be set if using bubbles_euler with model_eqns = 4") - self.prohibit(rhoref is not None and rhoref <= 0, - "rhoref (reference density) must be positive") - self.prohibit(model_eqns == 4 and pref is None, - "pref must be set if using bubbles_euler with model_eqns = 4") - self.prohibit(pref is not None and pref <= 0, - "pref (reference pressure) must be positive") - self.prohibit(model_eqns == 4 and num_fluids != 1, - "4-equation model (model_eqns = 4) is single-component and requires num_fluids = 1") - self.prohibit(cyl_coord, - "Bubble models untested in cylindrical coordinates") + nb = self.get("nb") + polydisperse = self.get("polydisperse", "F") == "T" + thermal = self.get("thermal") + model_eqns = self.get("model_eqns") + cyl_coord = self.get("cyl_coord", "F") == "T" + rhoref = self.get("rhoref") + pref = self.get("pref") + num_fluids = self.get("num_fluids") + + self.prohibit(nb is None or nb < 1, "The Ensemble-Averaged Bubble Model requires nb >= 1") + self.prohibit(polydisperse and nb == 1, "Polydisperse bubble dynamics requires nb > 1") + self.prohibit(polydisperse and nb is not None and nb % 2 == 0, "nb must be odd for polydisperse bubbles") + self.prohibit(thermal is not None and thermal > 3, "thermal must be <= 3") + self.prohibit(model_eqns == 3, "Bubble models untested with 6-equation model (model_eqns = 3)") + self.prohibit(model_eqns == 1, "Bubble models untested with pi-gamma model (model_eqns = 1)") + self.prohibit(model_eqns == 4 and rhoref is None, "rhoref must be set if using bubbles_euler with model_eqns = 4") + self.prohibit(rhoref is not None and rhoref <= 0, "rhoref (reference density) must be positive") + self.prohibit(model_eqns == 4 and pref is None, "pref must be set if using bubbles_euler with model_eqns = 4") + self.prohibit(pref is not None and pref <= 0, "pref (reference pressure) must be positive") + self.prohibit(model_eqns == 4 and num_fluids != 1, "4-equation model (model_eqns = 4) is single-component and requires num_fluids = 1") + self.prohibit(cyl_coord, "Bubble models untested in cylindrical coordinates") # === BUBBLE PHYSICS PARAMETERS === # Validate bubble reference parameters (bub_pp%) - R0ref = self.get('bub_pp%R0ref') - p0ref = self.get('bub_pp%p0ref') - rho0ref = self.get('bub_pp%rho0ref') - T0ref = self.get('bub_pp%T0ref') + R0ref = self.get("bub_pp%R0ref") + p0ref = self.get("bub_pp%p0ref") + rho0ref = self.get("bub_pp%rho0ref") + T0ref = self.get("bub_pp%T0ref") if R0ref is not None: - self.prohibit(R0ref <= 0, - "bub_pp%R0ref (reference bubble radius) must be positive") + self.prohibit(R0ref <= 0, "bub_pp%R0ref (reference bubble radius) must be positive") if p0ref is not None: - self.prohibit(p0ref <= 0, - "bub_pp%p0ref (reference pressure) must be positive") + self.prohibit(p0ref <= 0, "bub_pp%p0ref (reference pressure) must be positive") if rho0ref is not None: - self.prohibit(rho0ref <= 0, - "bub_pp%rho0ref (reference density) must be positive") + self.prohibit(rho0ref <= 0, "bub_pp%rho0ref (reference density) must be positive") if T0ref is not None: - self.prohibit(T0ref <= 0, - "bub_pp%T0ref (reference temperature) must be positive") + self.prohibit(T0ref <= 0, "bub_pp%T0ref (reference temperature) must be positive") # Viscosities must be non-negative - mu_l = self.get('bub_pp%mu_l') - mu_g = self.get('bub_pp%mu_g') - mu_v = self.get('bub_pp%mu_v') + mu_l = self.get("bub_pp%mu_l") + mu_g = self.get("bub_pp%mu_g") + mu_v = self.get("bub_pp%mu_v") if mu_l is not None: self.prohibit(mu_l < 0, "bub_pp%mu_l (liquid viscosity) must be non-negative") @@ -598,103 +488,85 @@ def check_bubbles_euler(self): # pylint: disable=too-many-locals self.prohibit(mu_v < 0, "bub_pp%mu_v (vapor viscosity) must be non-negative") # Surface tension must be non-negative - ss = self.get('bub_pp%ss') + ss = self.get("bub_pp%ss") if ss is not None: self.prohibit(ss < 0, "bub_pp%ss (surface tension) must be non-negative") def check_qbmm_and_polydisperse(self): """Checks constraints on QBMM and polydisperse bubble parameters""" - polydisperse = self.get('polydisperse', 'F') == 'T' - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - poly_sigma = self.get('poly_sigma') - qbmm = self.get('qbmm', 'F') == 'T' - nnode = self.get('nnode') - - self.prohibit(polydisperse and not bubbles_euler, - "Polydisperse bubble modeling requires the bubbles_euler flag to be set") - self.prohibit(polydisperse and poly_sigma is None, - "Polydisperse bubble modeling requires poly_sigma to be set") - self.prohibit(polydisperse and poly_sigma is not None and poly_sigma <= 0, - "poly_sigma must be positive") - self.prohibit(qbmm and not bubbles_euler, - "QBMM requires the bubbles_euler flag to be set") - self.prohibit(qbmm and nnode is not None and nnode != 4, - "QBMM requires nnode = 4") + polydisperse = self.get("polydisperse", "F") == "T" + bubbles_euler = self.get("bubbles_euler", "F") == "T" + poly_sigma = self.get("poly_sigma") + qbmm = self.get("qbmm", "F") == "T" + nnode = self.get("nnode") + + self.prohibit(polydisperse and not bubbles_euler, "Polydisperse bubble modeling requires the bubbles_euler flag to be set") + self.prohibit(polydisperse and poly_sigma is None, "Polydisperse bubble modeling requires poly_sigma to be set") + self.prohibit(polydisperse and poly_sigma is not None and poly_sigma <= 0, "poly_sigma must be positive") + self.prohibit(qbmm and not bubbles_euler, "QBMM requires the bubbles_euler flag to be set") + self.prohibit(qbmm and nnode is not None and nnode != 4, "QBMM requires nnode = 4") def check_adv_n(self): """Checks constraints on adv_n flag""" - adv_n = self.get('adv_n', 'F') == 'T' - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - num_fluids = self.get('num_fluids') - qbmm = self.get('qbmm', 'F') == 'T' + adv_n = self.get("adv_n", "F") == "T" + bubbles_euler = self.get("bubbles_euler", "F") == "T" + num_fluids = self.get("num_fluids") + qbmm = self.get("qbmm", "F") == "T" if not adv_n: return - self.prohibit(not bubbles_euler, - "adv_n requires bubbles_euler to be enabled") - self.prohibit(num_fluids != 1, - "adv_n requires num_fluids = 1") - self.prohibit(qbmm, - "adv_n is not compatible with qbmm") + self.prohibit(not bubbles_euler, "adv_n requires bubbles_euler to be enabled") + self.prohibit(num_fluids != 1, "adv_n requires num_fluids = 1") + self.prohibit(qbmm, "adv_n is not compatible with qbmm") def check_hypoelasticity(self): """Checks constraints on hypoelasticity parameters""" - hypoelasticity = self.get('hypoelasticity', 'F') == 'T' - model_eqns = self.get('model_eqns') - riemann_solver = self.get('riemann_solver') + hypoelasticity = self.get("hypoelasticity", "F") == "T" + model_eqns = self.get("model_eqns") + riemann_solver = self.get("riemann_solver") if not hypoelasticity: return - self.prohibit(model_eqns is not None and model_eqns != 2, - "hypoelasticity requires model_eqns = 2") - self.prohibit(riemann_solver is not None and riemann_solver != 1, - "hypoelasticity requires HLL Riemann solver (riemann_solver = 1)") + self.prohibit(model_eqns is not None and model_eqns != 2, "hypoelasticity requires model_eqns = 2") + self.prohibit(riemann_solver is not None and riemann_solver != 1, "hypoelasticity requires HLL Riemann solver (riemann_solver = 1)") def check_phase_change(self): """Checks constraints on phase change parameters""" - relax = self.get('relax', 'F') == 'T' - relax_model = self.get('relax_model') - model_eqns = self.get('model_eqns') - palpha_eps = self.get('palpha_eps') - ptgalpha_eps = self.get('ptgalpha_eps') + relax = self.get("relax", "F") == "T" + relax_model = self.get("relax_model") + model_eqns = self.get("model_eqns") + palpha_eps = self.get("palpha_eps") + ptgalpha_eps = self.get("ptgalpha_eps") if not relax: return - self.prohibit(( - model_eqns not in (2, 3) or - (model_eqns == 2 and relax_model not in (5, 6)) or - (model_eqns == 3 and relax_model not in (1, 4, 5, 6))), - "phase change requires model_eqns==2 with relax_model in [5,6] or model_eqns==3 with relax_model in [1,4,5,6]") - self.prohibit(palpha_eps is not None and palpha_eps <= 0, - "palpha_eps must be positive") - self.prohibit(palpha_eps is not None and palpha_eps >= 1, - "palpha_eps must be less than 1") - self.prohibit(ptgalpha_eps is not None and ptgalpha_eps <= 0, - "ptgalpha_eps must be positive") - self.prohibit(ptgalpha_eps is not None and ptgalpha_eps >= 1, - "ptgalpha_eps must be less than 1") + self.prohibit( + (model_eqns not in (2, 3) or (model_eqns == 2 and relax_model not in (5, 6)) or (model_eqns == 3 and relax_model not in (1, 4, 5, 6))), + "phase change requires model_eqns==2 with relax_model in [5,6] or model_eqns==3 with relax_model in [1,4,5,6]", + ) + self.prohibit(palpha_eps is not None and palpha_eps <= 0, "palpha_eps must be positive") + self.prohibit(palpha_eps is not None and palpha_eps >= 1, "palpha_eps must be less than 1") + self.prohibit(ptgalpha_eps is not None and ptgalpha_eps <= 0, "ptgalpha_eps must be positive") + self.prohibit(ptgalpha_eps is not None and ptgalpha_eps >= 1, "ptgalpha_eps must be less than 1") def check_ibm(self): """Checks constraints on Immersed Boundaries parameters""" - ib = self.get('ib', 'F') == 'T' - n = self.get('n', 0) - num_ibs = self.get('num_ibs', 0) + ib = self.get("ib", "F") == "T" + n = self.get("n", 0) + num_ibs = self.get("num_ibs", 0) - self.prohibit(ib and n <= 0, - "Immersed Boundaries do not work in 1D (requires n > 0)") - self.prohibit(ib and (num_ibs <= 0 or num_ibs > 1000), - "num_ibs must be between 1 and num_patches_max (1000)") - self.prohibit(not ib and num_ibs > 0, - "num_ibs is set, but ib is not enabled") + self.prohibit(ib and n <= 0, "Immersed Boundaries do not work in 1D (requires n > 0)") + self.prohibit(ib and (num_ibs <= 0 or num_ibs > 1000), "num_ibs must be between 1 and num_patches_max (1000)") + self.prohibit(not ib and num_ibs > 0, "num_ibs is set, but ib is not enabled") def check_stiffened_eos(self): """Checks constraints on stiffened equation of state fluids parameters""" - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") + bubbles_euler = self.get("bubbles_euler", "F") == "T" if num_fluids is None: return @@ -703,70 +575,54 @@ def check_stiffened_eos(self): bub_fac = 1 if (bubbles_euler) else 0 for i in range(1, num_fluids + 1 + bub_fac): - gamma = self.get(f'fluid_pp({i})%gamma') - pi_inf = self.get(f'fluid_pp({i})%pi_inf') - cv = self.get(f'fluid_pp({i})%cv') + gamma = self.get(f"fluid_pp({i})%gamma") + pi_inf = self.get(f"fluid_pp({i})%pi_inf") + cv = self.get(f"fluid_pp({i})%cv") # Positivity checks if gamma is not None: - self.prohibit(gamma <= 0, - f"fluid_pp({i})%gamma must be positive") + self.prohibit(gamma <= 0, f"fluid_pp({i})%gamma must be positive") if pi_inf is not None: - self.prohibit(pi_inf < 0, - f"fluid_pp({i})%pi_inf must be non-negative") + self.prohibit(pi_inf < 0, f"fluid_pp({i})%pi_inf must be non-negative") if cv is not None: - self.prohibit(cv < 0, - f"fluid_pp({i})%cv must be positive") + self.prohibit(cv < 0, f"fluid_pp({i})%cv must be positive") # Model-specific support if model_eqns == 1: - self.prohibit(gamma is not None, - f"model_eqns = 1 does not support fluid_pp({i})%gamma") - self.prohibit(pi_inf is not None, - f"model_eqns = 1 does not support fluid_pp({i})%pi_inf") + self.prohibit(gamma is not None, f"model_eqns = 1 does not support fluid_pp({i})%gamma") + self.prohibit(pi_inf is not None, f"model_eqns = 1 does not support fluid_pp({i})%pi_inf") def check_surface_tension(self): """Checks constraints on surface tension""" - surface_tension = self.get('surface_tension', 'F') == 'T' - sigma = self.get('sigma') - model_eqns = self.get('model_eqns') - num_fluids = self.get('num_fluids') + surface_tension = self.get("surface_tension", "F") == "T" + sigma = self.get("sigma") + model_eqns = self.get("model_eqns") + num_fluids = self.get("num_fluids") if not surface_tension and sigma is None: return - self.prohibit(surface_tension and sigma is None, - "sigma must be set if surface_tension is enabled") - self.prohibit(surface_tension and sigma is not None and sigma < 0, - "sigma must be greater than or equal to zero") - self.prohibit(sigma is not None and not surface_tension, - "sigma is set but surface_tension is not enabled") - self.prohibit(surface_tension and model_eqns not in [2, 3], - "The surface tension model requires model_eqns = 2 or model_eqns = 3") - self.prohibit(surface_tension and num_fluids != 2, - "The surface tension model requires num_fluids = 2") + self.prohibit(surface_tension and sigma is None, "sigma must be set if surface_tension is enabled") + self.prohibit(surface_tension and sigma is not None and sigma < 0, "sigma must be greater than or equal to zero") + self.prohibit(sigma is not None and not surface_tension, "sigma is set but surface_tension is not enabled") + self.prohibit(surface_tension and model_eqns not in [2, 3], "The surface tension model requires model_eqns = 2 or model_eqns = 3") + self.prohibit(surface_tension and num_fluids != 2, "The surface tension model requires num_fluids = 2") def check_mhd(self): """Checks constraints on MHD parameters""" - mhd = self.get('mhd', 'F') == 'T' - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') - relativity = self.get('relativity', 'F') == 'T' - Bx0 = self.get('Bx0') - n = self.get('n', 0) - - self.prohibit(mhd and num_fluids != 1, - "MHD is only available for single-component flows (num_fluids = 1)") - self.prohibit(mhd and model_eqns != 2, - "MHD is only available for the 5-equation model (model_eqns = 2)") - self.prohibit(relativity and not mhd, - "relativity requires mhd to be enabled") - self.prohibit(Bx0 is not None and not mhd, - "Bx0 must not be set if MHD is not enabled") - self.prohibit(mhd and n is not None and n == 0 and Bx0 is None, - "Bx0 must be set in 1D MHD simulations") - self.prohibit(mhd and n is not None and n > 0 and Bx0 is not None, - "Bx0 must not be set in 2D/3D MHD simulations") + mhd = self.get("mhd", "F") == "T" + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") + relativity = self.get("relativity", "F") == "T" + Bx0 = self.get("Bx0") + n = self.get("n", 0) + + self.prohibit(mhd and num_fluids != 1, "MHD is only available for single-component flows (num_fluids = 1)") + self.prohibit(mhd and model_eqns != 2, "MHD is only available for the 5-equation model (model_eqns = 2)") + self.prohibit(relativity and not mhd, "relativity requires mhd to be enabled") + self.prohibit(Bx0 is not None and not mhd, "Bx0 must not be set if MHD is not enabled") + self.prohibit(mhd and n is not None and n == 0 and Bx0 is None, "Bx0 must be set in 1D MHD simulations") + self.prohibit(mhd and n is not None and n > 0 and Bx0 is not None, "Bx0 must not be set in 2D/3D MHD simulations") # =================================================================== # Simulation-Specific Checks @@ -774,358 +630,280 @@ def check_mhd(self): def check_riemann_solver(self): """Checks constraints on Riemann solver (simulation only)""" - riemann_solver = self.get('riemann_solver') - model_eqns = self.get('model_eqns') - wave_speeds = self.get('wave_speeds') - avg_state = self.get('avg_state') - low_Mach = self.get('low_Mach', 0) - cyl_coord = self.get('cyl_coord', 'F') == 'T' - viscous = self.get('viscous', 'F') == 'T' + riemann_solver = self.get("riemann_solver") + model_eqns = self.get("model_eqns") + wave_speeds = self.get("wave_speeds") + avg_state = self.get("avg_state") + low_Mach = self.get("low_Mach", 0) + cyl_coord = self.get("cyl_coord", "F") == "T" + viscous = self.get("viscous", "F") == "T" if riemann_solver is None: return - self.prohibit(riemann_solver < 1 or riemann_solver > 5, - "riemann_solver must be 1, 2, 3, 4 or 5") - self.prohibit(riemann_solver != 2 and model_eqns == 3, - "6-equation model (model_eqns = 3) requires riemann_solver = 2 (HLLC)") - self.prohibit(wave_speeds is not None and wave_speeds not in [1, 2], - "wave_speeds must be 1 or 2") - self.prohibit(riemann_solver == 3 and wave_speeds is not None, - "Exact Riemann (riemann_solver = 3) does not support wave_speeds") - self.prohibit(avg_state is not None and avg_state not in [1, 2], - "avg_state must be 1 or 2") - self.prohibit(riemann_solver not in [3, 5] and wave_speeds is None, - "wave_speeds must be set if riemann_solver != 3,5") - self.prohibit(riemann_solver not in [3, 5] and avg_state is None, - "avg_state must be set if riemann_solver != 3,5") - self.prohibit(low_Mach not in [0, 1, 2], - "low_Mach must be 0, 1, or 2") - self.prohibit(riemann_solver != 2 and low_Mach == 2, - "low_Mach = 2 requires riemann_solver = 2") - self.prohibit(low_Mach != 0 and model_eqns not in [2, 3], - "low_Mach = 1 or 2 requires model_eqns = 2 or 3") - self.prohibit(riemann_solver == 5 and cyl_coord and viscous, - "Lax Friedrichs with cylindrical viscous flux not supported") + self.prohibit(riemann_solver < 1 or riemann_solver > 5, "riemann_solver must be 1, 2, 3, 4 or 5") + self.prohibit(riemann_solver != 2 and model_eqns == 3, "6-equation model (model_eqns = 3) requires riemann_solver = 2 (HLLC)") + self.prohibit(wave_speeds is not None and wave_speeds not in [1, 2], "wave_speeds must be 1 or 2") + self.prohibit(riemann_solver == 3 and wave_speeds is not None, "Exact Riemann (riemann_solver = 3) does not support wave_speeds") + self.prohibit(avg_state is not None and avg_state not in [1, 2], "avg_state must be 1 or 2") + self.prohibit(riemann_solver not in [3, 5] and wave_speeds is None, "wave_speeds must be set if riemann_solver != 3,5") + self.prohibit(riemann_solver not in [3, 5] and avg_state is None, "avg_state must be set if riemann_solver != 3,5") + self.prohibit(low_Mach not in [0, 1, 2], "low_Mach must be 0, 1, or 2") + self.prohibit(riemann_solver != 2 and low_Mach == 2, "low_Mach = 2 requires riemann_solver = 2") + self.prohibit(low_Mach != 0 and model_eqns not in [2, 3], "low_Mach = 1 or 2 requires model_eqns = 2 or 3") + self.prohibit(riemann_solver == 5 and cyl_coord and viscous, "Lax Friedrichs with cylindrical viscous flux not supported") def check_time_stepping(self): """Checks time stepping parameters (simulation/post-process)""" - cfl_dt = self.get('cfl_dt', 'F') == 'T' - cfl_adap_dt = self.get('cfl_adap_dt', 'F') == 'T' - adap_dt = self.get('adap_dt', 'F') == 'T' - time_stepper = self.get('time_stepper') + cfl_dt = self.get("cfl_dt", "F") == "T" + cfl_adap_dt = self.get("cfl_adap_dt", "F") == "T" + adap_dt = self.get("adap_dt", "F") == "T" + time_stepper = self.get("time_stepper") # Check time_stepper bounds - self.prohibit(time_stepper is not None and (time_stepper < 1 or time_stepper > 3), - "time_stepper must be 1, 2, or 3") + self.prohibit(time_stepper is not None and (time_stepper < 1 or time_stepper > 3), "time_stepper must be 1, 2, or 3") # CFL-based variable dt modes (use t_stop/t_save for termination) # Note: adap_dt is NOT included here - it uses t_step_* for termination variable_dt = cfl_dt or cfl_adap_dt # dt validation (applies to all modes if dt is set) - dt = self.get('dt') - self.prohibit(dt is not None and dt <= 0, - "dt must be positive") + dt = self.get("dt") + self.prohibit(dt is not None and dt <= 0, "dt must be positive") if variable_dt: - cfl_target = self.get('cfl_target') - t_stop = self.get('t_stop') - t_save = self.get('t_save') - n_start = self.get('n_start') - - self.prohibit(cfl_target is not None and (cfl_target <= 0 or cfl_target > 1), - "cfl_target must be in (0, 1]") - self.prohibit(t_stop is not None and t_stop <= 0, - "t_stop must be positive") - self.prohibit(t_save is not None and t_save <= 0, - "t_save must be positive") - self.prohibit(t_save is not None and t_stop is not None and t_save > t_stop, - "t_save must be <= t_stop") - self.prohibit(n_start is not None and n_start < 0, - "n_start must be non-negative") + cfl_target = self.get("cfl_target") + t_stop = self.get("t_stop") + t_save = self.get("t_save") + n_start = self.get("n_start") + + self.prohibit(cfl_target is not None and (cfl_target <= 0 or cfl_target > 1), "cfl_target must be in (0, 1]") + self.prohibit(t_stop is not None and t_stop <= 0, "t_stop must be positive") + self.prohibit(t_save is not None and t_save <= 0, "t_save must be positive") + self.prohibit(t_save is not None and t_stop is not None and t_save > t_stop, "t_save must be <= t_stop") + self.prohibit(n_start is not None and n_start < 0, "n_start must be non-negative") # t_step_* validation (applies to fixed and adap_dt modes) - t_step_start = self.get('t_step_start') - t_step_stop = self.get('t_step_stop') - t_step_save = self.get('t_step_save') - - self.prohibit(t_step_start is not None and t_step_start < 0, - "t_step_start must be non-negative") - self.prohibit(t_step_stop is not None and t_step_stop < 0, - "t_step_stop must be non-negative") - self.prohibit(t_step_stop is not None and t_step_start is not None and t_step_stop <= t_step_start, - "t_step_stop must be > t_step_start") - self.prohibit(t_step_save is not None and t_step_save <= 0, - "t_step_save must be positive") - self.prohibit(t_step_save is not None and t_step_stop is not None and t_step_start is not None and - t_step_save > t_step_stop - t_step_start, - "t_step_save must be <= (t_step_stop - t_step_start)") + t_step_start = self.get("t_step_start") + t_step_stop = self.get("t_step_stop") + t_step_save = self.get("t_step_save") + + self.prohibit(t_step_start is not None and t_step_start < 0, "t_step_start must be non-negative") + self.prohibit(t_step_stop is not None and t_step_stop < 0, "t_step_stop must be non-negative") + self.prohibit(t_step_stop is not None and t_step_start is not None and t_step_stop <= t_step_start, "t_step_stop must be > t_step_start") + self.prohibit(t_step_save is not None and t_step_save <= 0, "t_step_save must be positive") + self.prohibit( + t_step_save is not None and t_step_stop is not None and t_step_start is not None and t_step_save > t_step_stop - t_step_start, "t_step_save must be <= (t_step_stop - t_step_start)" + ) if not variable_dt: # dt is required in pure fixed dt mode (not cfl_dt, not cfl_adap_dt) # adap_dt mode uses dt as initial value, so it's optional - uses_fixed_stepping = self.is_set('t_step_start') or self.is_set('t_step_stop') - self.prohibit(uses_fixed_stepping and not adap_dt and not self.is_set('dt'), - "dt must be set when using fixed time stepping (t_step_start/t_step_stop)") + uses_fixed_stepping = self.is_set("t_step_start") or self.is_set("t_step_stop") + self.prohibit(uses_fixed_stepping and not adap_dt and not self.is_set("dt"), "dt must be set when using fixed time stepping (t_step_start/t_step_stop)") def check_finite_difference(self): """Checks constraints on finite difference parameters""" - fd_order = self.get('fd_order') + fd_order = self.get("fd_order") if fd_order is None: return - self.prohibit(fd_order not in [1, 2, 4], - "fd_order must be 1, 2, or 4") + self.prohibit(fd_order not in [1, 2, 4], "fd_order must be 1, 2, or 4") def check_weno_simulation(self): """Checks WENO-specific constraints for simulation""" - weno_order = self.get('weno_order') - weno_eps = self.get('weno_eps') - wenoz = self.get('wenoz', 'F') == 'T' - wenoz_q = self.get('wenoz_q') - teno = self.get('teno', 'F') == 'T' - teno_CT = self.get('teno_CT') - mapped_weno = self.get('mapped_weno', 'F') == 'T' - mp_weno = self.get('mp_weno', 'F') == 'T' - weno_avg = self.get('weno_avg', 'F') == 'T' - model_eqns = self.get('model_eqns') + weno_order = self.get("weno_order") + weno_eps = self.get("weno_eps") + wenoz = self.get("wenoz", "F") == "T" + wenoz_q = self.get("wenoz_q") + teno = self.get("teno", "F") == "T" + teno_CT = self.get("teno_CT") + mapped_weno = self.get("mapped_weno", "F") == "T" + mp_weno = self.get("mp_weno", "F") == "T" + weno_avg = self.get("weno_avg", "F") == "T" + model_eqns = self.get("model_eqns") # Check for multiple WENO schemes (regardless of weno_order being set) num_schemes = sum([mapped_weno, wenoz, teno]) - self.prohibit(num_schemes >= 2, - "Only one of mapped_weno, wenoz, or teno can be set to true") + self.prohibit(num_schemes >= 2, "Only one of mapped_weno, wenoz, or teno can be set to true") # Early return if weno_order not set (other checks need it) if weno_order is None: return - self.prohibit(weno_order != 1 and weno_eps is None, - "weno_order != 1 requires weno_eps to be set. A typical value is 1e-6") - self.prohibit(weno_eps is not None and weno_eps <= 0, - "weno_eps must be positive. A typical value is 1e-6") - self.prohibit(wenoz and weno_order == 7 and wenoz_q is None, - "wenoz at 7th order requires wenoz_q to be set (should be 2, 3, or 4)") - self.prohibit(wenoz and weno_order == 7 and wenoz_q is not None and wenoz_q not in [2, 3, 4], - "wenoz_q must be either 2, 3, or 4)") - self.prohibit(teno and teno_CT is None, - "teno requires teno_CT to be set. A typical value is 1e-6") - self.prohibit(teno and teno_CT is not None and teno_CT <= 0, - "teno_CT must be positive. A typical value is 1e-6") - - self.prohibit(weno_order == 1 and mapped_weno, - "mapped_weno is not compatible with weno_order = 1") - self.prohibit(weno_order == 1 and wenoz, - "wenoz is not compatible with weno_order = 1") - self.prohibit(weno_order in [1, 3] and teno, - "teno requires weno_order = 5 or 7") - self.prohibit(weno_order != 5 and mp_weno, - "mp_weno requires weno_order = 5") - self.prohibit(model_eqns == 1 and weno_avg, - "weno_avg is not compatible with model_eqns = 1") + self.prohibit(weno_order != 1 and weno_eps is None, "weno_order != 1 requires weno_eps to be set. A typical value is 1e-6") + self.prohibit(weno_eps is not None and weno_eps <= 0, "weno_eps must be positive. A typical value is 1e-6") + self.prohibit(wenoz and weno_order == 7 and wenoz_q is None, "wenoz at 7th order requires wenoz_q to be set (should be 2, 3, or 4)") + self.prohibit(wenoz and weno_order == 7 and wenoz_q is not None and wenoz_q not in [2, 3, 4], "wenoz_q must be either 2, 3, or 4)") + self.prohibit(teno and teno_CT is None, "teno requires teno_CT to be set. A typical value is 1e-6") + self.prohibit(teno and teno_CT is not None and teno_CT <= 0, "teno_CT must be positive. A typical value is 1e-6") + + self.prohibit(weno_order == 1 and mapped_weno, "mapped_weno is not compatible with weno_order = 1") + self.prohibit(weno_order == 1 and wenoz, "wenoz is not compatible with weno_order = 1") + self.prohibit(weno_order in [1, 3] and teno, "teno requires weno_order = 5 or 7") + self.prohibit(weno_order != 5 and mp_weno, "mp_weno requires weno_order = 5") + self.prohibit(model_eqns == 1 and weno_avg, "weno_avg is not compatible with model_eqns = 1") def check_muscl_simulation(self): """Checks MUSCL-specific constraints for simulation""" - muscl_order = self.get('muscl_order') - muscl_lim = self.get('muscl_lim') + muscl_order = self.get("muscl_order") + muscl_lim = self.get("muscl_lim") if muscl_order is None: return - self.prohibit(muscl_order == 2 and muscl_lim is None, - "muscl_lim must be defined if using muscl_order = 2") - self.prohibit(muscl_lim is not None and (muscl_lim < 1 or muscl_lim > 5), - "muscl_lim must be 1, 2, 3, 4, or 5") + self.prohibit(muscl_order == 2 and muscl_lim is None, "muscl_lim must be defined if using muscl_order = 2") + self.prohibit(muscl_lim is not None and (muscl_lim < 1 or muscl_lim > 5), "muscl_lim must be 1, 2, 3, 4, or 5") def check_model_eqns_simulation(self): """Checks model equation constraints specific to simulation""" - model_eqns = self.get('model_eqns') - avg_state = self.get('avg_state') - wave_speeds = self.get('wave_speeds') + model_eqns = self.get("model_eqns") + avg_state = self.get("avg_state") + wave_speeds = self.get("wave_speeds") if model_eqns != 3: return - self.prohibit(avg_state is not None and avg_state != 2, - "6-equation model (model_eqns = 3) requires avg_state = 2") - self.prohibit(wave_speeds is not None and wave_speeds != 1, - "6-equation model (model_eqns = 3) requires wave_speeds = 1") + self.prohibit(avg_state is not None and avg_state != 2, "6-equation model (model_eqns = 3) requires avg_state = 2") + self.prohibit(wave_speeds is not None and wave_speeds != 1, "6-equation model (model_eqns = 3) requires wave_speeds = 1") def check_bubbles_euler_simulation(self): """Checks bubble constraints specific to simulation""" - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - bubbles_lagrange = self.get('bubbles_lagrange', 'F') == 'T' - riemann_solver = self.get('riemann_solver') - avg_state = self.get('avg_state') - model_eqns = self.get('model_eqns') - bubble_model = self.get('bubble_model') + bubbles_euler = self.get("bubbles_euler", "F") == "T" + bubbles_lagrange = self.get("bubbles_lagrange", "F") == "T" + riemann_solver = self.get("riemann_solver") + avg_state = self.get("avg_state") + model_eqns = self.get("model_eqns") + bubble_model = self.get("bubble_model") - self.prohibit(bubbles_euler and bubbles_lagrange, - "Activate only one of the bubble subgrid models (bubbles_euler or bubbles_lagrange)") + self.prohibit(bubbles_euler and bubbles_lagrange, "Activate only one of the bubble subgrid models (bubbles_euler or bubbles_lagrange)") if not bubbles_euler: return - self.prohibit(riemann_solver is not None and riemann_solver != 2, - "Bubble modeling requires HLLC Riemann solver (riemann_solver = 2)") - self.prohibit(avg_state is not None and avg_state != 2, - "Bubble modeling requires arithmetic average (avg_state = 2)") - self.prohibit(model_eqns == 2 and bubble_model == 1, - "The 5-equation bubbly flow model does not support bubble_model = 1 (Gilmore)") + self.prohibit(riemann_solver is not None and riemann_solver != 2, "Bubble modeling requires HLLC Riemann solver (riemann_solver = 2)") + self.prohibit(avg_state is not None and avg_state != 2, "Bubble modeling requires arithmetic average (avg_state = 2)") + self.prohibit(model_eqns == 2 and bubble_model == 1, "The 5-equation bubbly flow model does not support bubble_model = 1 (Gilmore)") def check_body_forces(self): """Checks constraints on body forces parameters""" - for dir in ['x', 'y', 'z']: - bf = self.get(f'bf_{dir}', 'F') == 'T' + for dir in ["x", "y", "z"]: + bf = self.get(f"bf_{dir}", "F") == "T" if not bf: continue - self.prohibit(self.get(f'k_{dir}') is None, - f"k_{dir} must be specified if bf_{dir} is true") - self.prohibit(self.get(f'w_{dir}') is None, - f"w_{dir} must be specified if bf_{dir} is true") - self.prohibit(self.get(f'p_{dir}') is None, - f"p_{dir} must be specified if bf_{dir} is true") - self.prohibit(self.get(f'g_{dir}') is None, - f"g_{dir} must be specified if bf_{dir} is true") + self.prohibit(self.get(f"k_{dir}") is None, f"k_{dir} must be specified if bf_{dir} is true") + self.prohibit(self.get(f"w_{dir}") is None, f"w_{dir} must be specified if bf_{dir} is true") + self.prohibit(self.get(f"p_{dir}") is None, f"p_{dir} must be specified if bf_{dir} is true") + self.prohibit(self.get(f"g_{dir}") is None, f"g_{dir} must be specified if bf_{dir} is true") def check_viscosity(self): """Checks constraints on viscosity parameters""" - viscous = self.get('viscous', 'F') == 'T' - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') - weno_order = self.get('weno_order') - weno_avg = self.get('weno_avg', 'F') == 'T' - igr = self.get('igr', 'F') == 'T' + viscous = self.get("viscous", "F") == "T" + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") + weno_order = self.get("weno_order") + weno_avg = self.get("weno_avg", "F") == "T" + igr = self.get("igr", "F") == "T" # If num_fluids is not set, check at least fluid 1 (for model_eqns=1) if num_fluids is None: num_fluids = 1 for i in range(1, num_fluids + 1): - Re1 = self.get(f'fluid_pp({i})%Re(1)') - Re2 = self.get(f'fluid_pp({i})%Re(2)') + Re1 = self.get(f"fluid_pp({i})%Re(1)") + Re2 = self.get(f"fluid_pp({i})%Re(2)") for j, Re_val in [(1, Re1), (2, Re2)]: if Re_val is not None: - self.prohibit(Re_val <= 0, - f"fluid_pp({i})%Re({j}) must be positive") - self.prohibit(model_eqns == 1, - f"model_eqns = 1 does not support fluid_pp({i})%Re({j})") + self.prohibit(Re_val <= 0, f"fluid_pp({i})%Re({j}) must be positive") + self.prohibit(model_eqns == 1, f"model_eqns = 1 does not support fluid_pp({i})%Re({j})") if not igr: - self.prohibit(weno_order == 1 and not weno_avg, - f"weno_order = 1 without weno_avg does not support fluid_pp({i})%Re({j})") - self.prohibit(not viscous, - f"Re({j}) is specified, but viscous is not set to true") + self.prohibit(weno_order == 1 and not weno_avg, f"weno_order = 1 without weno_avg does not support fluid_pp({i})%Re({j})") + self.prohibit(not viscous, f"Re({j}) is specified, but viscous is not set to true") # Check Re(1) requirement - self.prohibit(Re1 is None and viscous, - f"viscous is set to true, but fluid_pp({i})%Re(1) is not specified") + self.prohibit(Re1 is None and viscous, f"viscous is set to true, but fluid_pp({i})%Re(1) is not specified") def check_mhd_simulation(self): """Checks MHD constraints specific to simulation""" - mhd = self.get('mhd', 'F') == 'T' - riemann_solver = self.get('riemann_solver') - relativity = self.get('relativity', 'F') == 'T' - hyper_cleaning = self.get('hyper_cleaning', 'F') == 'T' - wave_speeds = self.get('wave_speeds') - n = self.get('n', 0) - - self.prohibit(mhd and riemann_solver is not None and riemann_solver not in [1, 4], - "MHD simulations require riemann_solver = 1 (HLL) or riemann_solver = 4 (HLLD)") - self.prohibit(mhd and wave_speeds is not None and wave_speeds == 2, - "MHD requires wave_speeds = 1") - self.prohibit(riemann_solver == 4 and not mhd, - "HLLD (riemann_solver = 4) is only available for MHD simulations") - self.prohibit(riemann_solver == 4 and relativity, - "HLLD is not available for RMHD (relativity)") - self.prohibit(hyper_cleaning and not mhd, - "Hyperbolic cleaning requires mhd to be enabled") - self.prohibit(hyper_cleaning and n is not None and n == 0, - "Hyperbolic cleaning is not supported for 1D simulations") - - - def check_igr_simulation(self): # pylint: disable=too-many-locals + mhd = self.get("mhd", "F") == "T" + riemann_solver = self.get("riemann_solver") + relativity = self.get("relativity", "F") == "T" + hyper_cleaning = self.get("hyper_cleaning", "F") == "T" + wave_speeds = self.get("wave_speeds") + n = self.get("n", 0) + + self.prohibit(mhd and riemann_solver is not None and riemann_solver not in [1, 4], "MHD simulations require riemann_solver = 1 (HLL) or riemann_solver = 4 (HLLD)") + self.prohibit(mhd and wave_speeds is not None and wave_speeds == 2, "MHD requires wave_speeds = 1") + self.prohibit(riemann_solver == 4 and not mhd, "HLLD (riemann_solver = 4) is only available for MHD simulations") + self.prohibit(riemann_solver == 4 and relativity, "HLLD is not available for RMHD (relativity)") + self.prohibit(hyper_cleaning and not mhd, "Hyperbolic cleaning requires mhd to be enabled") + self.prohibit(hyper_cleaning and n is not None and n == 0, "Hyperbolic cleaning is not supported for 1D simulations") + + def check_igr_simulation(self): """Checks IGR constraints specific to simulation""" - igr = self.get('igr', 'F') == 'T' + igr = self.get("igr", "F") == "T" if not igr: return - num_igr_iters = self.get('num_igr_iters') - num_igr_warm_start_iters = self.get('num_igr_warm_start_iters') - igr_iter_solver = self.get('igr_iter_solver') - alf_factor = self.get('alf_factor') - model_eqns = self.get('model_eqns') - ib = self.get('ib', 'F') == 'T' - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - bubbles_lagrange = self.get('bubbles_lagrange', 'F') == 'T' - alt_soundspeed = self.get('alt_soundspeed', 'F') == 'T' - surface_tension = self.get('surface_tension', 'F') == 'T' - hypoelasticity = self.get('hypoelasticity', 'F') == 'T' - acoustic_source = self.get('acoustic_source', 'F') == 'T' - relax = self.get('relax', 'F') == 'T' - mhd = self.get('mhd', 'F') == 'T' - hyperelasticity = self.get('hyperelasticity', 'F') == 'T' - cyl_coord = self.get('cyl_coord', 'F') == 'T' - probe_wrt = self.get('probe_wrt', 'F') == 'T' - - self.prohibit(num_igr_iters is not None and num_igr_iters < 0, - "num_igr_iters must be greater than or equal to 0") - self.prohibit(num_igr_warm_start_iters is not None and num_igr_warm_start_iters < 0, - "num_igr_warm_start_iters must be greater than or equal to 0") - self.prohibit(igr_iter_solver is not None and igr_iter_solver not in [1, 2], - "igr_iter_solver must be 1 or 2") - self.prohibit(alf_factor is not None and alf_factor < 0, - "alf_factor must be non-negative") - self.prohibit(model_eqns is not None and model_eqns != 2, - "IGR only supports model_eqns = 2") - self.prohibit(ib, - "IGR does not support the immersed boundary method") - self.prohibit(bubbles_euler, - "IGR does not support Euler-Euler bubble models") - self.prohibit(bubbles_lagrange, - "IGR does not support Euler-Lagrange bubble models") - self.prohibit(alt_soundspeed, - "IGR does not support alt_soundspeed = T") - self.prohibit(surface_tension, - "IGR does not support surface tension") - self.prohibit(hypoelasticity, - "IGR does not support hypoelasticity") - self.prohibit(acoustic_source, - "IGR does not support acoustic sources") - self.prohibit(relax, - "IGR does not support phase change") - self.prohibit(mhd, - "IGR does not support magnetohydrodynamics") - self.prohibit(hyperelasticity, - "IGR does not support hyperelasticity") - self.prohibit(cyl_coord, - "IGR does not support cylindrical or axisymmetric coordinates") - self.prohibit(probe_wrt, - "IGR does not support probe writes") + num_igr_iters = self.get("num_igr_iters") + num_igr_warm_start_iters = self.get("num_igr_warm_start_iters") + igr_iter_solver = self.get("igr_iter_solver") + alf_factor = self.get("alf_factor") + model_eqns = self.get("model_eqns") + ib = self.get("ib", "F") == "T" + bubbles_euler = self.get("bubbles_euler", "F") == "T" + bubbles_lagrange = self.get("bubbles_lagrange", "F") == "T" + alt_soundspeed = self.get("alt_soundspeed", "F") == "T" + surface_tension = self.get("surface_tension", "F") == "T" + hypoelasticity = self.get("hypoelasticity", "F") == "T" + acoustic_source = self.get("acoustic_source", "F") == "T" + relax = self.get("relax", "F") == "T" + mhd = self.get("mhd", "F") == "T" + hyperelasticity = self.get("hyperelasticity", "F") == "T" + cyl_coord = self.get("cyl_coord", "F") == "T" + probe_wrt = self.get("probe_wrt", "F") == "T" + + self.prohibit(num_igr_iters is not None and num_igr_iters < 0, "num_igr_iters must be greater than or equal to 0") + self.prohibit(num_igr_warm_start_iters is not None and num_igr_warm_start_iters < 0, "num_igr_warm_start_iters must be greater than or equal to 0") + self.prohibit(igr_iter_solver is not None and igr_iter_solver not in [1, 2], "igr_iter_solver must be 1 or 2") + self.prohibit(alf_factor is not None and alf_factor < 0, "alf_factor must be non-negative") + self.prohibit(model_eqns is not None and model_eqns != 2, "IGR only supports model_eqns = 2") + self.prohibit(ib, "IGR does not support the immersed boundary method") + self.prohibit(bubbles_euler, "IGR does not support Euler-Euler bubble models") + self.prohibit(bubbles_lagrange, "IGR does not support Euler-Lagrange bubble models") + self.prohibit(alt_soundspeed, "IGR does not support alt_soundspeed = T") + self.prohibit(surface_tension, "IGR does not support surface tension") + self.prohibit(hypoelasticity, "IGR does not support hypoelasticity") + self.prohibit(acoustic_source, "IGR does not support acoustic sources") + self.prohibit(relax, "IGR does not support phase change") + self.prohibit(mhd, "IGR does not support magnetohydrodynamics") + self.prohibit(hyperelasticity, "IGR does not support hyperelasticity") + self.prohibit(cyl_coord, "IGR does not support cylindrical or axisymmetric coordinates") + self.prohibit(probe_wrt, "IGR does not support probe writes") # Check BCs - IGR does not support characteristic BCs # Characteristic BCs are BC_CHAR_SLIP_WALL (-5) through BC_CHAR_SUP_OUTFLOW (-12) - for dir in ['x', 'y', 'z']: - for bound in ['beg', 'end']: - bc = self.get(f'bc_{dir}%{bound}') + for dir in ["x", "y", "z"]: + for bound in ["beg", "end"]: + bc = self.get(f"bc_{dir}%{bound}") if bc is not None: - self.prohibit(-12 <= bc <= -5, - f"Characteristic boundary condition bc_{dir}%{bound} is not compatible with IGR") + self.prohibit(-12 <= bc <= -5, f"Characteristic boundary condition bc_{dir}%{bound} is not compatible with IGR") - def check_acoustic_source(self): # pylint: disable=too-many-locals,too-many-branches,too-many-statements + def check_acoustic_source(self): """Checks acoustic source parameters (simulation)""" - acoustic_source = self.get('acoustic_source', 'F') == 'T' + acoustic_source = self.get("acoustic_source", "F") == "T" if not acoustic_source: return - num_source = self.get('num_source') - n = self.get('n', 0) - p = self.get('p', 0) - cyl_coord = self.get('cyl_coord', 'F') == 'T' + num_source = self.get("num_source") + n = self.get("n", 0) + p = self.get("p", 0) + cyl_coord = self.get("cyl_coord", "F") == "T" # Determine dimensionality if n is not None and n == 0: @@ -1135,10 +913,8 @@ def check_acoustic_source(self): # pylint: disable=too-many-locals,too-many-bra else: dim = 3 - self.prohibit(num_source is None, - "num_source must be specified for acoustic_source") - self.prohibit(num_source is not None and num_source < 0, - "num_source must be non-negative") + self.prohibit(num_source is None, "num_source must be specified for acoustic_source") + self.prohibit(num_source is not None and num_source < 0, "num_source must be non-negative") if num_source is None or num_source <= 0: return @@ -1147,317 +923,243 @@ def check_acoustic_source(self): # pylint: disable=too-many-locals,too-many-bra for j in range(1, num_source + 1): jstr = str(j) - support = self.get(f'acoustic({j})%support') - loc = [self.get(f'acoustic({j})%loc({i})') for i in range(1, 4)] - mag = self.get(f'acoustic({j})%mag') - pulse = self.get(f'acoustic({j})%pulse') - frequency = self.get(f'acoustic({j})%frequency') - wavelength = self.get(f'acoustic({j})%wavelength') - gauss_sigma_time = self.get(f'acoustic({j})%gauss_sigma_time') - gauss_sigma_dist = self.get(f'acoustic({j})%gauss_sigma_dist') - bb_num_freq = self.get(f'acoustic({j})%bb_num_freq') - bb_bandwidth = self.get(f'acoustic({j})%bb_bandwidth') - bb_lowest_freq = self.get(f'acoustic({j})%bb_lowest_freq') - npulse = self.get(f'acoustic({j})%npulse') - dipole = self.get(f'acoustic({j})%dipole', 'F') == 'T' - dir_val = self.get(f'acoustic({j})%dir') - delay = self.get(f'acoustic({j})%delay') - length = self.get(f'acoustic({j})%length') - height = self.get(f'acoustic({j})%height') - foc_length = self.get(f'acoustic({j})%foc_length') - aperture = self.get(f'acoustic({j})%aperture') - num_elements = self.get(f'acoustic({j})%num_elements') - element_on = self.get(f'acoustic({j})%element_on') - element_spacing_angle = self.get(f'acoustic({j})%element_spacing_angle') - element_polygon_ratio = self.get(f'acoustic({j})%element_polygon_ratio') - - self.prohibit(support is None, - f"acoustic({jstr})%support must be specified for acoustic_source") + support = self.get(f"acoustic({j})%support") + loc = [self.get(f"acoustic({j})%loc({i})") for i in range(1, 4)] + mag = self.get(f"acoustic({j})%mag") + pulse = self.get(f"acoustic({j})%pulse") + frequency = self.get(f"acoustic({j})%frequency") + wavelength = self.get(f"acoustic({j})%wavelength") + gauss_sigma_time = self.get(f"acoustic({j})%gauss_sigma_time") + gauss_sigma_dist = self.get(f"acoustic({j})%gauss_sigma_dist") + bb_num_freq = self.get(f"acoustic({j})%bb_num_freq") + bb_bandwidth = self.get(f"acoustic({j})%bb_bandwidth") + bb_lowest_freq = self.get(f"acoustic({j})%bb_lowest_freq") + npulse = self.get(f"acoustic({j})%npulse") + dipole = self.get(f"acoustic({j})%dipole", "F") == "T" + dir_val = self.get(f"acoustic({j})%dir") + delay = self.get(f"acoustic({j})%delay") + length = self.get(f"acoustic({j})%length") + height = self.get(f"acoustic({j})%height") + foc_length = self.get(f"acoustic({j})%foc_length") + aperture = self.get(f"acoustic({j})%aperture") + num_elements = self.get(f"acoustic({j})%num_elements") + element_on = self.get(f"acoustic({j})%element_on") + element_spacing_angle = self.get(f"acoustic({j})%element_spacing_angle") + element_polygon_ratio = self.get(f"acoustic({j})%element_polygon_ratio") + + self.prohibit(support is None, f"acoustic({jstr})%support must be specified for acoustic_source") # Dimension-specific support checks (only if support was specified) if support is not None: if dim == 1: - self.prohibit(support != 1, - f"Only acoustic({jstr})%support = 1 is allowed for 1D simulations") - self.prohibit(support == 1 and loc[0] is None, - f"acoustic({jstr})%loc(1) must be specified for support = 1") + self.prohibit(support != 1, f"Only acoustic({jstr})%support = 1 is allowed for 1D simulations") + self.prohibit(support == 1 and loc[0] is None, f"acoustic({jstr})%loc(1) must be specified for support = 1") elif dim == 2: if cyl_coord: - self.prohibit(support not in [2, 6, 10], - f"Only acoustic({jstr})%support = 2, 6, or 10 is allowed for 2D axisymmetric") + self.prohibit(support not in [2, 6, 10], f"Only acoustic({jstr})%support = 2, 6, or 10 is allowed for 2D axisymmetric") else: - self.prohibit(support not in [2, 5, 6, 9, 10], - f"Only acoustic({jstr})%support = 2, 5, 6, 9, or 10 is allowed for 2D") + self.prohibit(support not in [2, 5, 6, 9, 10], f"Only acoustic({jstr})%support = 2, 5, 6, 9, or 10 is allowed for 2D") if support in [2, 5, 6, 9, 10]: - self.prohibit(loc[0] is None or loc[1] is None, - f"acoustic({jstr})%loc(1:2) must be specified for support = {support}") + self.prohibit(loc[0] is None or loc[1] is None, f"acoustic({jstr})%loc(1:2) must be specified for support = {support}") elif dim == 3: - self.prohibit(support not in [3, 7, 11], - f"Only acoustic({jstr})%support = 3, 7, or 11 is allowed for 3D") - self.prohibit(cyl_coord, - "Acoustic source is not supported in 3D cylindrical simulations") + self.prohibit(support not in [3, 7, 11], f"Only acoustic({jstr})%support = 3, 7, or 11 is allowed for 3D") + self.prohibit(cyl_coord, "Acoustic source is not supported in 3D cylindrical simulations") if support == 3: - self.prohibit(loc[0] is None or loc[1] is None, - f"acoustic({jstr})%loc(1:2) must be specified for support = 3") + self.prohibit(loc[0] is None or loc[1] is None, f"acoustic({jstr})%loc(1:2) must be specified for support = 3") elif support in [7, 11]: - self.prohibit(loc[0] is None or loc[1] is None or loc[2] is None, - f"acoustic({jstr})%loc(1:3) must be specified for support = {support}") + self.prohibit(loc[0] is None or loc[1] is None or loc[2] is None, f"acoustic({jstr})%loc(1:3) must be specified for support = {support}") # Pulse parameters - self.prohibit(mag is None, - f"acoustic({jstr})%mag must be specified") - self.prohibit(pulse is None, - f"acoustic({jstr})%pulse must be specified") - self.prohibit(pulse is not None and pulse not in [1, 2, 3, 4], - f"Only acoustic({jstr})%pulse = 1, 2, 3, or 4 is allowed") + self.prohibit(mag is None, f"acoustic({jstr})%mag must be specified") + self.prohibit(pulse is None, f"acoustic({jstr})%pulse must be specified") + self.prohibit(pulse is not None and pulse not in [1, 2, 3, 4], f"Only acoustic({jstr})%pulse = 1, 2, 3, or 4 is allowed") # Pulse-specific requirements if pulse in [1, 3]: freq_set = frequency is not None wave_set = wavelength is not None - self.prohibit(freq_set == wave_set, - f"One and only one of acoustic({jstr})%frequency or wavelength must be specified for pulse = {pulse}") + self.prohibit(freq_set == wave_set, f"One and only one of acoustic({jstr})%frequency or wavelength must be specified for pulse = {pulse}") # Physics: frequency and wavelength must be positive - self.prohibit(frequency is not None and frequency <= 0, - f"acoustic({jstr})%frequency must be positive") - self.prohibit(wavelength is not None and wavelength <= 0, - f"acoustic({jstr})%wavelength must be positive") + self.prohibit(frequency is not None and frequency <= 0, f"acoustic({jstr})%frequency must be positive") + self.prohibit(wavelength is not None and wavelength <= 0, f"acoustic({jstr})%wavelength must be positive") if pulse == 2: time_set = gauss_sigma_time is not None dist_set = gauss_sigma_dist is not None - self.prohibit(time_set == dist_set, - f"One and only one of acoustic({jstr})%gauss_sigma_time or gauss_sigma_dist must be specified for pulse = 2") - self.prohibit(delay is None, - f"acoustic({jstr})%delay must be specified for pulse = 2 (Gaussian)") + self.prohibit(time_set == dist_set, f"One and only one of acoustic({jstr})%gauss_sigma_time or gauss_sigma_dist must be specified for pulse = 2") + self.prohibit(delay is None, f"acoustic({jstr})%delay must be specified for pulse = 2 (Gaussian)") # Physics: gaussian parameters must be positive - self.prohibit(gauss_sigma_time is not None and gauss_sigma_time <= 0, - f"acoustic({jstr})%gauss_sigma_time must be positive") - self.prohibit(gauss_sigma_dist is not None and gauss_sigma_dist <= 0, - f"acoustic({jstr})%gauss_sigma_dist must be positive") + self.prohibit(gauss_sigma_time is not None and gauss_sigma_time <= 0, f"acoustic({jstr})%gauss_sigma_time must be positive") + self.prohibit(gauss_sigma_dist is not None and gauss_sigma_dist <= 0, f"acoustic({jstr})%gauss_sigma_dist must be positive") if pulse == 4: - self.prohibit(bb_num_freq is None, - f"acoustic({jstr})%bb_num_freq must be specified for pulse = 4") - self.prohibit(bb_bandwidth is None, - f"acoustic({jstr})%bb_bandwidth must be specified for pulse = 4") - self.prohibit(bb_lowest_freq is None, - f"acoustic({jstr})%bb_lowest_freq must be specified for pulse = 4") + self.prohibit(bb_num_freq is None, f"acoustic({jstr})%bb_num_freq must be specified for pulse = 4") + self.prohibit(bb_bandwidth is None, f"acoustic({jstr})%bb_bandwidth must be specified for pulse = 4") + self.prohibit(bb_lowest_freq is None, f"acoustic({jstr})%bb_lowest_freq must be specified for pulse = 4") # npulse checks - self.prohibit(npulse is None, - f"acoustic({jstr})%npulse must be specified") - self.prohibit(support is not None and support >= 5 and npulse is not None and not isinstance(npulse, int), - f"acoustic({jstr})%npulse must be an integer for support >= 5 (non-planar)") - self.prohibit(npulse is not None and npulse >= 5 and dipole, - f"acoustic({jstr})%dipole is not supported for npulse >= 5") - self.prohibit(support is not None and support < 5 and dir_val is None, - f"acoustic({jstr})%dir must be specified for support < 5 (planar)") - self.prohibit(support == 1 and dir_val is not None and dir_val == 0, - f"acoustic({jstr})%dir must be non-zero for support = 1") - self.prohibit(pulse == 3 and support is not None and support >= 5, - f"acoustic({jstr})%support >= 5 is not allowed for pulse = 3 (square wave)") + self.prohibit(npulse is None, f"acoustic({jstr})%npulse must be specified") + self.prohibit(support is not None and support >= 5 and npulse is not None and not isinstance(npulse, int), f"acoustic({jstr})%npulse must be an integer for support >= 5 (non-planar)") + self.prohibit(npulse is not None and npulse >= 5 and dipole, f"acoustic({jstr})%dipole is not supported for npulse >= 5") + self.prohibit(support is not None and support < 5 and dir_val is None, f"acoustic({jstr})%dir must be specified for support < 5 (planar)") + self.prohibit(support == 1 and dir_val is not None and dir_val == 0, f"acoustic({jstr})%dir must be non-zero for support = 1") + self.prohibit(pulse == 3 and support is not None and support >= 5, f"acoustic({jstr})%support >= 5 is not allowed for pulse = 3 (square wave)") # Geometry checks if support in [2, 3]: - self.prohibit(length is None, - f"acoustic({jstr})%length must be specified for support = {support}") - self.prohibit(length is not None and length <= 0, - f"acoustic({jstr})%length must be positive for support = {support}") + self.prohibit(length is None, f"acoustic({jstr})%length must be specified for support = {support}") + self.prohibit(length is not None and length <= 0, f"acoustic({jstr})%length must be positive for support = {support}") if support == 3: - self.prohibit(height is None, - f"acoustic({jstr})%height must be specified for support = 3") - self.prohibit(height is not None and height <= 0, - f"acoustic({jstr})%height must be positive for support = 3") + self.prohibit(height is None, f"acoustic({jstr})%height must be specified for support = 3") + self.prohibit(height is not None and height <= 0, f"acoustic({jstr})%height must be positive for support = 3") if support is not None and support >= 5: - self.prohibit(foc_length is None, - f"acoustic({jstr})%foc_length must be specified for support >= 5 (non-planar)") - self.prohibit(foc_length is not None and foc_length <= 0, - f"acoustic({jstr})%foc_length must be positive for support >= 5") - self.prohibit(aperture is None, - f"acoustic({jstr})%aperture must be specified for support >= 5 (non-planar)") - self.prohibit(aperture is not None and aperture <= 0, - f"acoustic({jstr})%aperture must be positive for support >= 5") + self.prohibit(foc_length is None, f"acoustic({jstr})%foc_length must be specified for support >= 5 (non-planar)") + self.prohibit(foc_length is not None and foc_length <= 0, f"acoustic({jstr})%foc_length must be positive for support >= 5") + self.prohibit(aperture is None, f"acoustic({jstr})%aperture must be specified for support >= 5 (non-planar)") + self.prohibit(aperture is not None and aperture <= 0, f"acoustic({jstr})%aperture must be positive for support >= 5") # Transducer array checks if support in [9, 10, 11]: - self.prohibit(num_elements is None, - f"acoustic({jstr})%num_elements must be specified for support = {support} (transducer array)") - self.prohibit(num_elements is not None and num_elements <= 0, - f"acoustic({jstr})%num_elements must be positive for support = {support}") - self.prohibit(element_on is not None and element_on < 0, - f"acoustic({jstr})%element_on must be non-negative for support = {support}") - self.prohibit(element_on is not None and num_elements is not None and element_on > num_elements, - f"acoustic({jstr})%element_on must be <= num_elements for support = {support}") + self.prohibit(num_elements is None, f"acoustic({jstr})%num_elements must be specified for support = {support} (transducer array)") + self.prohibit(num_elements is not None and num_elements <= 0, f"acoustic({jstr})%num_elements must be positive for support = {support}") + self.prohibit(element_on is not None and element_on < 0, f"acoustic({jstr})%element_on must be non-negative for support = {support}") + self.prohibit(element_on is not None and num_elements is not None and element_on > num_elements, f"acoustic({jstr})%element_on must be <= num_elements for support = {support}") if support in [9, 10]: - self.prohibit(element_spacing_angle is None, - f"acoustic({jstr})%element_spacing_angle must be specified for support = {support} (2D transducer)") - self.prohibit(element_spacing_angle is not None and element_spacing_angle < 0, - f"acoustic({jstr})%element_spacing_angle must be non-negative for support = {support}") + self.prohibit(element_spacing_angle is None, f"acoustic({jstr})%element_spacing_angle must be specified for support = {support} (2D transducer)") + self.prohibit(element_spacing_angle is not None and element_spacing_angle < 0, f"acoustic({jstr})%element_spacing_angle must be non-negative for support = {support}") if support == 11: - self.prohibit(element_polygon_ratio is None, - f"acoustic({jstr})%element_polygon_ratio must be specified for support = 11 (3D transducer)") - self.prohibit(element_polygon_ratio is not None and element_polygon_ratio <= 0, - f"acoustic({jstr})%element_polygon_ratio must be positive for support = 11") + self.prohibit(element_polygon_ratio is None, f"acoustic({jstr})%element_polygon_ratio must be specified for support = 11 (3D transducer)") + self.prohibit(element_polygon_ratio is not None and element_polygon_ratio <= 0, f"acoustic({jstr})%element_polygon_ratio must be positive for support = 11") def check_adaptive_time_stepping(self): """Checks adaptive time stepping parameters (simulation)""" - adap_dt = self.get('adap_dt', 'F') == 'T' + adap_dt = self.get("adap_dt", "F") == "T" if not adap_dt: return - time_stepper = self.get('time_stepper') - model_eqns = self.get('model_eqns') - polytropic = self.get('polytropic', 'F') == 'T' - bubbles_lagrange = self.get('bubbles_lagrange', 'F') == 'T' - qbmm = self.get('qbmm', 'F') == 'T' - adv_n = self.get('adv_n', 'F') == 'T' - - self.prohibit(time_stepper is not None and time_stepper != 3, - "adap_dt requires Runge-Kutta 3 (time_stepper = 3)") - self.prohibit(model_eqns == 1, - "adap_dt is not supported for model_eqns = 1") - self.prohibit(qbmm, - "adap_dt is not compatible with qbmm") - self.prohibit(not polytropic and not bubbles_lagrange, - "adap_dt requires polytropic = T or bubbles_lagrange = T") - self.prohibit(not adv_n and not bubbles_lagrange, - "adap_dt requires adv_n = T or bubbles_lagrange = T") + time_stepper = self.get("time_stepper") + model_eqns = self.get("model_eqns") + polytropic = self.get("polytropic", "F") == "T" + bubbles_lagrange = self.get("bubbles_lagrange", "F") == "T" + qbmm = self.get("qbmm", "F") == "T" + adv_n = self.get("adv_n", "F") == "T" + + self.prohibit(time_stepper is not None and time_stepper != 3, "adap_dt requires Runge-Kutta 3 (time_stepper = 3)") + self.prohibit(model_eqns == 1, "adap_dt is not supported for model_eqns = 1") + self.prohibit(qbmm, "adap_dt is not compatible with qbmm") + self.prohibit(not polytropic and not bubbles_lagrange, "adap_dt requires polytropic = T or bubbles_lagrange = T") + self.prohibit(not adv_n and not bubbles_lagrange, "adap_dt requires adv_n = T or bubbles_lagrange = T") def check_alt_soundspeed(self): """Checks alternative sound speed parameters (simulation)""" - alt_soundspeed = self.get('alt_soundspeed', 'F') == 'T' + alt_soundspeed = self.get("alt_soundspeed", "F") == "T" if not alt_soundspeed: return - model_eqns = self.get('model_eqns') - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - avg_state = self.get('avg_state') - riemann_solver = self.get('riemann_solver') - num_fluids = self.get('num_fluids') - - self.prohibit(model_eqns is not None and model_eqns != 2, - "5-equation model (model_eqns = 2) is required for alt_soundspeed") - self.prohibit(bubbles_euler, - "alt_soundspeed is not compatible with bubbles_euler") - self.prohibit(avg_state is not None and avg_state != 2, - "alt_soundspeed requires avg_state = 2") - self.prohibit(riemann_solver is not None and riemann_solver != 2, - "alt_soundspeed requires HLLC Riemann solver (riemann_solver = 2)") - self.prohibit(num_fluids is not None and num_fluids not in [2, 3], - "alt_soundspeed requires num_fluids = 2 or 3") + model_eqns = self.get("model_eqns") + bubbles_euler = self.get("bubbles_euler", "F") == "T" + avg_state = self.get("avg_state") + riemann_solver = self.get("riemann_solver") + num_fluids = self.get("num_fluids") + + self.prohibit(model_eqns is not None and model_eqns != 2, "5-equation model (model_eqns = 2) is required for alt_soundspeed") + self.prohibit(bubbles_euler, "alt_soundspeed is not compatible with bubbles_euler") + self.prohibit(avg_state is not None and avg_state != 2, "alt_soundspeed requires avg_state = 2") + self.prohibit(riemann_solver is not None and riemann_solver != 2, "alt_soundspeed requires HLLC Riemann solver (riemann_solver = 2)") + self.prohibit(num_fluids is not None and num_fluids not in [2, 3], "alt_soundspeed requires num_fluids = 2 or 3") def check_bubbles_lagrange(self): """Checks Lagrangian bubble parameters (simulation)""" - bubbles_lagrange = self.get('bubbles_lagrange', 'F') == 'T' + bubbles_lagrange = self.get("bubbles_lagrange", "F") == "T" if not bubbles_lagrange: return - n = self.get('n', 0) - file_per_process = self.get('file_per_process', 'F') == 'T' - model_eqns = self.get('model_eqns') - cluster_type = self.get('lag_params%cluster_type') - smooth_type = self.get('lag_params%smooth_type') - polytropic = self.get('polytropic', 'F') == 'T' - thermal = self.get('thermal') - - self.prohibit(n is not None and n == 0, - "bubbles_lagrange accepts 2D and 3D simulations only") - self.prohibit(file_per_process, - "file_per_process must be false for bubbles_lagrange") - self.prohibit(model_eqns == 3, - "The 6-equation flow model does not support bubbles_lagrange") - self.prohibit(polytropic, - "bubbles_lagrange requires polytropic = F") - self.prohibit(thermal is not None and thermal != 3, - "bubbles_lagrange requires thermal = 3") - self.prohibit(cluster_type is not None and cluster_type >= 2 and smooth_type != 1, - "cluster_type >= 2 requires smooth_type = 1") + n = self.get("n", 0) + file_per_process = self.get("file_per_process", "F") == "T" + model_eqns = self.get("model_eqns") + cluster_type = self.get("lag_params%cluster_type") + smooth_type = self.get("lag_params%smooth_type") + polytropic = self.get("polytropic", "F") == "T" + thermal = self.get("thermal") + + self.prohibit(n is not None and n == 0, "bubbles_lagrange accepts 2D and 3D simulations only") + self.prohibit(file_per_process, "file_per_process must be false for bubbles_lagrange") + self.prohibit(model_eqns == 3, "The 6-equation flow model does not support bubbles_lagrange") + self.prohibit(polytropic, "bubbles_lagrange requires polytropic = F") + self.prohibit(thermal is not None and thermal != 3, "bubbles_lagrange requires thermal = 3") + self.prohibit(cluster_type is not None and cluster_type >= 2 and smooth_type != 1, "cluster_type >= 2 requires smooth_type = 1") def check_continuum_damage(self): """Checks continuum damage model parameters (simulation)""" - cont_damage = self.get('cont_damage', 'F') == 'T' + cont_damage = self.get("cont_damage", "F") == "T" if not cont_damage: return - tau_star = self.get('tau_star') - cont_damage_s = self.get('cont_damage_s') - alpha_bar = self.get('alpha_bar') - model_eqns = self.get('model_eqns') + tau_star = self.get("tau_star") + cont_damage_s = self.get("cont_damage_s") + alpha_bar = self.get("alpha_bar") + model_eqns = self.get("model_eqns") - self.prohibit(tau_star is None, - "tau_star must be specified for cont_damage") - self.prohibit(cont_damage_s is None, - "cont_damage_s must be specified for cont_damage") - self.prohibit(alpha_bar is None, - "alpha_bar must be specified for cont_damage") - self.prohibit(model_eqns is not None and model_eqns != 2, - "cont_damage requires model_eqns = 2") + self.prohibit(tau_star is None, "tau_star must be specified for cont_damage") + self.prohibit(cont_damage_s is None, "cont_damage_s must be specified for cont_damage") + self.prohibit(alpha_bar is None, "alpha_bar must be specified for cont_damage") + self.prohibit(model_eqns is not None and model_eqns != 2, "cont_damage requires model_eqns = 2") def check_grcbc(self): """Checks Generalized Relaxation Characteristics BC (simulation)""" - for dir in ['x', 'y', 'z']: - grcbc_in = self.get(f'bc_{dir}%grcbc_in', 'F') == 'T' - grcbc_out = self.get(f'bc_{dir}%grcbc_out', 'F') == 'T' - grcbc_vel_out = self.get(f'bc_{dir}%grcbc_vel_out', 'F') == 'T' - bc_beg = self.get(f'bc_{dir}%beg') - bc_end = self.get(f'bc_{dir}%end') + for dir in ["x", "y", "z"]: + grcbc_in = self.get(f"bc_{dir}%grcbc_in", "F") == "T" + grcbc_out = self.get(f"bc_{dir}%grcbc_out", "F") == "T" + grcbc_vel_out = self.get(f"bc_{dir}%grcbc_vel_out", "F") == "T" + bc_beg = self.get(f"bc_{dir}%beg") + bc_end = self.get(f"bc_{dir}%end") if grcbc_in: # Check if EITHER beg OR end is set to -7 - self.prohibit(bc_beg != -7 and bc_end != -7, - f"Subsonic Inflow (grcbc_in) requires bc_{dir}%beg = -7 or bc_{dir}%end = -7") + self.prohibit(bc_beg != -7 and bc_end != -7, f"Subsonic Inflow (grcbc_in) requires bc_{dir}%beg = -7 or bc_{dir}%end = -7") if grcbc_out: # Check if EITHER beg OR end is set to -8 - self.prohibit(bc_beg != -8 and bc_end != -8, - f"Subsonic Outflow (grcbc_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") + self.prohibit(bc_beg != -8 and bc_end != -8, f"Subsonic Outflow (grcbc_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") if grcbc_vel_out: - self.prohibit(bc_beg != -8 and bc_end != -8, - f"Subsonic Outflow Velocity (grcbc_vel_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") + self.prohibit(bc_beg != -8 and bc_end != -8, f"Subsonic Outflow Velocity (grcbc_vel_out) requires bc_{dir}%beg = -8 or bc_{dir}%end = -8") def check_probe_integral_output(self): """Checks probe and integral output requirements (simulation)""" - probe_wrt = self.get('probe_wrt', 'F') == 'T' - integral_wrt = self.get('integral_wrt', 'F') == 'T' - fd_order = self.get('fd_order') - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - - self.prohibit(probe_wrt and fd_order is None, - "fd_order must be specified for probe_wrt") - self.prohibit(integral_wrt and fd_order is None, - "fd_order must be specified for integral_wrt") - self.prohibit(integral_wrt and not bubbles_euler, - "integral_wrt requires bubbles_euler to be enabled") + probe_wrt = self.get("probe_wrt", "F") == "T" + integral_wrt = self.get("integral_wrt", "F") == "T" + fd_order = self.get("fd_order") + bubbles_euler = self.get("bubbles_euler", "F") == "T" + + self.prohibit(probe_wrt and fd_order is None, "fd_order must be specified for probe_wrt") + self.prohibit(integral_wrt and fd_order is None, "fd_order must be specified for integral_wrt") + self.prohibit(integral_wrt and not bubbles_euler, "integral_wrt requires bubbles_euler to be enabled") def check_hyperelasticity(self): """Checks hyperelasticity constraints""" - hyperelasticity = self.get('hyperelasticity', 'F') == 'T' - pre_stress = self.get('pre_stress', 'F') == 'T' + hyperelasticity = self.get("hyperelasticity", "F") == "T" + pre_stress = self.get("pre_stress", "F") == "T" - self.prohibit(pre_stress and not hyperelasticity, - "pre_stress requires hyperelasticity to be enabled") + self.prohibit(pre_stress and not hyperelasticity, "pre_stress requires hyperelasticity to be enabled") if not hyperelasticity: return - model_eqns = self.get('model_eqns') + model_eqns = self.get("model_eqns") - self.prohibit(model_eqns == 1, - "hyperelasticity is not supported for model_eqns = 1") - self.prohibit(model_eqns is not None and model_eqns > 3, - "hyperelasticity is not supported for model_eqns > 3") + self.prohibit(model_eqns == 1, "hyperelasticity is not supported for model_eqns = 1") + self.prohibit(model_eqns is not None and model_eqns > 3, "hyperelasticity is not supported for model_eqns > 3") # =================================================================== # Pre-Process Specific Checks @@ -1465,140 +1167,111 @@ def check_hyperelasticity(self): def check_restart(self): """Checks constraints on restart parameters (pre-process)""" - old_grid = self.get('old_grid', 'F') == 'T' - old_ic = self.get('old_ic', 'F') == 'T' - t_step_old = self.get('t_step_old') - num_patches = self.get('num_patches', 0) - - self.prohibit(not old_grid and old_ic, - "old_ic can only be enabled with old_grid enabled") - self.prohibit(old_grid and t_step_old is None, - "old_grid requires t_step_old to be set") - self.prohibit(num_patches < 0, - "num_patches must be non-negative") - self.prohibit(num_patches == 0 and t_step_old is None, - "num_patches must be positive for the non-restart case") + old_grid = self.get("old_grid", "F") == "T" + old_ic = self.get("old_ic", "F") == "T" + t_step_old = self.get("t_step_old") + num_patches = self.get("num_patches", 0) + + self.prohibit(not old_grid and old_ic, "old_ic can only be enabled with old_grid enabled") + self.prohibit(old_grid and t_step_old is None, "old_grid requires t_step_old to be set") + self.prohibit(num_patches < 0, "num_patches must be non-negative") + self.prohibit(num_patches == 0 and t_step_old is None, "num_patches must be positive for the non-restart case") def check_qbmm_pre_process(self): """Checks QBMM constraints for pre-process""" - qbmm = self.get('qbmm', 'F') == 'T' - dist_type = self.get('dist_type') - rhoRV = self.get('rhoRV') + qbmm = self.get("qbmm", "F") == "T" + dist_type = self.get("dist_type") + rhoRV = self.get("rhoRV") if not qbmm: return - self.prohibit(dist_type is None, - "dist_type must be set if using QBMM") - self.prohibit(dist_type is not None and dist_type != 1 and rhoRV is not None and rhoRV > 0, - "rhoRV cannot be used with dist_type != 1") + self.prohibit(dist_type is None, "dist_type must be set if using QBMM") + self.prohibit(dist_type is not None and dist_type != 1 and rhoRV is not None and rhoRV > 0, "rhoRV cannot be used with dist_type != 1") def check_parallel_io_pre_process(self): """Checks parallel I/O constraints (pre-process)""" - parallel_io = self.get('parallel_io', 'F') == 'T' - down_sample = self.get('down_sample', 'F') == 'T' - igr = self.get('igr', 'F') == 'T' - p = self.get('p', 0) - file_per_process = self.get('file_per_process', 'F') == 'T' - m = self.get('m', 0) - n = self.get('n', 0) + parallel_io = self.get("parallel_io", "F") == "T" + down_sample = self.get("down_sample", "F") == "T" + igr = self.get("igr", "F") == "T" + p = self.get("p", 0) + file_per_process = self.get("file_per_process", "F") == "T" + m = self.get("m", 0) + n = self.get("n", 0) if down_sample: - self.prohibit(not parallel_io, - "down sample requires parallel_io = T") - self.prohibit(not igr, - "down sample requires igr = T") - self.prohibit(p == 0, - "down sample requires 3D (p > 0)") - self.prohibit(not file_per_process, - "down sample requires file_per_process = T") + self.prohibit(not parallel_io, "down sample requires parallel_io = T") + self.prohibit(not igr, "down sample requires igr = T") + self.prohibit(p == 0, "down sample requires 3D (p > 0)") + self.prohibit(not file_per_process, "down sample requires file_per_process = T") if m is not None and m >= 0: - self.prohibit((m + 1) % 3 != 0, - "down sample requires m divisible by 3") + self.prohibit((m + 1) % 3 != 0, "down sample requires m divisible by 3") if n is not None and n >= 0: - self.prohibit((n + 1) % 3 != 0, - "down sample requires n divisible by 3") + self.prohibit((n + 1) % 3 != 0, "down sample requires n divisible by 3") if p is not None and p >= 0: - self.prohibit((p + 1) % 3 != 0, - "down sample requires p divisible by 3") + self.prohibit((p + 1) % 3 != 0, "down sample requires p divisible by 3") - def check_grid_stretching(self): # pylint: disable=too-many-branches + def check_grid_stretching(self): """Checks grid stretching constraints (pre-process)""" - loops_x = self.get('loops_x', 1) - loops_y = self.get('loops_y', 1) - stretch_y = self.get('stretch_y', 'F') == 'T' - stretch_z = self.get('stretch_z', 'F') == 'T' - old_grid = self.get('old_grid', 'F') == 'T' - n = self.get('n', 0) - p = self.get('p', 0) - cyl_coord = self.get('cyl_coord', 'F') == 'T' - - self.prohibit(loops_x < 1, - "loops_x must be at least 1") - self.prohibit(loops_y < 1, - "loops_y must be at least 1") - self.prohibit(stretch_y and n == 0, - "stretch_y requires n > 0") - self.prohibit(stretch_z and p == 0, - "stretch_z requires p > 0") - self.prohibit(stretch_z and cyl_coord, - "stretch_z is not compatible with cylindrical coordinates") - - for direction in ['x', 'y', 'z']: - stretch = self.get(f'stretch_{direction}', 'F') == 'T' + loops_x = self.get("loops_x", 1) + loops_y = self.get("loops_y", 1) + stretch_y = self.get("stretch_y", "F") == "T" + stretch_z = self.get("stretch_z", "F") == "T" + old_grid = self.get("old_grid", "F") == "T" + n = self.get("n", 0) + p = self.get("p", 0) + cyl_coord = self.get("cyl_coord", "F") == "T" + + self.prohibit(loops_x < 1, "loops_x must be at least 1") + self.prohibit(loops_y < 1, "loops_y must be at least 1") + self.prohibit(stretch_y and n == 0, "stretch_y requires n > 0") + self.prohibit(stretch_z and p == 0, "stretch_z requires p > 0") + self.prohibit(stretch_z and cyl_coord, "stretch_z is not compatible with cylindrical coordinates") + + for direction in ["x", "y", "z"]: + stretch = self.get(f"stretch_{direction}", "F") == "T" if not stretch: continue - a = self.get(f'a_{direction}') - coord_a = self.get(f'{direction}_a') - coord_b = self.get(f'{direction}_b') - - self.prohibit(old_grid, - f"old_grid and stretch_{direction} are incompatible") - self.prohibit(a is None, - f"a_{direction} must be set with stretch_{direction} enabled") - self.prohibit(coord_a is None, - f"{direction}_a must be set with stretch_{direction} enabled") - self.prohibit(coord_b is None, - f"{direction}_b must be set with stretch_{direction} enabled") + a = self.get(f"a_{direction}") + coord_a = self.get(f"{direction}_a") + coord_b = self.get(f"{direction}_b") + + self.prohibit(old_grid, f"old_grid and stretch_{direction} are incompatible") + self.prohibit(a is None, f"a_{direction} must be set with stretch_{direction} enabled") + self.prohibit(coord_a is None, f"{direction}_a must be set with stretch_{direction} enabled") + self.prohibit(coord_b is None, f"{direction}_b must be set with stretch_{direction} enabled") if coord_a is not None and coord_b is not None: - self.prohibit(coord_a >= coord_b, - f"{direction}_a must be less than {direction}_b with stretch_{direction} enabled") + self.prohibit(coord_a >= coord_b, f"{direction}_a must be less than {direction}_b with stretch_{direction} enabled") def check_perturb_density(self): """Checks initial partial density perturbation constraints (pre-process)""" - perturb_flow = self.get('perturb_flow', 'F') == 'T' - perturb_flow_fluid = self.get('perturb_flow_fluid') - perturb_flow_mag = self.get('perturb_flow_mag') - perturb_sph = self.get('perturb_sph', 'F') == 'T' - perturb_sph_fluid = self.get('perturb_sph_fluid') - num_fluids = self.get('num_fluids') + perturb_flow = self.get("perturb_flow", "F") == "T" + perturb_flow_fluid = self.get("perturb_flow_fluid") + perturb_flow_mag = self.get("perturb_flow_mag") + perturb_sph = self.get("perturb_sph", "F") == "T" + perturb_sph_fluid = self.get("perturb_sph_fluid") + num_fluids = self.get("num_fluids") if perturb_flow: - self.prohibit(perturb_flow_fluid is None or perturb_flow_mag is None, - "perturb_flow_fluid and perturb_flow_mag must be set with perturb_flow = T") + self.prohibit(perturb_flow_fluid is None or perturb_flow_mag is None, "perturb_flow_fluid and perturb_flow_mag must be set with perturb_flow = T") else: - self.prohibit(perturb_flow_fluid is not None or perturb_flow_mag is not None, - "perturb_flow_fluid and perturb_flow_mag must not be set with perturb_flow = F") + self.prohibit(perturb_flow_fluid is not None or perturb_flow_mag is not None, "perturb_flow_fluid and perturb_flow_mag must not be set with perturb_flow = F") if num_fluids is not None and perturb_flow_fluid is not None: - self.prohibit(perturb_flow_fluid > num_fluids or perturb_flow_fluid < 0, - "perturb_flow_fluid must be between 0 and num_fluids") + self.prohibit(perturb_flow_fluid > num_fluids or perturb_flow_fluid < 0, "perturb_flow_fluid must be between 0 and num_fluids") if perturb_sph: - self.prohibit(perturb_sph_fluid is None, - "perturb_sph_fluid must be set with perturb_sph = T") + self.prohibit(perturb_sph_fluid is None, "perturb_sph_fluid must be set with perturb_sph = T") else: - self.prohibit(perturb_sph_fluid is not None, - "perturb_sph_fluid must not be set with perturb_sph = F") + self.prohibit(perturb_sph_fluid is not None, "perturb_sph_fluid must not be set with perturb_sph = F") if num_fluids is not None and perturb_sph_fluid is not None: - self.prohibit(perturb_sph_fluid > num_fluids or perturb_sph_fluid < 0, - "perturb_sph_fluid must be between 0 and num_fluids") + self.prohibit(perturb_sph_fluid > num_fluids or perturb_sph_fluid < 0, "perturb_sph_fluid must be between 0 and num_fluids") def check_chemistry(self): """Checks chemistry constraints (pre-process) - + Note: num_species is set automatically by Cantera at runtime when cantera_file is provided. No static validation is performed here - chemistry will fail at runtime if misconfigured. @@ -1606,26 +1279,23 @@ def check_chemistry(self): def check_misc_pre_process(self): """Checks miscellaneous pre-process constraints""" - mixlayer_vel_profile = self.get('mixlayer_vel_profile', 'F') == 'T' - mixlayer_perturb = self.get('mixlayer_perturb', 'F') == 'T' - elliptic_smoothing = self.get('elliptic_smoothing', 'F') == 'T' - elliptic_smoothing_iters = self.get('elliptic_smoothing_iters') - n = self.get('n', 0) - p = self.get('p', 0) - - self.prohibit(mixlayer_vel_profile and n == 0, - "mixlayer_vel_profile requires n > 0") - self.prohibit(mixlayer_perturb and p == 0, - "mixlayer_perturb requires p > 0") + mixlayer_vel_profile = self.get("mixlayer_vel_profile", "F") == "T" + mixlayer_perturb = self.get("mixlayer_perturb", "F") == "T" + elliptic_smoothing = self.get("elliptic_smoothing", "F") == "T" + elliptic_smoothing_iters = self.get("elliptic_smoothing_iters") + n = self.get("n", 0) + p = self.get("p", 0) + + self.prohibit(mixlayer_vel_profile and n == 0, "mixlayer_vel_profile requires n > 0") + self.prohibit(mixlayer_perturb and p == 0, "mixlayer_perturb requires p > 0") if elliptic_smoothing and elliptic_smoothing_iters is not None: - self.prohibit(elliptic_smoothing_iters < 1, - "elliptic_smoothing_iters must be positive") + self.prohibit(elliptic_smoothing_iters < 1, "elliptic_smoothing_iters must be positive") def _is_numeric(self, value) -> bool: """Check if value is numeric (not a string expression).""" return isinstance(value, (int, float)) and not isinstance(value, bool) - def check_patch_physics(self): # pylint: disable=too-many-locals,too-many-branches + def check_patch_physics(self): """Checks physics constraints on patch initial conditions (pre-process). Validates that initial conditions are physically meaningful: @@ -1637,17 +1307,17 @@ def check_patch_physics(self): # pylint: disable=too-many-locals,too-many-branc Note: String values (analytical expressions like "0.5*sin(x)") are evaluated at runtime by Fortran and cannot be validated here. """ - num_patches = self.get('num_patches', 0) - num_fluids = self.get('num_fluids', 1) - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - num_ibs = self.get('num_ibs', 0) or 0 # IBM (Immersed Boundary Method) + num_patches = self.get("num_patches", 0) + num_fluids = self.get("num_fluids", 1) + bubbles_euler = self.get("bubbles_euler", "F") == "T" + num_ibs = self.get("num_ibs", 0) or 0 # IBM (Immersed Boundary Method) if not self._is_numeric(num_patches) or num_patches <= 0: return for i in range(1, num_patches + 1): istr = str(i) - geometry = self.get(f'patch_icpp({i})%geometry') + geometry = self.get(f"patch_icpp({i})%geometry") # Skip if patch not defined if geometry is None: @@ -1656,19 +1326,17 @@ def check_patch_physics(self): # pylint: disable=too-many-locals,too-many-branc # Skip thermodynamic validation for special patches: # - alter_patch patches (modifications to other patches) # - hcid patches (hard-coded initial conditions computed at runtime) - hcid = self.get(f'patch_icpp({i})%hcid') - alter_patches = [self.get(f'patch_icpp({i})%alter_patch({j})') == 'T' - for j in range(1, num_patches + 1)] + hcid = self.get(f"patch_icpp({i})%hcid") + alter_patches = [self.get(f"patch_icpp({i})%alter_patch({j})") == "T" for j in range(1, num_patches + 1)] is_special = hcid is not None or any(alter_patches) # === THERMODYNAMICS === # Pressure must be positive for physical stability # (skip for special patches where values are computed differently) if not is_special: - pres = self.get(f'patch_icpp({i})%pres') + pres = self.get(f"patch_icpp({i})%pres") if pres is not None and self._is_numeric(pres): - self.prohibit(pres <= 0, - f"patch_icpp({istr})%pres must be positive (got {pres})") + self.prohibit(pres <= 0, f"patch_icpp({istr})%pres must be positive (got {pres})") # === FLUID PROPERTIES === # (skip for special patches where values are computed differently) @@ -1677,110 +1345,91 @@ def check_patch_physics(self): # pylint: disable=too-many-locals,too-many-branc jstr = str(j) # Volume fraction must be in [0, 1] (or non-negative for IBM cases) - alpha = self.get(f'patch_icpp({i})%alpha({j})') + alpha = self.get(f"patch_icpp({i})%alpha({j})") if alpha is not None and self._is_numeric(alpha): - self.prohibit(alpha < 0, - f"patch_icpp({istr})%alpha({jstr}) must be non-negative (got {alpha})") + self.prohibit(alpha < 0, f"patch_icpp({istr})%alpha({jstr}) must be non-negative (got {alpha})") # For non-IBM cases, alpha should be in [0, 1] if num_ibs == 0: - self.prohibit(alpha > 1, - f"patch_icpp({istr})%alpha({jstr}) must be <= 1 (got {alpha})") + self.prohibit(alpha > 1, f"patch_icpp({istr})%alpha({jstr}) must be <= 1 (got {alpha})") # Density (alpha_rho) must be non-negative # Note: alpha_rho = 0 is allowed for vacuum regions and numerical convenience - alpha_rho = self.get(f'patch_icpp({i})%alpha_rho({j})') + alpha_rho = self.get(f"patch_icpp({i})%alpha_rho({j})") if alpha_rho is not None and self._is_numeric(alpha_rho): - self.prohibit(alpha_rho < 0, - f"patch_icpp({istr})%alpha_rho({jstr}) must be non-negative (got {alpha_rho})") + self.prohibit(alpha_rho < 0, f"patch_icpp({istr})%alpha_rho({jstr}) must be non-negative (got {alpha_rho})") # === GEOMETRY === # Patch dimensions must be positive (except in cylindrical coords where # length_y/length_z can be sentinel values like -1000000.0) - length_x = self.get(f'patch_icpp({i})%length_x') - length_y = self.get(f'patch_icpp({i})%length_y') - length_z = self.get(f'patch_icpp({i})%length_z') - radius = self.get(f'patch_icpp({i})%radius') - cyl_coord = self.get('cyl_coord', 'F') == 'T' + length_x = self.get(f"patch_icpp({i})%length_x") + length_y = self.get(f"patch_icpp({i})%length_y") + length_z = self.get(f"patch_icpp({i})%length_z") + radius = self.get(f"patch_icpp({i})%radius") + cyl_coord = self.get("cyl_coord", "F") == "T" if length_x is not None and self._is_numeric(length_x): - self.prohibit(length_x <= 0, - f"patch_icpp({istr})%length_x must be positive (got {length_x})") + self.prohibit(length_x <= 0, f"patch_icpp({istr})%length_x must be positive (got {length_x})") # In cylindrical coordinates, length_y and length_z can be negative sentinel values if length_y is not None and self._is_numeric(length_y) and not cyl_coord: - self.prohibit(length_y <= 0, - f"patch_icpp({istr})%length_y must be positive (got {length_y})") + self.prohibit(length_y <= 0, f"patch_icpp({istr})%length_y must be positive (got {length_y})") if length_z is not None and self._is_numeric(length_z) and not cyl_coord: - self.prohibit(length_z <= 0, - f"patch_icpp({istr})%length_z must be positive (got {length_z})") + self.prohibit(length_z <= 0, f"patch_icpp({istr})%length_z must be positive (got {length_z})") if radius is not None and self._is_numeric(radius): - self.prohibit(radius <= 0, - f"patch_icpp({istr})%radius must be positive (got {radius})") + self.prohibit(radius <= 0, f"patch_icpp({istr})%radius must be positive (got {radius})") # === BUBBLES === # Bubble radius must be positive if bubbles_euler: - r0 = self.get(f'patch_icpp({i})%r0') + r0 = self.get(f"patch_icpp({i})%r0") if r0 is not None and self._is_numeric(r0): - self.prohibit(r0 <= 0, - f"patch_icpp({istr})%r0 must be positive (got {r0})") + self.prohibit(r0 <= 0, f"patch_icpp({istr})%r0 must be positive (got {r0})") - def check_bc_patches(self): # pylint: disable=too-many-branches,too-many-statements + def check_bc_patches(self): """Checks boundary condition patch geometry (pre-process)""" - num_bc_patches = self.get('num_bc_patches', 0) - num_bc_patches_max = self.get('num_bc_patches_max', 10) + num_bc_patches = self.get("num_bc_patches", 0) + num_bc_patches_max = self.get("num_bc_patches_max", 10) if num_bc_patches <= 0: return - self.prohibit(num_bc_patches > num_bc_patches_max, - f"num_bc_patches must be <= {num_bc_patches_max}") + self.prohibit(num_bc_patches > num_bc_patches_max, f"num_bc_patches must be <= {num_bc_patches_max}") for i in range(1, num_bc_patches + 1): - geometry = self.get(f'patch_bc({i})%geometry') - bc_type = self.get(f'patch_bc({i})%type') - direction = self.get(f'patch_bc({i})%dir') - radius = self.get(f'patch_bc({i})%radius') - centroid = [self.get(f'patch_bc({i})%centroid({j})') for j in range(1, 4)] - length = [self.get(f'patch_bc({i})%length({j})') for j in range(1, 4)] + geometry = self.get(f"patch_bc({i})%geometry") + bc_type = self.get(f"patch_bc({i})%type") + direction = self.get(f"patch_bc({i})%dir") + radius = self.get(f"patch_bc({i})%radius") + centroid = [self.get(f"patch_bc({i})%centroid({j})") for j in range(1, 4)] + length = [self.get(f"patch_bc({i})%length({j})") for j in range(1, 4)] if geometry is None: continue # Line Segment BC (geometry = 1) if geometry == 1: - self.prohibit(radius is not None, - f"Line Segment Patch {i} can't have radius defined") + self.prohibit(radius is not None, f"Line Segment Patch {i} can't have radius defined") if direction in [1, 2]: - self.prohibit(centroid[direction - 1] is not None or centroid[2] is not None, - f"Line Segment Patch {i} of Dir {direction} can't have centroid in Dir {direction} or 3") - self.prohibit(length[direction - 1] is not None or length[2] is not None, - f"Line Segment Patch {i} of Dir {direction} can't have length in Dir {direction} or 3") + self.prohibit(centroid[direction - 1] is not None or centroid[2] is not None, f"Line Segment Patch {i} of Dir {direction} can't have centroid in Dir {direction} or 3") + self.prohibit(length[direction - 1] is not None or length[2] is not None, f"Line Segment Patch {i} of Dir {direction} can't have length in Dir {direction} or 3") # Circle BC (geometry = 2) elif geometry == 2: - self.prohibit(radius is None, - f"Circle Patch {i} must have radius defined") - self.prohibit(any(length_val is not None for length_val in length), - f"Circle Patch {i} can't have lengths defined") + self.prohibit(radius is None, f"Circle Patch {i} must have radius defined") + self.prohibit(any(length_val is not None for length_val in length), f"Circle Patch {i} can't have lengths defined") if direction in [1, 2, 3]: - self.prohibit(centroid[direction - 1] is not None, - f"Circle Patch {i} of Dir {direction} can't have centroid in Dir {direction}") + self.prohibit(centroid[direction - 1] is not None, f"Circle Patch {i} of Dir {direction} can't have centroid in Dir {direction}") # Rectangle BC (geometry = 3) elif geometry == 3: - self.prohibit(radius is not None, - f"Rectangle Patch {i} can't have radius defined") + self.prohibit(radius is not None, f"Rectangle Patch {i} can't have radius defined") if direction in [1, 2, 3]: - self.prohibit(centroid[direction - 1] is not None, - f"Rectangle Patch {i} of Dir {direction} can't have centroid in Dir {direction}") - self.prohibit(length[direction - 1] is not None, - f"Rectangle Patch {i} of Dir {direction} can't have length in Dir {direction}") + self.prohibit(centroid[direction - 1] is not None, f"Rectangle Patch {i} of Dir {direction} can't have centroid in Dir {direction}") + self.prohibit(length[direction - 1] is not None, f"Rectangle Patch {i} of Dir {direction} can't have length in Dir {direction}") # Check for incompatible BC types if bc_type is not None: # BC types -14 to -4, -1 (periodic), or < -17 (dirichlet) are incompatible with patches - self.prohibit((-14 <= bc_type <= -4) or bc_type == -1 or bc_type < -17, - f"Incompatible BC type for boundary condition patch {i}") + self.prohibit((-14 <= bc_type <= -4) or bc_type == -1 or bc_type < -17, f"Incompatible BC type for boundary condition patch {i}") # =================================================================== # Post-Process Specific Checks @@ -1788,262 +1437,242 @@ def check_bc_patches(self): # pylint: disable=too-many-branches,too-many-statem def check_output_format(self): """Checks output format parameters (post-process)""" - format = self.get('format') - precision = self.get('precision') + format = self.get("format") + precision = self.get("precision") if format is not None: - self.prohibit(format not in [1, 2], - "format must be 1 or 2") + self.prohibit(format not in [1, 2], "format must be 1 or 2") if precision is not None: - self.prohibit(precision not in [1, 2], - "precision must be 1 or 2") - self.prohibit( - precision == 2 and CFG().single, - "precision = 2 (double output) requires MFC built without --single" - ) + self.prohibit(precision not in [1, 2], "precision must be 1 or 2") + self.prohibit(precision == 2 and CFG().single, "precision = 2 (double output) requires MFC built without --single") def check_vorticity(self): """Checks vorticity parameters (post-process)""" - omega_wrt = [self.get(f'omega_wrt({i})', 'F') == 'T' for i in range(1, 4)] - n = self.get('n', 0) - p = self.get('p', 0) - fd_order = self.get('fd_order') - - self.prohibit(n is not None and n == 0 and any(omega_wrt), - "omega_wrt requires n > 0 (at least 2D)") - self.prohibit(p is not None and p == 0 and (omega_wrt[0] or omega_wrt[1]), - "omega_wrt(1) and omega_wrt(2) require p > 0 (3D)") - self.prohibit(any(omega_wrt) and fd_order is None, - "fd_order must be set for omega_wrt") + omega_wrt = [self.get(f"omega_wrt({i})", "F") == "T" for i in range(1, 4)] + n = self.get("n", 0) + p = self.get("p", 0) + fd_order = self.get("fd_order") + + self.prohibit(n is not None and n == 0 and any(omega_wrt), "omega_wrt requires n > 0 (at least 2D)") + self.prohibit(p is not None and p == 0 and (omega_wrt[0] or omega_wrt[1]), "omega_wrt(1) and omega_wrt(2) require p > 0 (3D)") + self.prohibit(any(omega_wrt) and fd_order is None, "fd_order must be set for omega_wrt") def check_schlieren(self): """Checks schlieren parameters (post-process)""" - schlieren_wrt = self.get('schlieren_wrt', 'F') == 'T' - n = self.get('n', 0) - fd_order = self.get('fd_order') - num_fluids = self.get('num_fluids') + schlieren_wrt = self.get("schlieren_wrt", "F") == "T" + n = self.get("n", 0) + fd_order = self.get("fd_order") + num_fluids = self.get("num_fluids") - self.prohibit(n is not None and n == 0 and schlieren_wrt, - "schlieren_wrt requires n > 0 (at least 2D)") - self.prohibit(schlieren_wrt and fd_order is None, - "fd_order must be set for schlieren_wrt") + self.prohibit(n is not None and n == 0 and schlieren_wrt, "schlieren_wrt requires n > 0 (at least 2D)") + self.prohibit(schlieren_wrt and fd_order is None, "fd_order must be set for schlieren_wrt") if num_fluids is not None: for i in range(1, num_fluids + 1): - schlieren_alpha = self.get(f'schlieren_alpha({i})') + schlieren_alpha = self.get(f"schlieren_alpha({i})") if schlieren_alpha is not None: - self.prohibit(schlieren_alpha <= 0, - f"schlieren_alpha({i}) must be greater than zero") - self.prohibit(not schlieren_wrt, - f"schlieren_alpha({i}) should be set only with schlieren_wrt enabled") + self.prohibit(schlieren_alpha <= 0, f"schlieren_alpha({i}) must be greater than zero") + self.prohibit(not schlieren_wrt, f"schlieren_alpha({i}) should be set only with schlieren_wrt enabled") - def check_partial_domain(self): # pylint: disable=too-many-locals + def check_partial_domain(self): """Checks partial domain output constraints (post-process)""" - output_partial_domain = self.get('output_partial_domain', 'F') == 'T' + output_partial_domain = self.get("output_partial_domain", "F") == "T" if not output_partial_domain: return - format_val = self.get('format') - precision = self.get('precision') - flux_wrt = self.get('flux_wrt', 'F') == 'T' - heat_ratio_wrt = self.get('heat_ratio_wrt', 'F') == 'T' - pres_inf_wrt = self.get('pres_inf_wrt', 'F') == 'T' - c_wrt = self.get('c_wrt', 'F') == 'T' - schlieren_wrt = self.get('schlieren_wrt', 'F') == 'T' - qm_wrt = self.get('qm_wrt', 'F') == 'T' - liutex_wrt = self.get('liutex_wrt', 'F') == 'T' - ib = self.get('ib', 'F') == 'T' - omega_wrt = [self.get(f'omega_wrt({i})', 'F') == 'T' for i in range(1, 4)] - n = self.get('n', 0) - p = self.get('p', 0) - - self.prohibit(format_val == 1, - "output_partial_domain requires format = 2") - self.prohibit(precision == 1, - "output_partial_domain requires precision = 2") - self.prohibit(flux_wrt or heat_ratio_wrt or pres_inf_wrt or c_wrt or - schlieren_wrt or qm_wrt or liutex_wrt or ib or any(omega_wrt), - "output_partial_domain is incompatible with certain output flags") - - x_output_beg = self.get('x_output%beg') - x_output_end = self.get('x_output%end') - self.prohibit(x_output_beg is None or x_output_end is None, - "x_output%beg and x_output%end must be set for output_partial_domain") + format_val = self.get("format") + precision = self.get("precision") + flux_wrt = self.get("flux_wrt", "F") == "T" + heat_ratio_wrt = self.get("heat_ratio_wrt", "F") == "T" + pres_inf_wrt = self.get("pres_inf_wrt", "F") == "T" + c_wrt = self.get("c_wrt", "F") == "T" + schlieren_wrt = self.get("schlieren_wrt", "F") == "T" + qm_wrt = self.get("qm_wrt", "F") == "T" + liutex_wrt = self.get("liutex_wrt", "F") == "T" + ib = self.get("ib", "F") == "T" + omega_wrt = [self.get(f"omega_wrt({i})", "F") == "T" for i in range(1, 4)] + n = self.get("n", 0) + p = self.get("p", 0) + + self.prohibit(format_val == 1, "output_partial_domain requires format = 2") + self.prohibit(precision == 1, "output_partial_domain requires precision = 2") + self.prohibit( + flux_wrt or heat_ratio_wrt or pres_inf_wrt or c_wrt or schlieren_wrt or qm_wrt or liutex_wrt or ib or any(omega_wrt), "output_partial_domain is incompatible with certain output flags" + ) + + x_output_beg = self.get("x_output%beg") + x_output_end = self.get("x_output%end") + self.prohibit(x_output_beg is None or x_output_end is None, "x_output%beg and x_output%end must be set for output_partial_domain") if n is not None and n != 0: - y_output_beg = self.get('y_output%beg') - y_output_end = self.get('y_output%end') - self.prohibit(y_output_beg is None or y_output_end is None, - "y_output%beg and y_output%end must be set for output_partial_domain with n > 0") + y_output_beg = self.get("y_output%beg") + y_output_end = self.get("y_output%end") + self.prohibit(y_output_beg is None or y_output_end is None, "y_output%beg and y_output%end must be set for output_partial_domain with n > 0") if p is not None and p != 0: - z_output_beg = self.get('z_output%beg') - z_output_end = self.get('z_output%end') - self.prohibit(z_output_beg is None or z_output_end is None, - "z_output%beg and z_output%end must be set for output_partial_domain with p > 0") - - for direction in ['x', 'y', 'z']: - beg = self.get(f'{direction}_output%beg') - end = self.get(f'{direction}_output%end') + z_output_beg = self.get("z_output%beg") + z_output_end = self.get("z_output%end") + self.prohibit(z_output_beg is None or z_output_end is None, "z_output%beg and z_output%end must be set for output_partial_domain with p > 0") + + for direction in ["x", "y", "z"]: + beg = self.get(f"{direction}_output%beg") + end = self.get(f"{direction}_output%end") if beg is not None and end is not None: - self.prohibit(beg > end, - f"{direction}_output%beg must be <= {direction}_output%end") + self.prohibit(beg > end, f"{direction}_output%beg must be <= {direction}_output%end") def check_partial_density(self): """Checks partial density output constraints (post-process)""" - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") if num_fluids is None: return for i in range(1, num_fluids + 1): - alpha_rho_wrt = self.get(f'alpha_rho_wrt({i})', 'F') == 'T' + alpha_rho_wrt = self.get(f"alpha_rho_wrt({i})", "F") == "T" if alpha_rho_wrt: - self.prohibit(model_eqns == 1, - f"alpha_rho_wrt({i}) is not supported for model_eqns = 1") + self.prohibit(model_eqns == 1, f"alpha_rho_wrt({i}) is not supported for model_eqns = 1") def check_momentum_post(self): """Checks momentum output constraints (post-process)""" - mom_wrt = [self.get(f'mom_wrt({i})', 'F') == 'T' for i in range(1, 4)] - n = self.get('n', 0) - p = self.get('p', 0) + mom_wrt = [self.get(f"mom_wrt({i})", "F") == "T" for i in range(1, 4)] + n = self.get("n", 0) + p = self.get("p", 0) - self.prohibit(n == 0 and mom_wrt[1], - "mom_wrt(2) requires n > 0") - self.prohibit(p == 0 and mom_wrt[2], - "mom_wrt(3) requires p > 0") + self.prohibit(n == 0 and mom_wrt[1], "mom_wrt(2) requires n > 0") + self.prohibit(p == 0 and mom_wrt[2], "mom_wrt(3) requires p > 0") def check_velocity_post(self): """Checks velocity output constraints (post-process)""" - vel_wrt = [self.get(f'vel_wrt({i})', 'F') == 'T' for i in range(1, 4)] - n = self.get('n', 0) - p = self.get('p', 0) + vel_wrt = [self.get(f"vel_wrt({i})", "F") == "T" for i in range(1, 4)] + n = self.get("n", 0) + p = self.get("p", 0) - self.prohibit(n == 0 and vel_wrt[1], - "vel_wrt(2) requires n > 0") - self.prohibit(p == 0 and vel_wrt[2], - "vel_wrt(3) requires p > 0") + self.prohibit(n == 0 and vel_wrt[1], "vel_wrt(2) requires n > 0") + self.prohibit(p == 0 and vel_wrt[2], "vel_wrt(3) requires p > 0") def check_flux_limiter(self): """Checks flux limiter constraints (post-process)""" - flux_wrt = [self.get(f'flux_wrt({i})', 'F') == 'T' for i in range(1, 4)] - flux_lim = self.get('flux_lim') - n = self.get('n', 0) - p = self.get('p', 0) + flux_wrt = [self.get(f"flux_wrt({i})", "F") == "T" for i in range(1, 4)] + flux_lim = self.get("flux_lim") + n = self.get("n", 0) + p = self.get("p", 0) - self.prohibit(n == 0 and flux_wrt[1], - "flux_wrt(2) requires n > 0") - self.prohibit(p == 0 and flux_wrt[2], - "flux_wrt(3) requires p > 0") + self.prohibit(n == 0 and flux_wrt[1], "flux_wrt(2) requires n > 0") + self.prohibit(p == 0 and flux_wrt[2], "flux_wrt(3) requires p > 0") if flux_lim is not None: - self.prohibit(flux_lim not in [1, 2, 3, 4, 5, 6, 7], - "flux_lim must be between 1 and 7") + self.prohibit(flux_lim not in [1, 2, 3, 4, 5, 6, 7], "flux_lim must be between 1 and 7") def check_volume_fraction(self): """Checks volume fraction output constraints (post-process)""" - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") if num_fluids is None: return for i in range(1, num_fluids + 1): - alpha_wrt = self.get(f'alpha_wrt({i})', 'F') == 'T' + alpha_wrt = self.get(f"alpha_wrt({i})", "F") == "T" if alpha_wrt: - self.prohibit(model_eqns == 1, - f"alpha_wrt({i}) is not supported for model_eqns = 1") + self.prohibit(model_eqns == 1, f"alpha_wrt({i}) is not supported for model_eqns = 1") def check_fft(self): """Checks FFT output constraints (post-process)""" - fft_wrt = self.get('fft_wrt', 'F') == 'T' + fft_wrt = self.get("fft_wrt", "F") == "T" if not fft_wrt: return - n = self.get('n', 0) - p = self.get('p', 0) - cyl_coord = self.get('cyl_coord', 'F') == 'T' - m_glb = self.get('m_glb') - n_glb = self.get('n_glb') - p_glb = self.get('p_glb') + n = self.get("n", 0) + p = self.get("p", 0) + cyl_coord = self.get("cyl_coord", "F") == "T" + m_glb = self.get("m_glb") + n_glb = self.get("n_glb") + p_glb = self.get("p_glb") - self.prohibit(n == 0 or p == 0, - "FFT WRT only supported in 3D") - self.prohibit(cyl_coord, - "FFT WRT incompatible with cylindrical coordinates") + self.prohibit(n == 0 or p == 0, "FFT WRT only supported in 3D") + self.prohibit(cyl_coord, "FFT WRT incompatible with cylindrical coordinates") if m_glb is not None and n_glb is not None and p_glb is not None: - self.prohibit((m_glb + 1) % 2 != 0 or (n_glb + 1) % 2 != 0 or (p_glb + 1) % 2 != 0, - "FFT WRT requires global dimensions divisible by 2") + self.prohibit((m_glb + 1) % 2 != 0 or (n_glb + 1) % 2 != 0 or (p_glb + 1) % 2 != 0, "FFT WRT requires global dimensions divisible by 2") # BC checks: all boundaries must be periodic (-1) - for direction in ['x', 'y', 'z']: - for end in ['beg', 'end']: - bc_val = self.get(f'bc_{direction}%{end}') + for direction in ["x", "y", "z"]: + for end in ["beg", "end"]: + bc_val = self.get(f"bc_{direction}%{end}") if bc_val is not None: - self.prohibit(bc_val != -1, - "FFT WRT requires periodic BCs (all BCs should be -1)") + self.prohibit(bc_val != -1, "FFT WRT requires periodic BCs (all BCs should be -1)") def check_qm(self): """Checks Q-criterion output constraints (post-process)""" - qm_wrt = self.get('qm_wrt', 'F') == 'T' - n = self.get('n', 0) + qm_wrt = self.get("qm_wrt", "F") == "T" + n = self.get("n", 0) - self.prohibit(n == 0 and qm_wrt, - "qm_wrt requires n > 0 (at least 2D)") + self.prohibit(n == 0 and qm_wrt, "qm_wrt requires n > 0 (at least 2D)") def check_liutex_post(self): """Checks liutex output constraints (post-process)""" - liutex_wrt = self.get('liutex_wrt', 'F') == 'T' - n = self.get('n', 0) + liutex_wrt = self.get("liutex_wrt", "F") == "T" + n = self.get("n", 0) - self.prohibit(n == 0 and liutex_wrt, - "liutex_wrt requires n > 0 (at least 2D)") + self.prohibit(n == 0 and liutex_wrt, "liutex_wrt requires n > 0 (at least 2D)") def check_surface_tension_post(self): """Checks surface tension output constraints (post-process)""" - cf_wrt = self.get('cf_wrt', 'F') == 'T' - surface_tension = self.get('surface_tension', 'F') == 'T' + cf_wrt = self.get("cf_wrt", "F") == "T" + surface_tension = self.get("surface_tension", "F") == "T" - self.prohibit(cf_wrt and not surface_tension, - "cf_wrt can only be enabled if surface_tension is enabled") + self.prohibit(cf_wrt and not surface_tension, "cf_wrt can only be enabled if surface_tension is enabled") - def check_no_flow_variables(self): # pylint: disable=too-many-locals + def check_no_flow_variables(self): """Checks that at least one flow variable is selected (post-process)""" - rho_wrt = self.get('rho_wrt', 'F') == 'T' - E_wrt = self.get('E_wrt', 'F') == 'T' - pres_wrt = self.get('pres_wrt', 'F') == 'T' - gamma_wrt = self.get('gamma_wrt', 'F') == 'T' - heat_ratio_wrt = self.get('heat_ratio_wrt', 'F') == 'T' - pi_inf_wrt = self.get('pi_inf_wrt', 'F') == 'T' - pres_inf_wrt = self.get('pres_inf_wrt', 'F') == 'T' - cons_vars_wrt = self.get('cons_vars_wrt', 'F') == 'T' - prim_vars_wrt = self.get('prim_vars_wrt', 'F') == 'T' - c_wrt = self.get('c_wrt', 'F') == 'T' - schlieren_wrt = self.get('schlieren_wrt', 'F') == 'T' + rho_wrt = self.get("rho_wrt", "F") == "T" + E_wrt = self.get("E_wrt", "F") == "T" + pres_wrt = self.get("pres_wrt", "F") == "T" + gamma_wrt = self.get("gamma_wrt", "F") == "T" + heat_ratio_wrt = self.get("heat_ratio_wrt", "F") == "T" + pi_inf_wrt = self.get("pi_inf_wrt", "F") == "T" + pres_inf_wrt = self.get("pres_inf_wrt", "F") == "T" + cons_vars_wrt = self.get("cons_vars_wrt", "F") == "T" + prim_vars_wrt = self.get("prim_vars_wrt", "F") == "T" + c_wrt = self.get("c_wrt", "F") == "T" + schlieren_wrt = self.get("schlieren_wrt", "F") == "T" # Check array variables - num_fluids = self.get('num_fluids') + num_fluids = self.get("num_fluids") if num_fluids is None: num_fluids = 1 - alpha_rho_wrt_any = any(self.get(f'alpha_rho_wrt({i})', 'F') == 'T' for i in range(1, num_fluids + 1)) - mom_wrt_any = any(self.get(f'mom_wrt({i})', 'F') == 'T' for i in range(1, 4)) - vel_wrt_any = any(self.get(f'vel_wrt({i})', 'F') == 'T' for i in range(1, 4)) - flux_wrt_any = any(self.get(f'flux_wrt({i})', 'F') == 'T' for i in range(1, 4)) - alpha_wrt_any = any(self.get(f'alpha_wrt({i})', 'F') == 'T' for i in range(1, num_fluids + 1)) - omega_wrt_any = any(self.get(f'omega_wrt({i})', 'F') == 'T' for i in range(1, 4)) - - has_output = (rho_wrt or E_wrt or pres_wrt or gamma_wrt or heat_ratio_wrt or - pi_inf_wrt or pres_inf_wrt or cons_vars_wrt or prim_vars_wrt or - c_wrt or schlieren_wrt or alpha_rho_wrt_any or mom_wrt_any or - vel_wrt_any or flux_wrt_any or alpha_wrt_any or omega_wrt_any) + alpha_rho_wrt_any = any(self.get(f"alpha_rho_wrt({i})", "F") == "T" for i in range(1, num_fluids + 1)) + mom_wrt_any = any(self.get(f"mom_wrt({i})", "F") == "T" for i in range(1, 4)) + vel_wrt_any = any(self.get(f"vel_wrt({i})", "F") == "T" for i in range(1, 4)) + flux_wrt_any = any(self.get(f"flux_wrt({i})", "F") == "T" for i in range(1, 4)) + alpha_wrt_any = any(self.get(f"alpha_wrt({i})", "F") == "T" for i in range(1, num_fluids + 1)) + omega_wrt_any = any(self.get(f"omega_wrt({i})", "F") == "T" for i in range(1, 4)) + + has_output = ( + rho_wrt + or E_wrt + or pres_wrt + or gamma_wrt + or heat_ratio_wrt + or pi_inf_wrt + or pres_inf_wrt + or cons_vars_wrt + or prim_vars_wrt + or c_wrt + or schlieren_wrt + or alpha_rho_wrt_any + or mom_wrt_any + or vel_wrt_any + or flux_wrt_any + or alpha_wrt_any + or omega_wrt_any + ) - self.prohibit(not has_output, - "None of the flow variables have been selected for post-process") + self.prohibit(not has_output, "None of the flow variables have been selected for post-process") # =================================================================== # Cross-Cutting Physics Checks @@ -2051,29 +1680,26 @@ def check_no_flow_variables(self): # pylint: disable=too-many-locals def check_domain_bounds(self): """Checks that domain end > domain begin for each active dimension""" - x_beg = self.get('x_domain%beg') - x_end = self.get('x_domain%end') + x_beg = self.get("x_domain%beg") + x_end = self.get("x_domain%end") if self._is_numeric(x_beg) and self._is_numeric(x_end): - self.prohibit(x_end <= x_beg, - f"x_domain%end ({x_end}) must be greater than x_domain%beg ({x_beg})") + self.prohibit(x_end <= x_beg, f"x_domain%end ({x_end}) must be greater than x_domain%beg ({x_beg})") - n = self.get('n', 0) + n = self.get("n", 0) if self._is_numeric(n) and n > 0: - y_beg = self.get('y_domain%beg') - y_end = self.get('y_domain%end') + y_beg = self.get("y_domain%beg") + y_end = self.get("y_domain%end") if self._is_numeric(y_beg) and self._is_numeric(y_end): - self.prohibit(y_end <= y_beg, - f"y_domain%end ({y_end}) must be greater than y_domain%beg ({y_beg})") + self.prohibit(y_end <= y_beg, f"y_domain%end ({y_end}) must be greater than y_domain%beg ({y_beg})") - p = self.get('p', 0) + p = self.get("p", 0) if self._is_numeric(p) and p > 0: - z_beg = self.get('z_domain%beg') - z_end = self.get('z_domain%end') + z_beg = self.get("z_domain%beg") + z_end = self.get("z_domain%end") if self._is_numeric(z_beg) and self._is_numeric(z_end): - self.prohibit(z_end <= z_beg, - f"z_domain%end ({z_end}) must be greater than z_domain%beg ({z_beg})") + self.prohibit(z_end <= z_beg, f"z_domain%end ({z_end}) must be greater than z_domain%beg ({z_beg})") - def check_volume_fraction_sum(self): # pylint: disable=too-many-locals + def check_volume_fraction_sum(self): """Warns if volume fractions do not sum to 1 for multi-component models. For model_eqns in [2, 3, 4], the mixture constraint sum(alpha_j) = 1 @@ -2082,14 +1708,14 @@ def check_volume_fraction_sum(self): # pylint: disable=too-many-locals the void fraction, not a partition of unity), and bubbles_lagrange cases (where the Lagrangian phase is not tracked on the Euler grid). """ - model_eqns = self.get('model_eqns') + model_eqns = self.get("model_eqns") if model_eqns not in [2, 3, 4]: return - num_patches = self.get('num_patches', 0) - num_fluids = self.get('num_fluids', 1) - bubbles_euler = self.get('bubbles_euler', 'F') == 'T' - bubbles_lagrange = self.get('bubbles_lagrange', 'F') == 'T' + num_patches = self.get("num_patches", 0) + num_fluids = self.get("num_fluids", 1) + bubbles_euler = self.get("bubbles_euler", "F") == "T" + bubbles_lagrange = self.get("bubbles_lagrange", "F") == "T" # For bubbles_euler with single fluid, alpha is the void fraction # and does not need to sum to 1 @@ -2103,7 +1729,7 @@ def check_volume_fraction_sum(self): # pylint: disable=too-many-locals # IBM cases use alpha as a level-set indicator, not a physical # volume fraction, so the sum-to-1 constraint does not apply - num_ibs = self.get('num_ibs', 0) or 0 + num_ibs = self.get("num_ibs", 0) or 0 if num_ibs > 0: return @@ -2111,16 +1737,15 @@ def check_volume_fraction_sum(self): # pylint: disable=too-many-locals return for i in range(1, num_patches + 1): - geometry = self.get(f'patch_icpp({i})%geometry') + geometry = self.get(f"patch_icpp({i})%geometry") if geometry is None: continue # Skip special patches - hcid = self.get(f'patch_icpp({i})%hcid') + hcid = self.get(f"patch_icpp({i})%hcid") if hcid is not None: continue - alter_patches = [self.get(f'patch_icpp({i})%alter_patch({j})') == 'T' - for j in range(1, num_patches + 1)] + alter_patches = [self.get(f"patch_icpp({i})%alter_patch({j})") == "T" for j in range(1, num_patches + 1)] if any(alter_patches): continue @@ -2128,7 +1753,7 @@ def check_volume_fraction_sum(self): # pylint: disable=too-many-locals alphas = [] has_expression = False for j in range(1, num_fluids + 1): - alpha = self.get(f'patch_icpp({i})%alpha({j})') + alpha = self.get(f"patch_icpp({i})%alpha({j})") if alpha is None: has_expression = True break @@ -2141,8 +1766,7 @@ def check_volume_fraction_sum(self): # pylint: disable=too-many-locals continue alpha_sum = sum(alphas) - self.warn(abs(alpha_sum - 1.0) > 1e-6, - f"patch_icpp({i}): volume fractions sum to {alpha_sum:.8g}, expected 1.0") + self.warn(abs(alpha_sum - 1.0) > 1e-6, f"patch_icpp({i}): volume fractions sum to {alpha_sum:.8g}, expected 1.0") def check_alpha_rho_consistency(self): """Warns about inconsistent alpha/alpha_rho pairs. @@ -2151,43 +1775,38 @@ def check_alpha_rho_consistency(self): - alpha(j) = 0 but alpha_rho(j) != 0 (density in absent phase) - alpha(j) > 0 but alpha_rho(j) = 0 (present phase has zero density) """ - num_patches = self.get('num_patches', 0) - num_fluids = self.get('num_fluids', 1) + num_patches = self.get("num_patches", 0) + num_fluids = self.get("num_fluids", 1) if not self._is_numeric(num_patches) or num_patches <= 0 or not self._is_numeric(num_fluids): return for i in range(1, num_patches + 1): - geometry = self.get(f'patch_icpp({i})%geometry') + geometry = self.get(f"patch_icpp({i})%geometry") if geometry is None: continue # Skip special patches - hcid = self.get(f'patch_icpp({i})%hcid') + hcid = self.get(f"patch_icpp({i})%hcid") if hcid is not None: continue - alter_patches = [self.get(f'patch_icpp({i})%alter_patch({j})') == 'T' - for j in range(1, num_patches + 1)] + alter_patches = [self.get(f"patch_icpp({i})%alter_patch({j})") == "T" for j in range(1, num_patches + 1)] if any(alter_patches): continue for j in range(1, num_fluids + 1): - alpha = self.get(f'patch_icpp({i})%alpha({j})') - alpha_rho = self.get(f'patch_icpp({i})%alpha_rho({j})') + alpha = self.get(f"patch_icpp({i})%alpha({j})") + alpha_rho = self.get(f"patch_icpp({i})%alpha_rho({j})") if alpha is None or alpha_rho is None: continue if not self._is_numeric(alpha) or not self._is_numeric(alpha_rho): continue - self.warn(alpha == 0 and alpha_rho != 0, - f"patch_icpp({i}): alpha({j}) = 0 but alpha_rho({j}) = {alpha_rho} " - f"(density in absent phase)") - self.warn(alpha > 1e-10 and alpha_rho == 0, - f"patch_icpp({i}): alpha({j}) = {alpha} but alpha_rho({j}) = 0 " - f"(present phase has zero density)") + self.warn(alpha == 0 and alpha_rho != 0, f"patch_icpp({i}): alpha({j}) = 0 but alpha_rho({j}) = {alpha_rho} (density in absent phase)") + self.warn(alpha > 1e-10 and alpha_rho == 0, f"patch_icpp({i}): alpha({j}) = {alpha} but alpha_rho({j}) = 0 (present phase has zero density)") - def check_patch_within_domain(self): # pylint: disable=too-many-locals + def check_patch_within_domain(self): """Checks that centroid+length patches are not entirely outside the domain. Only applies to geometry types whose bounding box is fully determined @@ -2195,36 +1814,31 @@ def check_patch_within_domain(self): # pylint: disable=too-many-locals Skipped when grid stretching is active because the physical domain extents are transformed and the domain bounds are not directly comparable. """ - num_patches = self.get('num_patches', 0) + num_patches = self.get("num_patches", 0) if not self._is_numeric(num_patches) or num_patches <= 0: return # Skip when any grid stretching is active — domain bounds don't map # directly to physical coordinates in stretched grids - if (self.get('stretch_x', 'F') == 'T' or - self.get('stretch_y', 'F') == 'T' or - self.get('stretch_z', 'F') == 'T'): + if self.get("stretch_x", "F") == "T" or self.get("stretch_y", "F") == "T" or self.get("stretch_z", "F") == "T": return - x_beg = self.get('x_domain%beg') - x_end = self.get('x_domain%end') - n = self.get('n', 0) - p = self.get('p', 0) - y_beg = self.get('y_domain%beg') if self._is_numeric(n) and n > 0 else None - y_end = self.get('y_domain%end') if self._is_numeric(n) and n > 0 else None - z_beg = self.get('z_domain%beg') if self._is_numeric(p) and p > 0 else None - z_end = self.get('z_domain%end') if self._is_numeric(p) and p > 0 else None + x_beg = self.get("x_domain%beg") + x_end = self.get("x_domain%end") + n = self.get("n", 0) + p = self.get("p", 0) + y_beg = self.get("y_domain%beg") if self._is_numeric(n) and n > 0 else None + y_end = self.get("y_domain%end") if self._is_numeric(n) and n > 0 else None + z_beg = self.get("z_domain%beg") if self._is_numeric(p) and p > 0 else None + z_end = self.get("z_domain%end") if self._is_numeric(p) and p > 0 else None # Pre-check domain bounds are numeric (could be analytical expressions) - x_bounds_ok = (x_beg is not None and x_end is not None - and self._is_numeric(x_beg) and self._is_numeric(x_end)) - y_bounds_ok = (y_beg is not None and y_end is not None - and self._is_numeric(y_beg) and self._is_numeric(y_end)) - z_bounds_ok = (z_beg is not None and z_end is not None - and self._is_numeric(z_beg) and self._is_numeric(z_end)) + x_bounds_ok = x_beg is not None and x_end is not None and self._is_numeric(x_beg) and self._is_numeric(x_end) + y_bounds_ok = y_beg is not None and y_end is not None and self._is_numeric(y_beg) and self._is_numeric(y_end) + z_bounds_ok = z_beg is not None and z_end is not None and self._is_numeric(z_beg) and self._is_numeric(z_end) for i in range(1, num_patches + 1): - geometry = self.get(f'patch_icpp({i})%geometry') + geometry = self.get(f"patch_icpp({i})%geometry") if geometry is None: continue @@ -2233,39 +1847,30 @@ def check_patch_within_domain(self): # pylint: disable=too-many-locals if geometry not in [1, 3, 9]: continue - xc = self.get(f'patch_icpp({i})%x_centroid') - lx = self.get(f'patch_icpp({i})%length_x') + xc = self.get(f"patch_icpp({i})%x_centroid") + lx = self.get(f"patch_icpp({i})%length_x") has_x = xc is not None and lx is not None - if (has_x and x_bounds_ok - and self._is_numeric(xc) and self._is_numeric(lx)): + if has_x and x_bounds_ok and self._is_numeric(xc) and self._is_numeric(lx): patch_x_lo = xc - lx / 2.0 patch_x_hi = xc + lx / 2.0 - self.prohibit(patch_x_hi < x_beg or patch_x_lo > x_end, - f"patch_icpp({i}): x-extent [{patch_x_lo}, {patch_x_hi}] " - f"is entirely outside domain [{x_beg}, {x_end}]") + self.prohibit(patch_x_hi < x_beg or patch_x_lo > x_end, f"patch_icpp({i}): x-extent [{patch_x_lo}, {patch_x_hi}] is entirely outside domain [{x_beg}, {x_end}]") if geometry in [3, 9] and y_bounds_ok: - yc = self.get(f'patch_icpp({i})%y_centroid') - ly = self.get(f'patch_icpp({i})%length_y') - if (yc is not None and ly is not None - and self._is_numeric(yc) and self._is_numeric(ly)): + yc = self.get(f"patch_icpp({i})%y_centroid") + ly = self.get(f"patch_icpp({i})%length_y") + if yc is not None and ly is not None and self._is_numeric(yc) and self._is_numeric(ly): patch_y_lo = yc - ly / 2.0 patch_y_hi = yc + ly / 2.0 - self.prohibit(patch_y_hi < y_beg or patch_y_lo > y_end, - f"patch_icpp({i}): y-extent [{patch_y_lo}, {patch_y_hi}] " - f"is entirely outside domain [{y_beg}, {y_end}]") + self.prohibit(patch_y_hi < y_beg or patch_y_lo > y_end, f"patch_icpp({i}): y-extent [{patch_y_lo}, {patch_y_hi}] is entirely outside domain [{y_beg}, {y_end}]") if geometry == 9 and z_bounds_ok: - zc = self.get(f'patch_icpp({i})%z_centroid') - lz = self.get(f'patch_icpp({i})%length_z') - if (zc is not None and lz is not None - and self._is_numeric(zc) and self._is_numeric(lz)): + zc = self.get(f"patch_icpp({i})%z_centroid") + lz = self.get(f"patch_icpp({i})%length_z") + if zc is not None and lz is not None and self._is_numeric(zc) and self._is_numeric(lz): patch_z_lo = zc - lz / 2.0 patch_z_hi = zc + lz / 2.0 - self.prohibit(patch_z_hi < z_beg or patch_z_lo > z_end, - f"patch_icpp({i}): z-extent [{patch_z_lo}, {patch_z_hi}] " - f"is entirely outside domain [{z_beg}, {z_end}]") + self.prohibit(patch_z_hi < z_beg or patch_z_lo > z_end, f"patch_icpp({i}): z-extent [{patch_z_lo}, {patch_z_hi}] is entirely outside domain [{z_beg}, {z_end}]") def check_eos_parameter_sanity(self): """Warns if EOS gamma parameter looks like raw physical gamma. @@ -2277,26 +1882,22 @@ def check_eos_parameter_sanity(self): - gamma < 0.1 implies physical gamma > 11 (unusual) - gamma > 1000 implies physical gamma ≈ 1.001 (unusual) """ - num_fluids = self.get('num_fluids') - model_eqns = self.get('model_eqns') + num_fluids = self.get("num_fluids") + model_eqns = self.get("model_eqns") if not self._is_numeric(num_fluids) or model_eqns == 1: return for i in range(1, int(num_fluids) + 1): - gamma = self.get(f'fluid_pp({i})%gamma') + gamma = self.get(f"fluid_pp({i})%gamma") if gamma is None or not self._is_numeric(gamma) or gamma <= 0: continue # gamma = 1/(physical_gamma - 1), so physical_gamma = 1/gamma + 1 physical_gamma = 1.0 / gamma + 1.0 - self.warn(gamma < 0.1, - f"fluid_pp({i})%gamma = {gamma} implies physical gamma = {physical_gamma:.2f} " - f"(unusually high). MFC uses the transformed parameter Gamma = 1/(gamma-1)") - self.warn(gamma > 1000, - f"fluid_pp({i})%gamma = {gamma} implies physical gamma = {physical_gamma:.6f} " - f"(very close to 1). Did you enter the physical gamma instead of 1/(gamma-1)?") + self.warn(gamma < 0.1, f"fluid_pp({i})%gamma = {gamma} implies physical gamma = {physical_gamma:.2f} (unusually high). MFC uses the transformed parameter Gamma = 1/(gamma-1)") + self.warn(gamma > 1000, f"fluid_pp({i})%gamma = {gamma} implies physical gamma = {physical_gamma:.6f} (very close to 1). Did you enter the physical gamma instead of 1/(gamma-1)?") def check_velocity_components(self): """Checks that velocity components are not set in inactive dimensions. @@ -2306,10 +1907,10 @@ def check_velocity_components(self): physically meaningful even in 1D (they carry transverse momentum coupled to the magnetic field). """ - n = self.get('n', 0) - p = self.get('p', 0) - num_patches = self.get('num_patches', 0) - mhd = self.get('mhd', 'F') == 'T' + n = self.get("n", 0) + p = self.get("p", 0) + num_patches = self.get("num_patches", 0) + mhd = self.get("mhd", "F") == "T" if not self._is_numeric(num_patches) or num_patches <= 0: return @@ -2322,21 +1923,19 @@ def check_velocity_components(self): p_is_2d = self._is_numeric(p) and p == 0 for i in range(1, num_patches + 1): - geometry = self.get(f'patch_icpp({i})%geometry') + geometry = self.get(f"patch_icpp({i})%geometry") if geometry is None: continue if n_is_1d: - vel2 = self.get(f'patch_icpp({i})%vel(2)') + vel2 = self.get(f"patch_icpp({i})%vel(2)") if vel2 is not None and self._is_numeric(vel2): - self.prohibit(vel2 != 0, - f"patch_icpp({i})%vel(2) = {vel2} but n = 0 (1D simulation)") + self.prohibit(vel2 != 0, f"patch_icpp({i})%vel(2) = {vel2} but n = 0 (1D simulation)") if p_is_2d: - vel3 = self.get(f'patch_icpp({i})%vel(3)') + vel3 = self.get(f"patch_icpp({i})%vel(3)") if vel3 is not None and self._is_numeric(vel3): - self.prohibit(vel3 != 0, - f"patch_icpp({i})%vel(3) = {vel3} but p = 0 (1D/2D simulation)") + self.prohibit(vel3 != 0, f"patch_icpp({i})%vel(3) = {vel3} but p = 0 (1D/2D simulation)") # =================================================================== # Build-Flag Compatibility Checks @@ -2349,21 +1948,14 @@ def check_build_flags(self): MFC binaries were compiled (--mpi/--no-mpi, --single, etc.) before any binary is invoked. """ - parallel_io = self.get('parallel_io', 'F') == 'T' - self.prohibit( - parallel_io and not CFG().mpi, - "parallel_io = T requires MFC built with --mpi" - ) + parallel_io = self.get("parallel_io", "F") == "T" + self.prohibit(parallel_io and not CFG().mpi, "parallel_io = T requires MFC built with --mpi") def check_geometry_precision_simulation(self): """Checks that 3D cylindrical geometry is not used with --single builds.""" - cyl_coord = self.get('cyl_coord', 'F') == 'T' - p = self.get('p', 0) - self.prohibit( - CFG().single and cyl_coord and p > 0, - "Fully 3D cylindrical geometry (cyl_coord = T, p > 0) is not supported " - "in single precision (--single)" - ) + cyl_coord = self.get("cyl_coord", "F") == "T" + p = self.get("p", 0) + self.prohibit(CFG().single and cyl_coord and p > 0, "Fully 3D cylindrical geometry (cyl_coord = T, p > 0) is not supported in single precision (--single)") # =================================================================== # Main Validation Entry Points @@ -2452,7 +2044,7 @@ def validate_post_process(self): self.check_surface_tension_post() self.check_no_flow_variables() - def validate(self, stage: str = 'simulation'): + def validate(self, stage: str = "simulation"): """Main validation method Args: @@ -2465,11 +2057,11 @@ def validate(self, stage: str = 'simulation'): self.errors = [] self.warnings = [] - if stage == 'simulation': + if stage == "simulation": self.validate_simulation() - elif stage == 'pre_process': + elif stage == "pre_process": self.validate_pre_process() - elif stage == 'post_process': + elif stage == "post_process": self.validate_post_process() else: # No stage-specific constraints for auxiliary targets like 'syscheck'. @@ -2500,15 +2092,14 @@ def _format_errors(self) -> str: # Auto-generate hints from CONSTRAINTS with value_labels for param_name, constraint in CONSTRAINTS.items(): - if not re.search(r'\b' + re.escape(param_name.lower()) + r'\b', err_lower): + if not re.search(r"\b" + re.escape(param_name.lower()) + r"\b", err_lower): continue choices = constraint.get("choices") if not choices: continue labels = constraint.get("value_labels", {}) if labels: - items = [f"{v} ({labels[v]})" if v in labels else str(v) - for v in choices] + items = [f"{v} ({labels[v]})" if v in labels else str(v) for v in choices] hint = f"Valid values: {', '.join(items)}" else: hint = f"Valid values: {choices}" @@ -2521,7 +2112,7 @@ def _format_errors(self) -> str: return "\n".join(lines) -def validate_case_constraints(params: Dict[str, Any], stage: str = 'simulation') -> List[str]: +def validate_case_constraints(params: Dict[str, Any], stage: str = "simulation") -> List[str]: """Convenience function to validate case parameters Args: diff --git a/toolchain/mfc/clean.py b/toolchain/mfc/clean.py index 3d9c2ff096..9c1f7436e8 100644 --- a/toolchain/mfc/clean.py +++ b/toolchain/mfc/clean.py @@ -5,8 +5,8 @@ import os import shutil -from .printer import cons from .common import MFC_BUILD_DIR +from .printer import cons def clean(): diff --git a/toolchain/mfc/cli/__init__.py b/toolchain/mfc/cli/__init__.py index d8b5178a5b..ee63d2e815 100644 --- a/toolchain/mfc/cli/__init__.py +++ b/toolchain/mfc/cli/__init__.py @@ -11,16 +11,16 @@ """ from .schema import ( + ArgAction, + Argument, CLISchema, Command, - Argument, - Positional, - Example, CommonArgumentSet, - MutuallyExclusiveGroup, - ArgAction, - CompletionType, Completion, + CompletionType, + Example, + MutuallyExclusiveGroup, + Positional, ) __all__ = [ diff --git a/toolchain/mfc/cli/argparse_gen.py b/toolchain/mfc/cli/argparse_gen.py index b95087b83b..6859f61077 100644 --- a/toolchain/mfc/cli/argparse_gen.py +++ b/toolchain/mfc/cli/argparse_gen.py @@ -8,10 +8,7 @@ import dataclasses from typing import Dict, Tuple -from .schema import ( - CLISchema, Command, Argument, Positional, - ArgAction, CommonArgumentSet -) +from .schema import ArgAction, Argument, CLISchema, Command, CommonArgumentSet, Positional def _action_to_argparse(action: ArgAction) -> str: @@ -61,7 +58,7 @@ def _add_positional(parser: argparse.ArgumentParser, pos: Positional): "metavar": pos.name.upper(), } - if pos.type != str: + if pos.type is not str: kwargs["type"] = pos.type if pos.nargs is not None: kwargs["nargs"] = pos.nargs @@ -80,52 +77,29 @@ def _add_mfc_config_arguments(parser: argparse.ArgumentParser, config): This handles --mpi/--no-mpi, --gpu/--no-gpu, etc. from MFCConfig dataclass. """ # Import here to avoid circular dependency - from ..state import gpuConfigOptions # pylint: disable=import-outside-toplevel + from ..state import gpuConfigOptions for f in dataclasses.fields(config): - if f.name == 'gpu': + if f.name == "gpu": parser.add_argument( f"--{f.name}", action="store", - nargs='?', + nargs="?", const=gpuConfigOptions.ACC.value, default=gpuConfigOptions.NONE.value, dest=f.name, choices=[e.value for e in gpuConfigOptions], - help=f"Turn the {f.name} option to OpenACC or OpenMP." - ) - parser.add_argument( - f"--no-{f.name}", - action="store_const", - const=gpuConfigOptions.NONE.value, - dest=f.name, - help=f"Turn the {f.name} option OFF." + help=f"Turn the {f.name} option to OpenACC or OpenMP.", ) + parser.add_argument(f"--no-{f.name}", action="store_const", const=gpuConfigOptions.NONE.value, dest=f.name, help=f"Turn the {f.name} option OFF.") else: - parser.add_argument( - f"--{f.name}", - action="store_true", - help=f"Turn the {f.name} option ON." - ) - parser.add_argument( - f"--no-{f.name}", - action="store_false", - dest=f.name, - help=f"Turn the {f.name} option OFF." - ) + parser.add_argument(f"--{f.name}", action="store_true", help=f"Turn the {f.name} option ON.") + parser.add_argument(f"--no-{f.name}", action="store_false", dest=f.name, help=f"Turn the {f.name} option OFF.") - parser.set_defaults(**{ - f.name: getattr(config, f.name) - for f in dataclasses.fields(config) - }) + parser.set_defaults(**{f.name: getattr(config, f.name) for f in dataclasses.fields(config)}) -def _add_common_arguments( - parser: argparse.ArgumentParser, - command: Command, - common_sets: Dict[str, CommonArgumentSet], - config=None -): +def _add_common_arguments(parser: argparse.ArgumentParser, command: Command, common_sets: Dict[str, CommonArgumentSet], config=None): """Add common arguments to a command parser.""" for set_name in command.include_common: common_set = common_sets.get(set_name) @@ -140,12 +114,7 @@ def _add_common_arguments( _add_argument(parser, arg) -def _add_command_subparser( - subparsers, - cmd: Command, - common_sets: Dict[str, CommonArgumentSet], - config -) -> argparse.ArgumentParser: +def _add_command_subparser(subparsers, cmd: Command, common_sets: Dict[str, CommonArgumentSet], config) -> argparse.ArgumentParser: """Add a single command's subparser and return it.""" subparser = subparsers.add_parser( name=cmd.name, @@ -190,7 +159,7 @@ def _add_command_subparser( def generate_parser( schema: CLISchema, - config=None # MFCConfig instance + config=None, # MFCConfig instance ) -> Tuple[argparse.ArgumentParser, Dict[str, argparse.ArgumentParser]]: """ Generate complete argparse parser from schema. diff --git a/toolchain/mfc/cli/commands.py b/toolchain/mfc/cli/commands.py index d4b34df3d8..ea2c2ce241 100644 --- a/toolchain/mfc/cli/commands.py +++ b/toolchain/mfc/cli/commands.py @@ -10,38 +10,24 @@ When adding a new command or option, ONLY modify this file. Then run `./mfc.sh generate` to update completions. """ -# pylint: disable=too-many-lines - -from .schema import ( - CLISchema, Command, Argument, Positional, Example, - ArgAction, Completion, CompletionType, - CommonArgumentSet, MutuallyExclusiveGroup -) +from .schema import ArgAction, Argument, CLISchema, Command, CommonArgumentSet, Completion, CompletionType, Example, MutuallyExclusiveGroup, Positional # ============================================================================= # CONSTANTS (shared with other modules) # ============================================================================= -TARGET_NAMES = [ - 'fftw', 'hdf5', 'silo', 'lapack', 'hipfort', - 'pre_process', 'simulation', 'post_process', - 'syscheck', 'documentation' -] +TARGET_NAMES = ["fftw", "hdf5", "silo", "lapack", "hipfort", "pre_process", "simulation", "post_process", "syscheck", "documentation"] -DEFAULT_TARGET_NAMES = ['pre_process', 'simulation', 'post_process'] +DEFAULT_TARGET_NAMES = ["pre_process", "simulation", "post_process"] -TEMPLATE_NAMES = [ - 'bridges2', 'carpenter', 'carpenter-cray', 'default', - 'delta', 'deltaai', 'frontier', 'hipergator', 'nautilus', - 'oscar', 'phoenix', 'phoenix-bench', 'santis', 'tuo' -] +TEMPLATE_NAMES = ["bridges2", "carpenter", "carpenter-cray", "default", "delta", "deltaai", "frontier", "hipergator", "nautilus", "oscar", "phoenix", "phoenix-bench", "santis", "tuo"] -GPU_OPTIONS = ['acc', 'mp'] +GPU_OPTIONS = ["acc", "mp"] -ENGINE_OPTIONS = ['interactive', 'batch'] +ENGINE_OPTIONS = ["interactive", "batch"] -MPI_BINARIES = ['mpirun', 'jsrun', 'srun', 'mpiexec'] +MPI_BINARIES = ["mpirun", "jsrun", "srun", "mpiexec"] # ============================================================================= @@ -62,7 +48,7 @@ metavar="TARGET", completion=Completion(type=CompletionType.CHOICES, choices=TARGET_NAMES), ), - ] + ], ) COMMON_JOBS = CommonArgumentSet( @@ -76,7 +62,7 @@ default=1, metavar="JOBS", ), - ] + ], ) COMMON_VERBOSE = CommonArgumentSet( @@ -89,7 +75,7 @@ action=ArgAction.COUNT, default=0, ), - ] + ], ) COMMON_DEBUG_LOG = CommonArgumentSet( @@ -102,7 +88,7 @@ action=ArgAction.STORE_TRUE, dest="debug_log", ), - ] + ], ) COMMON_GPUS = CommonArgumentSet( @@ -116,7 +102,7 @@ type=int, default=None, ), - ] + ], ) # MFCConfig flags are handled specially in argparse_gen.py @@ -460,28 +446,30 @@ ), ], mutually_exclusive=[ - MutuallyExclusiveGroup(arguments=[ - Argument( - name="generate", - help="(Test Generation) Generate golden files.", - action=ArgAction.STORE_TRUE, - default=False, - ), - Argument( - name="add-new-variables", - help="(Test Generation) If new variables are found in D/ when running tests, add them to the golden files.", - action=ArgAction.STORE_TRUE, - default=False, - dest="add_new_variables", - ), - Argument( - name="remove-old-tests", - help="(Test Generation) Delete test directories that are no longer needed.", - action=ArgAction.STORE_TRUE, - default=False, - dest="remove_old_tests", - ), - ]), + MutuallyExclusiveGroup( + arguments=[ + Argument( + name="generate", + help="(Test Generation) Generate golden files.", + action=ArgAction.STORE_TRUE, + default=False, + ), + Argument( + name="add-new-variables", + help="(Test Generation) If new variables are found in D/ when running tests, add them to the golden files.", + action=ArgAction.STORE_TRUE, + default=False, + dest="add_new_variables", + ), + Argument( + name="remove-old-tests", + help="(Test Generation) Delete test directories that are no longer needed.", + action=ArgAction.STORE_TRUE, + default=False, + dest="remove_old_tests", + ), + ] + ), ], examples=[ Example("./mfc.sh test", "Run all tests"), @@ -718,20 +706,20 @@ LINT_COMMAND = Command( name="lint", help="Lints and tests all toolchain code.", - description="Run pylint and unit tests on MFC's toolchain Python code.", + description="Run ruff and unit tests on MFC's toolchain Python code.", arguments=[ Argument( name="no-test", - help="Skip running unit tests (only run pylint).", + help="Skip running unit tests (only run ruff).", action=ArgAction.STORE_TRUE, ), ], examples=[ - Example("./mfc.sh lint", "Run pylint and unit tests"), - Example("./mfc.sh lint --no-test", "Run only pylint (skip unit tests)"), + Example("./mfc.sh lint", "Run ruff and unit tests"), + Example("./mfc.sh lint --no-test", "Run only ruff (skip unit tests)"), ], key_options=[ - ("--no-test", "Skip unit tests, only run pylint"), + ("--no-test", "Skip unit tests, only run ruff"), ], ) @@ -918,7 +906,7 @@ "Use --list-steps to see available timesteps." ), type=str, - default='last', + default="last", metavar="STEP", ), Argument( @@ -943,33 +931,102 @@ name="cmap", help="Matplotlib colormap name (--png, --mp4 only).", type=str, - default='viridis', + default="viridis", metavar="CMAP", - completion=Completion(type=CompletionType.CHOICES, choices=[ - # Perceptually uniform sequential - "viridis", "plasma", "inferno", "magma", "cividis", - # Diverging - "RdBu", "RdYlBu", "RdYlGn", "RdGy", "coolwarm", "bwr", "seismic", - "PiYG", "PRGn", "BrBG", "PuOr", "Spectral", - # Sequential - "Blues", "Greens", "Oranges", "Reds", "Purples", "Greys", - "YlOrRd", "YlOrBr", "YlGn", "YlGnBu", "GnBu", "BuGn", - "BuPu", "PuBu", "PuBuGn", "PuRd", "RdPu", - # Sequential 2 - "hot", "afmhot", "gist_heat", "copper", - "bone", "gray", "pink", "spring", "summer", "autumn", "winter", "cool", - "binary", "gist_yarg", "gist_gray", - # Cyclic - "twilight", "twilight_shifted", "hsv", - # Qualitative - "tab10", "tab20", "tab20b", "tab20c", - "Set1", "Set2", "Set3", "Paired", "Accent", "Dark2", "Pastel1", "Pastel2", - # Miscellaneous - "turbo", "jet", "rainbow", "nipy_spectral", "gist_ncar", - "gist_rainbow", "gist_stern", "gist_earth", "ocean", "terrain", - "gnuplot", "gnuplot2", "CMRmap", "cubehelix", "brg", "flag", "prism", - "Wistia", - ]), + completion=Completion( + type=CompletionType.CHOICES, + choices=[ + # Perceptually uniform sequential + "viridis", + "plasma", + "inferno", + "magma", + "cividis", + # Diverging + "RdBu", + "RdYlBu", + "RdYlGn", + "RdGy", + "coolwarm", + "bwr", + "seismic", + "PiYG", + "PRGn", + "BrBG", + "PuOr", + "Spectral", + # Sequential + "Blues", + "Greens", + "Oranges", + "Reds", + "Purples", + "Greys", + "YlOrRd", + "YlOrBr", + "YlGn", + "YlGnBu", + "GnBu", + "BuGn", + "BuPu", + "PuBu", + "PuBuGn", + "PuRd", + "RdPu", + # Sequential 2 + "hot", + "afmhot", + "gist_heat", + "copper", + "bone", + "gray", + "pink", + "spring", + "summer", + "autumn", + "winter", + "cool", + "binary", + "gist_yarg", + "gist_gray", + # Cyclic + "twilight", + "twilight_shifted", + "hsv", + # Qualitative + "tab10", + "tab20", + "tab20b", + "tab20c", + "Set1", + "Set2", + "Set3", + "Paired", + "Accent", + "Dark2", + "Pastel1", + "Pastel2", + # Miscellaneous + "turbo", + "jet", + "rainbow", + "nipy_spectral", + "gist_ncar", + "gist_rainbow", + "gist_stern", + "gist_earth", + "ocean", + "terrain", + "gnuplot", + "gnuplot2", + "CMRmap", + "cubehelix", + "brg", + "flag", + "prism", + "Wistia", + ], + ), ), Argument( name="vmin", @@ -996,7 +1053,7 @@ name="slice-axis", help="Axis for 3D slice (--png, --mp4 only).", type=str, - default='z', + default="z", choices=["x", "y", "z"], dest="slice_axis", completion=Completion(type=CompletionType.CHOICES, choices=["x", "y", "z"]), @@ -1054,11 +1111,7 @@ Argument( name="interactive", short="i", - help=( - "Launch an interactive Dash web UI in your browser. " - "Loads all timesteps (or the set given by --step) and lets you " - "scrub through them and switch variables live." - ), + help=("Launch an interactive Dash web UI in your browser. Loads all timesteps (or the set given by --step) and lets you scrub through them and switch variables live."), action=ArgAction.STORE_TRUE, default=False, ), @@ -1077,10 +1130,7 @@ ), Argument( name="png", - help=( - "Save PNG image(s) to the output directory instead of " - "launching the terminal UI." - ), + help=("Save PNG image(s) to the output directory instead of launching the terminal UI."), action=ArgAction.STORE_TRUE, default=False, ), @@ -1250,7 +1300,6 @@ running, and cleaning of MFC in various configurations on all supported platforms. \ The README documents this tool and its various commands in more detail. To get \ started, run `./mfc.sh build -h`.""", - arguments=[ Argument( name="help", @@ -1259,7 +1308,6 @@ action=ArgAction.STORE_TRUE, ), ], - commands=[ BUILD_COMMAND, RUN_COMMAND, @@ -1284,7 +1332,6 @@ COUNT_COMMAND, COUNT_DIFF_COMMAND, ], - common_sets=[ COMMON_TARGETS, COMMON_JOBS, @@ -1293,7 +1340,6 @@ COMMON_GPUS, COMMON_MFC_CONFIG, ], - help_topics=HELP_TOPICS, ) @@ -1308,6 +1354,7 @@ for alias in cmd.aliases: COMMAND_ALIASES[alias] = cmd.name + # Commands dict (replaces COMMANDS in user_guide.py) def get_commands_dict(): """Generate COMMANDS dict from schema for user_guide.py compatibility.""" @@ -1321,4 +1368,5 @@ def get_commands_dict(): for cmd in MFC_CLI_SCHEMA.commands } + COMMANDS = get_commands_dict() diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index 7a286bc981..de1ac0cd98 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -6,8 +6,8 @@ """ from typing import List, Set -from .schema import CLISchema, Command, CompletionType +from .schema import CLISchema, Command, CompletionType # Mapping of completion types to bash completion expressions _BASH_COMPLETION_MAP = { @@ -31,16 +31,26 @@ def _collect_all_options(cmd: Command, schema: CLISchema) -> List[str]: # MFC config flags if common_set.mfc_config_flags: - options.update([ - "--mpi", "--no-mpi", - "--gpu", "--no-gpu", - "--debug", "--no-debug", - "--gcov", "--no-gcov", - "--unified", "--no-unified", - "--single", "--no-single", - "--mixed", "--no-mixed", - "--fastmath", "--no-fastmath", - ]) + options.update( + [ + "--mpi", + "--no-mpi", + "--gpu", + "--no-gpu", + "--debug", + "--no-debug", + "--gcov", + "--no-gcov", + "--unified", + "--no-unified", + "--single", + "--no-single", + "--mixed", + "--no-mixed", + "--fastmath", + "--no-fastmath", + ] + ) else: for arg in common_set.arguments: if arg.short: @@ -86,9 +96,7 @@ def _generate_bash_prev_cases(cmd: Command, schema: CLISchema) -> List[str]: """Generate bash prev-based completion cases for a command.""" lines = [] has_prev_cases = False - completable_types = (CompletionType.CHOICES, CompletionType.FILES_PY, - CompletionType.FILES_PACK, CompletionType.FILES, - CompletionType.DIRECTORIES, CompletionType.FILES_YAML) + completable_types = (CompletionType.CHOICES, CompletionType.FILES_PY, CompletionType.FILES_PACK, CompletionType.FILES, CompletionType.DIRECTORIES, CompletionType.FILES_YAML) all_args = _collect_all_args(cmd, schema) @@ -103,29 +111,29 @@ def _generate_bash_prev_cases(cmd: Command, schema: CLISchema) -> List[str]: if multivalue_args: # Generate backward-scanning logic for multi-value args - lines.append(' # Check for multi-value arguments by scanning backwards') - lines.append(' local i') - lines.append(' for ((i=COMP_CWORD-1; i>=2; i--)); do') + lines.append(" # Check for multi-value arguments by scanning backwards") + lines.append(" local i") + lines.append(" for ((i=COMP_CWORD-1; i>=2; i--)); do") lines.append(' case "${COMP_WORDS[i]}" in') for arg in multivalue_args: - flags = [f'-{arg.short}'] if arg.short else [] - flags.append(f'--{arg.name}') - lines.append(f' {"|".join(flags)})') + flags = [f"-{arg.short}"] if arg.short else [] + flags.append(f"--{arg.name}") + lines.append(f" {'|'.join(flags)})") comp_choices = arg.completion.choices or arg.choices completion_code = _bash_completion_for_type(arg.completion.type, comp_choices) if completion_code: - lines.append(f' {completion_code}') - lines.append(' return 0') - lines.append(' ;;') + lines.append(f" {completion_code}") + lines.append(" return 0") + lines.append(" ;;") # Stop scanning if we hit any other flag - lines.append(' -*)') - lines.append(' break') - lines.append(' ;;') - lines.append(' esac') - lines.append(' done') - lines.append('') + lines.append(" -*)") + lines.append(" break") + lines.append(" ;;") + lines.append(" esac") + lines.append(" done") + lines.append("") # Then handle single-value arguments with prev-based completion for arg in all_args: @@ -139,19 +147,19 @@ def _generate_bash_prev_cases(cmd: Command, schema: CLISchema) -> List[str]: lines.append(' case "${prev}" in') has_prev_cases = True - flags = [f'-{arg.short}'] if arg.short else [] - flags.append(f'--{arg.name}') + flags = [f"-{arg.short}"] if arg.short else [] + flags.append(f"--{arg.name}") - lines.append(f' {"|".join(flags)})') + lines.append(f" {'|'.join(flags)})") comp_choices = arg.completion.choices or arg.choices completion_code = _bash_completion_for_type(arg.completion.type, comp_choices) if completion_code: - lines.append(f' {completion_code}') - lines.append(' return 0') - lines.append(' ;;') + lines.append(f" {completion_code}") + lines.append(" return 0") + lines.append(" ;;") if has_prev_cases: - lines.append(' esac') + lines.append(" esac") return lines @@ -162,18 +170,18 @@ def _generate_bash_command_case(cmd: Command, schema: CLISchema) -> List[str]: # Include aliases in case pattern patterns = [cmd.name] + cmd.aliases - lines.append(f' {"|".join(patterns)})') + lines.append(f" {'|'.join(patterns)})") options = _collect_all_options(cmd, schema) # Handle subcommands (like packer pack, packer compare) if cmd.subcommands: - lines.append(' if [[ ${COMP_CWORD} -eq 2 ]]; then') + lines.append(" if [[ ${COMP_CWORD} -eq 2 ]]; then") subcmd_names = [sc.name for sc in cmd.subcommands] lines.append(f' COMPREPLY=( $(compgen -W "{" ".join(subcmd_names)}" -- "${{cur}}") )') - lines.append(' return 0') - lines.append(' fi') - lines.append(' ;;') + lines.append(" return 0") + lines.append(" fi") + lines.append(" ;;") return lines # Generate prev-based completion @@ -186,23 +194,23 @@ def _generate_bash_command_case(cmd: Command, schema: CLISchema) -> List[str]: lines.append(' COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )') if cmd.positionals and cmd.positionals[0].completion.type != CompletionType.NONE: - lines.append(' else') + lines.append(" else") pos = cmd.positionals[0] comp_choices = pos.completion.choices or pos.choices completion_code = _bash_completion_for_type(pos.completion.type, comp_choices) if completion_code: - lines.append(f' {completion_code}') + lines.append(f" {completion_code}") - lines.append(' fi') + lines.append(" fi") elif cmd.positionals and cmd.positionals[0].completion.type != CompletionType.NONE: pos = cmd.positionals[0] comp_choices = pos.completion.choices or pos.choices completion_code = _bash_completion_for_type(pos.completion.type, comp_choices) if completion_code: - lines.append(f' {completion_code}') + lines.append(f" {completion_code}") - lines.append(' return 0') - lines.append(' ;;') + lines.append(" return 0") + lines.append(" ;;") return lines @@ -211,26 +219,26 @@ def generate_bash_completion(schema: CLISchema) -> str: commands = schema.get_all_command_names() lines = [ - '#!/usr/bin/env bash', - '# AUTO-GENERATED from cli/commands.py - Do not edit manually', - '# Regenerate with: ./mfc.sh generate', - '', - '_mfc_completions() {', - ' local cur prev command', - ' COMPREPLY=()', + "#!/usr/bin/env bash", + "# AUTO-GENERATED from cli/commands.py - Do not edit manually", + "# Regenerate with: ./mfc.sh generate", + "", + "_mfc_completions() {", + " local cur prev command", + " COMPREPLY=()", ' cur="${COMP_WORDS[COMP_CWORD]}"', ' prev="${COMP_WORDS[COMP_CWORD-1]}"', - '', + "", f' local commands="{" ".join(sorted(commands))}"', - '', - ' # First argument - complete commands', - ' if [[ ${COMP_CWORD} -eq 1 ]]; then', + "", + " # First argument - complete commands", + " if [[ ${COMP_CWORD} -eq 1 ]]; then", ' COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") )', - ' return 0', - ' fi', - '', + " return 0", + " fi", + "", ' local command="${COMP_WORDS[1]}"', - '', + "", ' case "${command}" in', ] @@ -239,20 +247,22 @@ def generate_bash_completion(schema: CLISchema) -> str: continue lines.extend(_generate_bash_command_case(cmd, schema)) - lines.extend([ - ' esac', - '', - ' return 0', - '}', - '', - '# -o filenames: handle escaping/slashes for file completions', - '# Removed -o bashdefault to prevent unwanted directory fallback', - 'complete -o filenames -F _mfc_completions ./mfc.sh', - 'complete -o filenames -F _mfc_completions mfc.sh', - 'complete -o filenames -F _mfc_completions mfc', - ]) + lines.extend( + [ + " esac", + "", + " return 0", + "}", + "", + "# -o filenames: handle escaping/slashes for file completions", + "# Removed -o bashdefault to prevent unwanted directory fallback", + "complete -o filenames -F _mfc_completions ./mfc.sh", + "complete -o filenames -F _mfc_completions mfc.sh", + "complete -o filenames -F _mfc_completions mfc", + ] + ) - return '\n'.join(lines) + return "\n".join(lines) def _zsh_completion_for_positional(pos, index: int) -> str: @@ -264,11 +274,11 @@ def _zsh_completion_for_positional(pos, index: int) -> str: completion = ':_files -g "*.pack"' elif pos.completion.type == CompletionType.CHOICES: choices = pos.completion.choices or pos.choices or [] - completion = f':({" ".join(choices)})' + completion = f":({' '.join(choices)})" elif pos.completion.type == CompletionType.DIRECTORIES: - completion = ':_files -/' + completion = ":_files -/" elif pos.completion.type == CompletionType.FILES: - completion = ':_files' + completion = ":_files" help_text = pos.help.replace("'", "").replace("[", "").replace("]", "")[:120] return f"'{index}:{help_text}{completion}'" @@ -282,15 +292,15 @@ def _zsh_completion_for_arg(arg) -> str: if arg.completion.type == CompletionType.CHOICES: choices = arg.completion.choices or arg.choices or [] - return f'{label}:({" ".join(str(c) for c in choices)})' + return f"{label}:({' '.join(str(c) for c in choices)})" if arg.completion.type == CompletionType.FILES_PY: return f'{label}:_files -g "*.py"' if arg.completion.type == CompletionType.FILES_PACK: return f'{label}:_files -g "*.pack"' if arg.completion.type == CompletionType.FILES: - return f'{label}:_files' + return f"{label}:_files" if arg.completion.type == CompletionType.DIRECTORIES: - return f'{label}:_files -/' + return f"{label}:_files -/" return "" @@ -314,24 +324,26 @@ def _generate_zsh_command_args(cmd: Command, schema: CLISchema) -> List[str]: continue if common_set.mfc_config_flags: - arg_lines.extend([ - "'--mpi[Enable MPI]'", - "'--no-mpi[Disable MPI]'", - "'--gpu[Enable GPU]:mode:(acc mp)'", - "'--no-gpu[Disable GPU]'", - "'--debug[Build with debug compiler flags (for MFC code)]'", - "'--no-debug[Build without debug flags]'", - "'--gcov[Enable gcov coverage]'", - "'--no-gcov[Disable gcov coverage]'", - "'--unified[Enable unified memory]'", - "'--no-unified[Disable unified memory]'", - "'--single[Enable single precision]'", - "'--no-single[Disable single precision]'", - "'--mixed[Enable mixed precision]'", - "'--no-mixed[Disable mixed precision]'", - "'--fastmath[Enable fast math]'", - "'--no-fastmath[Disable fast math]'", - ]) + arg_lines.extend( + [ + "'--mpi[Enable MPI]'", + "'--no-mpi[Disable MPI]'", + "'--gpu[Enable GPU]:mode:(acc mp)'", + "'--no-gpu[Disable GPU]'", + "'--debug[Build with debug compiler flags (for MFC code)]'", + "'--no-debug[Build without debug flags]'", + "'--gcov[Enable gcov coverage]'", + "'--no-gcov[Disable gcov coverage]'", + "'--unified[Enable unified memory]'", + "'--no-unified[Disable unified memory]'", + "'--single[Enable single precision]'", + "'--no-single[Disable single precision]'", + "'--mixed[Enable mixed precision]'", + "'--no-mixed[Disable mixed precision]'", + "'--fastmath[Enable fast math]'", + "'--no-fastmath[Disable fast math]'", + ] + ) else: for arg in common_set.arguments: desc = arg.help.replace("'", "").replace("[", "").replace("]", "")[:120] @@ -360,16 +372,16 @@ def _generate_zsh_command_args(cmd: Command, schema: CLISchema) -> List[str]: def generate_zsh_completion(schema: CLISchema) -> str: """Generate zsh completion script from schema.""" lines = [ - '#compdef mfc.sh ./mfc.sh mfc', - '# AUTO-GENERATED from cli/commands.py - Do not edit manually', - '# Regenerate with: ./mfc.sh generate', - '', - '_mfc() {', - ' local context state state_descr line', - ' typeset -A opt_args', - '', - ' local -a commands', - ' commands=(', + "#compdef mfc.sh ./mfc.sh mfc", + "# AUTO-GENERATED from cli/commands.py - Do not edit manually", + "# Regenerate with: ./mfc.sh generate", + "", + "_mfc() {", + " local context state state_descr line", + " typeset -A opt_args", + "", + " local -a commands", + " commands=(", ] # Commands with descriptions @@ -379,42 +391,46 @@ def generate_zsh_completion(schema: CLISchema) -> str: for alias in cmd.aliases: lines.append(f' "{alias}:Alias for {cmd.name}"') - lines.extend([ - ' )', - '', - ' _arguments -C \\', - " '1: :->command' \\", - " '*:: :->args'", - '', - ' case $state in', - ' command)', - " _describe -t commands 'mfc command' commands", - ' ;;', - ' args)', - ' case $words[1] in', - ]) + lines.extend( + [ + " )", + "", + " _arguments -C \\", + " '1: :->command' \\", + " '*:: :->args'", + "", + " case $state in", + " command)", + " _describe -t commands 'mfc command' commands", + " ;;", + " args)", + " case $words[1] in", + ] + ) # Generate case for each command for cmd in schema.commands: all_names = [cmd.name] + cmd.aliases for name in all_names: - lines.append(f' {name})') + lines.append(f" {name})") arg_lines = _generate_zsh_command_args(cmd, schema) if arg_lines: - lines.append(' _arguments \\') - lines.append(' ' + ' \\\n '.join(arg_lines)) + lines.append(" _arguments \\") + lines.append(" " + " \\\n ".join(arg_lines)) else: # Explicitly disable default completion for commands with no args - lines.append(' :') - lines.append(' ;;') - - lines.extend([ - ' esac', - ' ;;', - ' esac', - '}', - '', - '_mfc "$@"', - ]) - - return '\n'.join(lines) + lines.append(" :") + lines.append(" ;;") + + lines.extend( + [ + " esac", + " ;;", + " esac", + "}", + "", + '_mfc "$@"', + ] + ) + + return "\n".join(lines) diff --git a/toolchain/mfc/cli/docs_gen.py b/toolchain/mfc/cli/docs_gen.py index 989ea6a612..9fc1b8bfbf 100644 --- a/toolchain/mfc/cli/docs_gen.py +++ b/toolchain/mfc/cli/docs_gen.py @@ -7,7 +7,8 @@ import re from typing import List -from .schema import CLISchema, Command, Argument + +from .schema import Argument, CLISchema, Command def _escape_doxygen(text: str) -> str: @@ -194,11 +195,7 @@ def _generate_command_section(cmd: Command, schema: CLISchema) -> List[str]: return lines -def _generate_commands_by_category( - schema: CLISchema, - category_commands: List[str], - header: str -) -> List[str]: +def _generate_commands_by_category(schema: CLISchema, category_commands: List[str], header: str) -> List[str]: """Generate command sections for a category.""" lines = [] matching = [c for c in schema.commands if c.name in category_commands] @@ -260,45 +257,47 @@ def generate_cli_reference(schema: CLISchema) -> str: lines.extend(_generate_commands_by_category(schema, other_commands, "Other Commands")) # Common options section - lines.extend([ - "## Common Options", - "", - "Many commands share common option sets:", - "", - "### Target Selection (`-t, --targets`)", - "", - "Available targets:", - "- `pre_process` - Pre-processor", - "- `simulation` - Main simulation", - "- `post_process` - Post-processor", - "- `syscheck` - System check utility", - "- `documentation` - Build documentation", - "", - "### Build Configuration Flags", - "", - "| Flag | Description |", - "|------|-------------|", - "| `--mpi` / `--no-mpi` | Enable/disable MPI support |", - "| `--gpu [acc/mp]` / `--no-gpu` | Enable GPU with OpenACC or OpenMP |", - "| `--debug` / `--no-debug` | Build with debug compiler flags |", - "| `--gcov` / `--no-gcov` | Enable code coverage |", - "| `--single` / `--no-single` | Single precision |", - "| `--mixed` / `--no-mixed` | Mixed precision |", - "", - "### Verbosity (`-v, --verbose`)", - "", - "Controls output verbosity level:", - "", - "- `-v` - Basic verbose output", - "- `-vv` - Show build commands", - "- `-vvv` - Full verbose output including CMake details", - "", - "### Debug Logging (`-d, --debug-log`)", - "", - "Enables debug logging for the Python toolchain (mfc.sh internals).", - "This is for troubleshooting the build system, not the MFC simulation code.", - "", - ]) + lines.extend( + [ + "## Common Options", + "", + "Many commands share common option sets:", + "", + "### Target Selection (`-t, --targets`)", + "", + "Available targets:", + "- `pre_process` - Pre-processor", + "- `simulation` - Main simulation", + "- `post_process` - Post-processor", + "- `syscheck` - System check utility", + "- `documentation` - Build documentation", + "", + "### Build Configuration Flags", + "", + "| Flag | Description |", + "|------|-------------|", + "| `--mpi` / `--no-mpi` | Enable/disable MPI support |", + "| `--gpu [acc/mp]` / `--no-gpu` | Enable GPU with OpenACC or OpenMP |", + "| `--debug` / `--no-debug` | Build with debug compiler flags |", + "| `--gcov` / `--no-gcov` | Enable code coverage |", + "| `--single` / `--no-single` | Single precision |", + "| `--mixed` / `--no-mixed` | Mixed precision |", + "", + "### Verbosity (`-v, --verbose`)", + "", + "Controls output verbosity level:", + "", + "- `-v` - Basic verbose output", + "- `-vv` - Show build commands", + "- `-vvv` - Full verbose output including CMake details", + "", + "### Debug Logging (`-d, --debug-log`)", + "", + "Enables debug logging for the Python toolchain (mfc.sh internals).", + "This is for troubleshooting the build system, not the MFC simulation code.", + "", + ] + ) return "\n".join(lines) @@ -316,28 +315,30 @@ def generate_command_summary(schema: CLISchema) -> str: alias_str = f" ({cmd.aliases[0]})" if cmd.aliases else "" lines.append(f"- **{cmd.name}**{alias_str}: {cmd.help}") - lines.extend([ - "", - "## Common Patterns", - "", - "```bash", - "# Build MFC", - "./mfc.sh build", - "./mfc.sh build --gpu # With GPU support", - "./mfc.sh build -j 8 # Parallel build", - "", - "# Run a case", - "./mfc.sh run case.py", - "./mfc.sh run case.py -n 4 # 4 MPI ranks", - "", - "# Run tests", - "./mfc.sh test", - "./mfc.sh test -j 4 # Parallel tests", - "", - "# Validate a case", - "./mfc.sh validate case.py", - "```", - "", - ]) + lines.extend( + [ + "", + "## Common Patterns", + "", + "```bash", + "# Build MFC", + "./mfc.sh build", + "./mfc.sh build --gpu # With GPU support", + "./mfc.sh build -j 8 # Parallel build", + "", + "# Run a case", + "./mfc.sh run case.py", + "./mfc.sh run case.py -n 4 # 4 MPI ranks", + "", + "# Run tests", + "./mfc.sh test", + "./mfc.sh test -j 4 # Parallel tests", + "", + "# Validate a case", + "./mfc.sh validate case.py", + "```", + "", + ] + ) return "\n".join(lines) diff --git a/toolchain/mfc/cli/schema.py b/toolchain/mfc/cli/schema.py index 6fc673b8d0..871d98f6da 100644 --- a/toolchain/mfc/cli/schema.py +++ b/toolchain/mfc/cli/schema.py @@ -8,11 +8,12 @@ from dataclasses import dataclass, field from enum import Enum, auto -from typing import List, Optional, Any, Union +from typing import Any, List, Optional, Union class ArgAction(Enum): """Supported argparse actions.""" + STORE = "store" STORE_TRUE = "store_true" STORE_FALSE = "store_false" @@ -23,44 +24,47 @@ class ArgAction(Enum): class CompletionType(Enum): """Types of shell completion behavior.""" - NONE = auto() # No completion - FILES = auto() # All file completion - FILES_PY = auto() # Python files only (*.py) - FILES_PACK = auto() # Pack files only (*.pack) - FILES_YAML = auto() # YAML files only (*.yaml, *.yml) - DIRECTORIES = auto() # Directory completion - CHOICES = auto() # Static choices from choices list + + NONE = auto() # No completion + FILES = auto() # All file completion + FILES_PY = auto() # Python files only (*.py) + FILES_PACK = auto() # Pack files only (*.pack) + FILES_YAML = auto() # YAML files only (*.yaml, *.yml) + DIRECTORIES = auto() # Directory completion + CHOICES = auto() # Static choices from choices list @dataclass class Completion: """Completion configuration for an argument.""" + type: CompletionType = CompletionType.NONE choices: Optional[List[str]] = None @dataclass -class Argument: # pylint: disable=too-many-instance-attributes +class Argument: """ Definition of a single CLI option argument (--flag). This represents one add_argument() call for a flag-style argument. """ + # Identity - name: str # Long form without dashes (e.g., "targets") - short: Optional[str] = None # Short form without dash (e.g., "t") + name: str # Long form without dashes (e.g., "targets") + short: Optional[str] = None # Short form without dash (e.g., "t") # Argparse configuration help: str = "" action: ArgAction = ArgAction.STORE - type: Optional[type] = None # str, int, float, etc. + type: Optional[type] = None # str, int, float, etc. default: Any = None choices: Optional[List[Any]] = None nargs: Optional[Union[str, int]] = None # "+", "*", "?", int, or "..." for REMAINDER metavar: Optional[str] = None required: bool = False - dest: Optional[str] = None # Override destination name - const: Any = None # For store_const action + dest: Optional[str] = None # Override destination name + const: Any = None # For store_const action # Completion completion: Completion = field(default_factory=Completion) @@ -83,7 +87,8 @@ def get_dest(self) -> str: @dataclass class Positional: """Definition of a positional argument.""" - name: str # Metavar and destination + + name: str # Metavar and destination help: str = "" type: type = str nargs: Optional[Union[str, int]] = None @@ -97,6 +102,7 @@ class Positional: @dataclass class Example: """A usage example for documentation.""" + command: str description: str @@ -104,17 +110,19 @@ class Example: @dataclass class MutuallyExclusiveGroup: """A group where only one argument can be specified.""" + arguments: List[Argument] = field(default_factory=list) required: bool = False @dataclass -class Command: # pylint: disable=too-many-instance-attributes +class Command: """ Definition of a CLI command/subcommand. This is the main building block for the CLI structure. """ + # Identity name: str help: str @@ -132,12 +140,12 @@ class Command: # pylint: disable=too-many-instance-attributes subcommands: List["Command"] = field(default_factory=list) # Documentation - description: Optional[str] = None # Long description for docs + description: Optional[str] = None # Long description for docs examples: List[Example] = field(default_factory=list) key_options: List[tuple] = field(default_factory=list) # (option, description) pairs # Handler module path (for dispatch) - handler: Optional[str] = None # Module.function path + handler: Optional[str] = None # Module.function path @dataclass @@ -147,7 +155,8 @@ class CommonArgumentSet: Replaces the add_common_arguments() function pattern. """ - name: str # Identifier for include_common + + name: str # Identifier for include_common arguments: List[Argument] = field(default_factory=list) # For MFCConfig flags that need --X and --no-X pairs mfc_config_flags: bool = False @@ -165,6 +174,7 @@ class CLISchema: - User guide help content - CLI reference documentation """ + prog: str = "./mfc.sh" description: str = "" diff --git a/toolchain/mfc/cli/test_cli.py b/toolchain/mfc/cli/test_cli.py index 6eaee9963c..abec7829e1 100644 --- a/toolchain/mfc/cli/test_cli.py +++ b/toolchain/mfc/cli/test_cli.py @@ -3,7 +3,6 @@ Verifies that modules can be imported and basic functionality works. """ -# pylint: disable=import-outside-toplevel import unittest @@ -14,32 +13,37 @@ class TestCliImports(unittest.TestCase): def test_schema_import(self): """Schema module should import and export expected classes.""" from . import schema - self.assertTrue(hasattr(schema, 'Command')) - self.assertTrue(hasattr(schema, 'Argument')) - self.assertTrue(hasattr(schema, 'Positional')) - self.assertTrue(hasattr(schema, 'CLISchema')) + + self.assertTrue(hasattr(schema, "Command")) + self.assertTrue(hasattr(schema, "Argument")) + self.assertTrue(hasattr(schema, "Positional")) + self.assertTrue(hasattr(schema, "CLISchema")) def test_commands_import(self): """Commands module should import and have MFC_CLI_SCHEMA.""" from . import commands - self.assertTrue(hasattr(commands, 'MFC_CLI_SCHEMA')) + + self.assertTrue(hasattr(commands, "MFC_CLI_SCHEMA")) self.assertIsNotNone(commands.MFC_CLI_SCHEMA) def test_argparse_gen_import(self): """Argparse generator should import.""" from . import argparse_gen - self.assertTrue(hasattr(argparse_gen, 'generate_parser')) + + self.assertTrue(hasattr(argparse_gen, "generate_parser")) def test_completion_gen_import(self): """Completion generator should import.""" from . import completion_gen - self.assertTrue(hasattr(completion_gen, 'generate_bash_completion')) - self.assertTrue(hasattr(completion_gen, 'generate_zsh_completion')) + + self.assertTrue(hasattr(completion_gen, "generate_bash_completion")) + self.assertTrue(hasattr(completion_gen, "generate_zsh_completion")) def test_docs_gen_import(self): """Docs generator should import.""" from . import docs_gen - self.assertTrue(hasattr(docs_gen, 'generate_cli_reference')) + + self.assertTrue(hasattr(docs_gen, "generate_cli_reference")) class TestCliSchema(unittest.TestCase): @@ -48,20 +52,23 @@ class TestCliSchema(unittest.TestCase): def test_cli_schema_has_commands(self): """MFC_CLI_SCHEMA should have commands defined.""" from .commands import MFC_CLI_SCHEMA + self.assertTrue(len(MFC_CLI_SCHEMA.commands) > 0) def test_cli_schema_has_description(self): """MFC_CLI_SCHEMA should have a description.""" from .commands import MFC_CLI_SCHEMA + self.assertIsNotNone(MFC_CLI_SCHEMA.description) self.assertIsInstance(MFC_CLI_SCHEMA.description, str) def test_commands_have_names(self): """Each command should have a name.""" from .commands import MFC_CLI_SCHEMA + for cmd in MFC_CLI_SCHEMA.commands: - self.assertIsNotNone(cmd.name, f"Command missing name") - self.assertTrue(len(cmd.name) > 0, f"Command has empty name") + self.assertIsNotNone(cmd.name, "Command missing name") + self.assertTrue(len(cmd.name) > 0, "Command has empty name") class TestArgparseGenerator(unittest.TestCase): @@ -70,6 +77,7 @@ class TestArgparseGenerator(unittest.TestCase): def test_generate_parser_returns_parser(self): """generate_parser should return a tuple with ArgumentParser.""" import argparse + from .argparse_gen import generate_parser from .commands import MFC_CLI_SCHEMA @@ -101,8 +109,8 @@ class TestCompletionGenerator(unittest.TestCase): def test_bash_completion_generates_output(self): """Bash completion should generate non-empty output.""" - from .completion_gen import generate_bash_completion from .commands import MFC_CLI_SCHEMA + from .completion_gen import generate_bash_completion output = generate_bash_completion(MFC_CLI_SCHEMA) self.assertIsInstance(output, str) @@ -111,8 +119,8 @@ def test_bash_completion_generates_output(self): def test_zsh_completion_generates_output(self): """Zsh completion should generate non-empty output.""" - from .completion_gen import generate_zsh_completion from .commands import MFC_CLI_SCHEMA + from .completion_gen import generate_zsh_completion output = generate_zsh_completion(MFC_CLI_SCHEMA) self.assertIsInstance(output, str) @@ -125,8 +133,8 @@ class TestDocsGenerator(unittest.TestCase): def test_docs_generates_markdown(self): """Docs generator should produce markdown output.""" - from .docs_gen import generate_cli_reference from .commands import MFC_CLI_SCHEMA + from .docs_gen import generate_cli_reference output = generate_cli_reference(MFC_CLI_SCHEMA) self.assertIsInstance(output, str) @@ -134,5 +142,42 @@ def test_docs_generates_markdown(self): self.assertIn("#", output) # Should contain markdown headers +class TestMFCConfigHash(unittest.TestCase): + """Test MFCConfig __hash__ / __eq__ contract.""" + + def test_equal_configs_same_hash(self): + """Equal MFCConfig objects must have the same hash.""" + from ..state import MFCConfig + + a = MFCConfig() + b = MFCConfig() + self.assertEqual(a, b) + self.assertEqual(hash(a), hash(b)) + + def test_different_configs_different_hash(self): + """Different MFCConfig objects should (likely) have different hashes.""" + from ..state import MFCConfig + + a = MFCConfig(debug=False) + b = MFCConfig(debug=True) + self.assertNotEqual(a, b) + self.assertNotEqual(hash(a), hash(b)) + + def test_usable_in_set(self): + """MFCConfig should be usable in a set.""" + from ..state import MFCConfig + + s = {MFCConfig(), MFCConfig(debug=True)} + self.assertEqual(len(s), 2) + self.assertIn(MFCConfig(), s) + + def test_usable_as_dict_key(self): + """MFCConfig should be usable as a dict key.""" + from ..state import MFCConfig + + d = {MFCConfig(): "default", MFCConfig(debug=True): "debug"} + self.assertEqual(d[MFCConfig()], "default") + + if __name__ == "__main__": unittest.main() diff --git a/toolchain/mfc/common.py b/toolchain/mfc/common.py index ce02e8251c..ff066c8672 100644 --- a/toolchain/mfc/common.py +++ b/toolchain/mfc/common.py @@ -1,28 +1,30 @@ -import os, yaml, typing, shutil, subprocess, logging +import logging +import os +import shutil +import subprocess +import typing +from os.path import abspath, dirname, join, normpath, realpath -from os.path import join, abspath, normpath, dirname, realpath +import yaml 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 # pylint: disable=global-statement + global _debug_logger # noqa: PLW0603 if enabled: - logging.basicConfig( - level=logging.DEBUG, - format='[DEBUG %(asctime)s] %(message)s', - datefmt='%H:%M:%S' - ) - _debug_logger = logging.getLogger('mfc') + 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: @@ -30,13 +32,13 @@ def debug(msg: str): 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")) -MFC_TOOLCHAIN_DIR = abspath(join(MFC_ROOT_DIR, "toolchain")) +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")) +MFC_TOOLCHAIN_DIR = abspath(join(MFC_ROOT_DIR, "toolchain")) MFC_EXAMPLE_DIRPATH = abspath(join(MFC_ROOT_DIR, "examples")) -MFC_LOCK_FILEPATH = abspath(join(MFC_BUILD_DIR, "lock.yaml")) -MFC_TEMPLATE_DIR = abspath(join(MFC_TOOLCHAIN_DIR, "templates")) +MFC_LOCK_FILEPATH = abspath(join(MFC_BUILD_DIR, "lock.yaml")) +MFC_TEMPLATE_DIR = abspath(join(MFC_TOOLCHAIN_DIR, "templates")) MFC_BENCH_FILEPATH = abspath(join(MFC_TOOLCHAIN_DIR, "bench.yaml")) MFC_MECHANISMS_DIR = abspath(join(MFC_TOOLCHAIN_DIR, "mechanisms")) @@ -56,8 +58,9 @@ def debug(msg: str): class MFCException(Exception): pass -def system(command: typing.List[str], print_cmd = None, **kwargs) -> subprocess.CompletedProcess: - cmd = [ str(x) for x in command if not isspace(str(x)) ] + +def system(command: typing.List[str], print_cmd=None, **kwargs) -> subprocess.CompletedProcess: + cmd = [str(x) for x in command if not isspace(str(x))] if print_cmd in [True, None]: cons.print(f"$ {' '.join(cmd)}") @@ -127,12 +130,12 @@ def delete_directory(dirpath: str) -> None: def get_program_output(arguments: typing.List[str] = None, cwd=None): - with subprocess.Popen([ str(_) for _ in arguments ] or [], cwd=cwd, stdout=subprocess.PIPE) as proc: + with subprocess.Popen([str(_) for _ in arguments] or [], cwd=cwd, stdout=subprocess.PIPE) as proc: return (proc.communicate()[0].decode(), proc.returncode) def get_py_program_output(filepath: str, arguments: typing.List[str] = None): - dirpath = os.path.abspath (os.path.dirname(filepath)) + dirpath = os.path.abspath(os.path.dirname(filepath)) filename = os.path.basename(filepath) return get_program_output(["python3", filename] + arguments, cwd=dirpath) @@ -169,7 +172,7 @@ def format_list_to_string(arr: list, item_style=None, empty=None): pre, post = "", "" if item_style is not None: - pre = f"[{item_style}]" + pre = f"[{item_style}]" post = f"[/{item_style}]" if len(arr) == 0: @@ -181,7 +184,7 @@ def format_list_to_string(arr: list, item_style=None, empty=None): if len(arr) == 2: return f"{pre}{arr[0]}{post} and {pre}{arr[1]}{post}" - lhs = ', '.join([ f"{pre}{e}{post}" for e in arr[:-1]]) + lhs = ", ".join([f"{pre}{e}{post}" for e in arr[:-1]]) rhs = f", and {pre}{arr[-1]}{post}" return lhs + rhs @@ -199,7 +202,6 @@ def find(predicate, arr: list): return None, None -# pylint: disable=redefined-builtin def quit(sig): os.kill(os.getpid(), sig) @@ -229,27 +231,26 @@ def is_number(x: str) -> bool: def get_cpuinfo(): if does_command_exist("lscpu"): # Linux - with subprocess.Popen(['lscpu'], stdout=subprocess.PIPE, universal_newlines=True) as proc: + with subprocess.Popen(["lscpu"], stdout=subprocess.PIPE, universal_newlines=True) as proc: output = f"From lscpu\n{proc.communicate()[0]}" elif does_command_exist("sysctl"): # MacOS - with subprocess.Popen(['sysctl', '-a'], stdout=subprocess.PIPE) as proc1: - with subprocess.Popen(['grep', 'machdep.cpu'], stdin=proc1.stdout, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True) as proc2: - proc1.stdout.close() # Allow proc1 to receive a SIGPIPE if proc2 exits. + with subprocess.Popen(["sysctl", "-a"], stdout=subprocess.PIPE) as proc1: + with subprocess.Popen(["grep", "machdep.cpu"], stdin=proc1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) as proc2: + proc1.stdout.close() # Allow proc1 to receive a SIGPIPE if proc2 exits. output = f"From sysctl -a \n{proc2.communicate()[0]}" else: output = "No CPU info found" return f"CPU Info:\n{output}" + def generate_git_tagline() -> str: if not does_command_exist("git"): return "Could not find git" - rev = system(["git", "rev-parse", "HEAD"], print_cmd=False, stdout=subprocess.PIPE).stdout.decode().strip() + rev = system(["git", "rev-parse", "HEAD"], print_cmd=False, stdout=subprocess.PIPE).stdout.decode().strip() branch = system(["git", "rev-parse", "--abbrev-ref", "HEAD"], print_cmd=False, stdout=subprocess.PIPE).stdout.decode().strip() - dirty = "dirty" if system(["git", "diff", "--quiet"], print_cmd=False).returncode != 0 else "clean" + dirty = "dirty" if system(["git", "diff", "--quiet"], print_cmd=False).returncode != 0 else "clean" return f"{rev} on {branch} ({dirty})" diff --git a/toolchain/mfc/completion.py b/toolchain/mfc/completion.py index 908abbde14..43f209971b 100644 --- a/toolchain/mfc/completion.py +++ b/toolchain/mfc/completion.py @@ -9,9 +9,8 @@ import shutil from pathlib import Path -from .printer import cons from .common import MFC_ROOT_DIR - +from .printer import cons # Installation directory (user-local, independent of MFC clone location) COMPLETION_INSTALL_DIR = Path.home() / ".local" / "share" / "mfc" / "completions" @@ -173,7 +172,7 @@ def show_status(): else: cons.print(" [dim]✗ Zsh completion not installed[/dim]") else: - cons.print(f" [dim]✗ Not installed[/dim]") + cons.print(" [dim]✗ Not installed[/dim]") cons.print() @@ -191,7 +190,6 @@ def show_status(): def completion(): """Main entry point for completion command.""" - # pylint: disable=import-outside-toplevel from .state import ARG action = ARG("completion_action") diff --git a/toolchain/mfc/count.py b/toolchain/mfc/count.py index 5bd0314c17..3e809c1eb3 100644 --- a/toolchain/mfc/count.py +++ b/toolchain/mfc/count.py @@ -1,23 +1,27 @@ -import os, glob, typing, typing +import glob +import os +import typing + import rich.table -from .state import ARG -from .common import MFC_ROOT_DIR, format_list_to_string, MFCException +from .common import MFC_ROOT_DIR, MFCException, format_list_to_string from .printer import cons +from .state import ARG + def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, int], int]: files = {} total = 0 - for filepath in glob.glob(os.path.join(mfc_dir, 'src', srcdirname, '*.*f*')): + for filepath in glob.glob(os.path.join(mfc_dir, "src", srcdirname, "*.*f*")): with open(filepath) as f: counter = 0 - for l in f.read().split('\n'): + for line in f.read().split("\n"): # Skip whitespace - if l.isspace() or len(l) == 0: + if line.isspace() or len(line) == 0: continue # Skip comments but not !$acc ones! - if l.lstrip().startswith("!") and not l.lstrip().startswith("!$acc"): + if line.lstrip().startswith("!") and not line.lstrip().startswith("!$acc"): continue counter += 1 @@ -26,14 +30,15 @@ def handle_dir(mfc_dir: str, srcdirname: str) -> typing.Tuple[typing.Dict[str, i return (files, total) + def count(): - target_str_list = format_list_to_string(ARG('targets'), 'magenta') + target_str_list = format_list_to_string(ARG("targets"), "magenta") cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace lines)") cons.indent() total = 0 - for codedir in ['common'] + ARG("targets"): + for codedir in ["common"] + ARG("targets"): dirfiles, dircount = handle_dir(MFC_ROOT_DIR, codedir) table = rich.table.Table(show_header=True, box=rich.table.box.SIMPLE) table.add_column(f"File (in [magenta]{codedir}[/magenta])", justify="left") @@ -50,21 +55,21 @@ def count(): cons.print() cons.unindent() -# pylint: disable=too-many-locals + def count_diff(): - target_str_list = format_list_to_string(ARG('targets'), 'magenta') + target_str_list = format_list_to_string(ARG("targets"), "magenta") cons.print(f"[bold]Counting lines of code in {target_str_list}[/bold] (excluding whitespace lines)") cons.indent() total = 0 - MFC_COMPARE_DIR=os.getenv('MFC_PR') + MFC_COMPARE_DIR = os.getenv("MFC_PR") if MFC_COMPARE_DIR is None: raise MFCException("MFC_PR is not in your environment.") - print('compare dir', MFC_COMPARE_DIR) + print("compare dir", MFC_COMPARE_DIR) # MFC_COMPARE_DIR="/Users/spencer/Downloads/MFC-shbfork" - for codedir in ['common'] + ARG("targets"): + for codedir in ["common"] + ARG("targets"): dirfiles_root, dircount_root = handle_dir(MFC_ROOT_DIR, codedir) dirfiles_pr, dircount_pr = handle_dir(MFC_COMPARE_DIR, codedir) table = rich.table.Table(show_header=True, box=rich.table.box.SIMPLE) @@ -78,17 +83,19 @@ def count_diff(): dirfiles_root[filepath] = dirfiles_root.get(filepath, 0) dirfiles_pr[filepath] = dirfiles_pr.get(filepath, 0) - PLUS = "++ " + PLUS = "++ " MINUS = "-- " diff_count = dirfiles_pr[filepath] - dirfiles_root[filepath] mycolor = "red" if diff_count > 0 else "green" mysymbol = PLUS if diff_count > 0 else MINUS - table.add_row(os.path.basename(filepath), - f"[bold cyan]{dirfiles_root[filepath]}[/bold cyan]", - f"[bold cyan]{dirfiles_pr[filepath]}[/bold cyan]", - mysymbol, - f"[bold {mycolor}]{diff_count}[/bold {mycolor}]") + table.add_row( + os.path.basename(filepath), + f"[bold cyan]{dirfiles_root[filepath]}[/bold cyan]", + f"[bold cyan]{dirfiles_pr[filepath]}[/bold cyan]", + mysymbol, + f"[bold {mycolor}]{diff_count}[/bold {mycolor}]", + ) total += dircount_root diff --git a/toolchain/mfc/gen_case_constraints_docs.py b/toolchain/mfc/gen_case_constraints_docs.py index ffb70357e0..96f569cbd7 100644 --- a/toolchain/mfc/gen_case_constraints_docs.py +++ b/toolchain/mfc/gen_case_constraints_docs.py @@ -6,17 +6,17 @@ maps them to parameters and stages, and emits Markdown to stdout. Also generates case design playbook from curated working examples. -""" # pylint: disable=too-many-lines +""" from __future__ import annotations import json -import sys import subprocess +import sys +from collections import defaultdict from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Iterable, Any -from collections import defaultdict +from typing import Any, Dict, Iterable, List HERE = Path(__file__).resolve().parent CASE_VALIDATOR_PATH = HERE / "case_validator.py" @@ -28,20 +28,23 @@ if _toolchain_dir not in sys.path: sys.path.insert(0, _toolchain_dir) -from mfc.params import CONSTRAINTS, DEPENDENCIES, get_value_label # noqa: E402 pylint: disable=wrong-import-position -from mfc.params.ast_analyzer import ( # noqa: E402 pylint: disable=wrong-import-position - Rule, classify_message, feature_title, +from mfc.params import CONSTRAINTS, DEPENDENCIES, get_value_label # noqa: E402 +from mfc.params.ast_analyzer import ( # noqa: E402 + Rule, analyze_case_validator, + classify_message, + feature_title, ) - # --------------------------------------------------------------------------- # Case Playbook Generation (from working examples) # --------------------------------------------------------------------------- + @dataclass class PlaybookEntry: """A curated example case for the playbook""" + case_dir: str title: str description: str @@ -52,68 +55,16 @@ class PlaybookEntry: # 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"] + "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"]), ] @@ -144,13 +95,7 @@ def load_case_params(case_dir: str) -> Dict[str, Any]: return {} try: - result = subprocess.run( - ["python3", str(case_path)], - capture_output=True, - text=True, - timeout=10, - check=True - ) + 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) as e: @@ -208,7 +153,7 @@ def get_time_stepper_name(stepper: int | None) -> str: return get_value_label("time_stepper", stepper) or "Not specified" -def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: # pylint: disable=too-many-branches,too-many-statements +def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: """Render a single playbook entry as Markdown""" lines = [] @@ -216,49 +161,49 @@ def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: 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.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: + if summary["num_fluids"] is not None: lines.append(f"- **Number of fluids:** {summary['num_fluids']}") # Dimensionality - n, p = summary['n'], summary['p'] + 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']: + if summary["cyl_coord"]: lines.append("- **Coordinates:** Cylindrical/Axisymmetric") # Active features active_features = [] - if summary['bubbles_euler']: + if summary["bubbles_euler"]: active_features.append("Euler-Euler bubbles") - if summary['bubbles_lagrange']: + if summary["bubbles_lagrange"]: active_features.append("Euler-Lagrange bubbles") - if summary['qbmm']: + if summary["qbmm"]: active_features.append("QBMM") - if summary['polydisperse']: + if summary["polydisperse"]: active_features.append("Polydisperse") - if summary['surface_tension']: + if summary["surface_tension"]: active_features.append("Surface tension") - if summary['mhd']: + if summary["mhd"]: active_features.append("MHD") - if summary['relax']: + if summary["relax"]: active_features.append("Phase change") - if summary['hypoelasticity']: + if summary["hypoelasticity"]: active_features.append("Hypoelasticity") - if summary['viscous']: + if summary["viscous"]: active_features.append("Viscous") - if summary['ib']: + if summary["ib"]: active_features.append("Immersed boundaries") - if summary['igr']: + if summary["igr"]: active_features.append("IGR solver") - if summary['acoustic_source']: + if summary["acoustic_source"]: active_features.append("Acoustic sources") if active_features: @@ -267,36 +212,36 @@ def render_playbook_card(entry: PlaybookEntry, summary: Dict[str, Any]) -> str: # Numerics lines.append("\n**Numerical Methods:**\n") - if summary['recon_type'] == 1 and summary['weno_order']: + 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']: + 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']) + 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']) + 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']: + if summary["riemann_solver"]: lines.append("- [Riemann Solvers](#riemann-solvers)") - if summary['bubbles_euler'] or summary['bubbles_lagrange']: + if summary["bubbles_euler"] or summary["bubbles_lagrange"]: lines.append("- [Bubble Models](#bubble-models)") - if summary['mhd']: + if summary["mhd"]: lines.append("- [MHD](#compat-physics-models)") - if summary['ib']: + if summary["ib"]: lines.append("- [Immersed Boundaries](#compat-geometry)") - if summary['viscous']: + if summary["viscous"]: lines.append("- [Viscosity](#compat-physics-models)") lines.append("\n
\n") @@ -312,9 +257,7 @@ def generate_playbook() -> str: lines.append("## 🧩 Case Design Playbook {#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" + "> **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 @@ -334,7 +277,7 @@ def generate_playbook() -> str: summary = summarize_case_params(params) card = render_playbook_card(entry, summary) lines.append(card) - except Exception as e: # pylint: disable=broad-except + except Exception as e: print(f"WARNING: Failed to process playbook entry '{entry.case_dir}': {e}", file=sys.stderr) continue @@ -345,7 +288,8 @@ def generate_playbook() -> str: # Markdown rendering # --------------------------------------------------------------------------- -def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-locals,too-many-branches,too-many-statements + +def render_markdown(rules: Iterable[Rule]) -> str: """ Render user-friendly compatibility tables and summaries. """ @@ -361,13 +305,8 @@ def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-l lines.append("@page case_constraints Case Creator Guide\n") lines.append("# Case Creator Guide\n") - lines.append( - "> **Quick reference** for building MFC cases: working examples, compatibility rules, " - "and configuration requirements.\n" - ) - lines.append( - "> Auto-generated from `case_validator.py` and `examples/`.\n" - ) + lines.append("> **Quick reference** for building MFC cases: working examples, compatibility rules, and configuration requirements.\n") + lines.append("> Auto-generated from `case_validator.py` and `examples/`.\n") # Add playbook at the top playbook = generate_playbook() @@ -429,7 +368,7 @@ def render_markdown(rules: Iterable[Rule]) -> str: # pylint: disable=too-many-l lines.append("## 📊 Feature Compatibility {#feature-compatibility}\n") lines.append("What works together:\n") - for category, features in major_features.items(): # pylint: disable=too-many-nested-blocks + for category, features in major_features.items(): cat_id = "compat-" + category.lower().replace(" ", "-") lines.append(f"\n### {category} {{#{cat_id}}}\n") @@ -698,7 +637,7 @@ def _render_cond_parts(trigger_str, cond_dict): if not (schema_parts or dep_parts or requirements or incompatibilities or ranges or warnings): continue - lines.append(f"\n
") + lines.append("\n
") lines.append(f"{title} (`{param}`)\n") if schema_parts: @@ -743,10 +682,7 @@ def _render_cond_parts(trigger_str, cond_dict): all_warnings = [r for r in rules if r.severity == "warning"] if all_warnings: lines.append("## ⚠️ Physics Warnings {#physics-warnings}\n") - lines.append( - "These checks are **non-fatal** — they print a yellow warning but do not abort the run. " - "They catch common mistakes in initial conditions and EOS parameters.\n" - ) + lines.append("These checks are **non-fatal** — they print a yellow warning but do not abort the run. They catch common mistakes in initial conditions and EOS parameters.\n") # Group by method warnings_by_method: Dict[str, List[Rule]] = defaultdict(list) @@ -774,7 +710,7 @@ def _render_cond_parts(trigger_str, cond_dict): descs.append(r.message) desc_str = "; ".join(descs[:2]) if len(descs) > 2: - desc_str += f" (+{len(descs)-2} more)" + desc_str += f" (+{len(descs) - 2} more)" lines.append(f"| **{title}** | {stages_str} | {desc_str} |") lines.append("") @@ -791,6 +727,7 @@ def _render_cond_parts(trigger_str, cond_dict): # Main # --------------------------------------------------------------------------- + def main(as_string: bool = False) -> str: """Generate case constraints documentation. Returns markdown string.""" analysis = analyze_case_validator(CASE_VALIDATOR_PATH) diff --git a/toolchain/mfc/gen_physics_docs.py b/toolchain/mfc/gen_physics_docs.py index 027349d595..f81c689117 100644 --- a/toolchain/mfc/gen_physics_docs.py +++ b/toolchain/mfc/gen_physics_docs.py @@ -20,8 +20,8 @@ if _toolchain_dir not in sys.path: sys.path.insert(0, _toolchain_dir) -from mfc.case_validator import PHYSICS_DOCS # noqa: E402 pylint: disable=wrong-import-position -from mfc.params.ast_analyzer import ( # noqa: E402 pylint: disable=wrong-import-position +from mfc.case_validator import PHYSICS_DOCS # noqa: E402 +from mfc.params.ast_analyzer import ( # noqa: E402 Rule, analyze_case_validator, ) @@ -166,7 +166,6 @@ def _render_method(doc: dict, method_rules: List[Rule], lines: List[str]) -> Non cites = ", ".join(f"\\cite {r}" for r in doc["references"]) lines.append(f"**References:** {cites}\n") - # Undocumented checks are omitted — they are discoverable via # @ref case_constraints "Case Creator Guide". @@ -184,19 +183,13 @@ def render(rules: List[Rule]) -> str: lines: List[str] = [] lines.append("@page physics_constraints Physics Constraints\n") lines.append("# Physics Constraints Reference\n") + lines.append("> Auto-generated from `PHYSICS_DOCS` in `case_validator.py` and AST-extracted validation rules. Do not edit by hand.\n") + lines.append("This document catalogs the physics constraints enforced by MFC's case parameter validator. Constraints are organized by physical category with mathematical justifications.\n") lines.append( - "> Auto-generated from `PHYSICS_DOCS` in `case_validator.py` and " - "AST-extracted validation rules. Do not edit by hand.\n" - ) - lines.append( - "This document catalogs the physics constraints enforced by MFC's case parameter validator. " - "Constraints are organized by physical category with mathematical justifications.\n" - ) - lines.append( - "For parameter syntax and allowed values, see @ref case \"Case Files\" and " - "the @ref parameters \"Case Parameters\" reference. " + 'For parameter syntax and allowed values, see @ref case "Case Files" and ' + 'the @ref parameters "Case Parameters" reference. ' "For feature compatibility and working examples, see " - "@ref case_constraints \"Case Creator Guide\".\n" + '@ref case_constraints "Case Creator Guide".\n' ) extra_categories = [c for c in by_category if c not in CATEGORY_ORDER] diff --git a/toolchain/mfc/generate.py b/toolchain/mfc/generate.py index 78f2273890..a6a6273c38 100644 --- a/toolchain/mfc/generate.py +++ b/toolchain/mfc/generate.py @@ -4,17 +4,17 @@ This module regenerates all derived files from the single source of truth in cli/commands.py. Run `./mfc.sh generate` after modifying commands. """ -# pylint: disable=import-outside-toplevel import json +import sys from pathlib import Path -from .printer import cons -from .common import MFC_ROOT_DIR -from .state import ARG from .cli.commands import MFC_CLI_SCHEMA from .cli.completion_gen import generate_bash_completion, generate_zsh_completion from .cli.docs_gen import generate_cli_reference +from .common import MFC_ROOT_DIR +from .printer import cons +from .state import ARG def _check_or_write(path: Path, content: str, check_mode: bool) -> bool: @@ -51,8 +51,8 @@ def _constraint_docs(docs_dir: Path) -> list: def generate(): """Regenerate completion scripts and optionally JSON schema.""" - from .params.generators.json_schema_gen import generate_json_schema from .params.generators.docs_gen import generate_parameter_docs + from .params.generators.json_schema_gen import generate_json_schema check_mode = ARG("check") json_schema_mode = ARG("json_schema") @@ -72,8 +72,7 @@ def generate(): (completions_dir / "mfc.bash", generate_bash_completion(MFC_CLI_SCHEMA)), (completions_dir / "_mfc", generate_zsh_completion(MFC_CLI_SCHEMA)), (docs_dir / "cli-reference.md", generate_cli_reference(MFC_CLI_SCHEMA)), - (Path(MFC_ROOT_DIR) / "toolchain" / "mfc-case-schema.json", - json.dumps(generate_json_schema(include_descriptions=True), indent=2)), + (Path(MFC_ROOT_DIR) / "toolchain" / "mfc-case-schema.json", json.dumps(generate_json_schema(include_descriptions=True), indent=2)), (docs_dir / "parameters.md", generate_parameter_docs()), ] + _constraint_docs(docs_dir) @@ -83,7 +82,7 @@ def generate(): all_ok = False if not all_ok: - exit(1) + sys.exit(1) if not check_mode: cons.print() @@ -92,14 +91,14 @@ def generate(): def _generate_json_schema(): """Generate JSON Schema and parameter documentation (standalone mode).""" - from .params.generators.json_schema_gen import generate_json_schema, get_schema_stats - from .params.generators.docs_gen import generate_parameter_docs from .ide import update_vscode_settings + from .params.generators.docs_gen import generate_parameter_docs + from .params.generators.json_schema_gen import generate_json_schema, get_schema_stats # Generate JSON Schema schema = generate_json_schema(include_descriptions=True) schema_path = Path(MFC_ROOT_DIR) / "toolchain" / "mfc-case-schema.json" - with open(schema_path, 'w') as f: + with open(schema_path, "w") as f: json.dump(schema, f, indent=2) # Generate parameter documentation @@ -114,7 +113,7 @@ def _generate_json_schema(): cons.print(f"[green]Generated[/green] {schema_path}") cons.print(f"[green]Generated[/green] {docs_path}") cons.print() - cons.print(f"[bold]Parameter Statistics:[/bold]") + cons.print("[bold]Parameter Statistics:[/bold]") cons.print(f" Total parameters: {stats['total_params']}") cons.print(f" With constraints: {stats['with_constraints']}") cons.print(f" With descriptions: {stats['with_descriptions']}") diff --git a/toolchain/mfc/ide.py b/toolchain/mfc/ide.py index 767b67e8ae..59279f103b 100644 --- a/toolchain/mfc/ide.py +++ b/toolchain/mfc/ide.py @@ -3,7 +3,6 @@ Automatically configures IDE settings (VS Code, etc.) for MFC development. """ -# pylint: disable=import-outside-toplevel import re from pathlib import Path @@ -16,7 +15,7 @@ # The MFC schema configuration to insert # Matches common case file names - users get auto-completion for JSON/YAML case files -_VSCODE_MFC_CONFIG = '''\ +_VSCODE_MFC_CONFIG = """\ "json.schemas": [ { "fileMatch": ["**/case.json", "**/input.json", "**/mfc-case.json", "**/mfc.json"], @@ -25,7 +24,7 @@ ], "yaml.schemas": { "./toolchain/mfc-case-schema.json": ["**/case.yaml", "**/case.yml", "**/input.yaml", "**/input.yml", "**/mfc-case.yaml", "**/mfc.yaml"] - }''' + }""" def ensure_vscode_settings() -> bool: @@ -59,26 +58,21 @@ def ensure_vscode_settings() -> bool: return False # Insert before the final closing brace - last_brace = content.rfind('}') + last_brace = content.rfind("}") if last_brace != -1: # Check if we need a comma before_brace = content[:last_brace].rstrip() - needs_comma = before_brace and not before_brace.endswith('{') and not before_brace.endswith(',') - comma = ',' if needs_comma else '' - new_content = ( - content[:last_brace].rstrip() + - comma + '\n\n ' + - marked_config + '\n' + - content[last_brace:] - ) + needs_comma = before_brace and not before_brace.endswith("{") and not before_brace.endswith(",") + comma = "," if needs_comma else "" + new_content = content[:last_brace].rstrip() + comma + "\n\n " + marked_config + "\n" + content[last_brace:] else: # Malformed JSON, just append - new_content = content + '\n' + marked_config + new_content = content + "\n" + marked_config else: # Ensure .vscode directory exists vscode_dir.mkdir(exist_ok=True) # Create new settings file with just our config - new_content = f'{{\n {marked_config}\n}}\n' + new_content = f"{{\n {marked_config}\n}}\n" settings_path.write_text(new_content) return True @@ -106,31 +100,23 @@ def update_vscode_settings() -> None: content = settings_path.read_text() # Check if our markers already exist - marker_pattern = re.compile( - rf'{re.escape(_VSCODE_MARKER_BEGIN)}.*?{re.escape(_VSCODE_MARKER_END)}', - re.DOTALL - ) + marker_pattern = re.compile(rf"{re.escape(_VSCODE_MARKER_BEGIN)}.*?{re.escape(_VSCODE_MARKER_END)}", re.DOTALL) if marker_pattern.search(content): # Replace existing marked section new_content = marker_pattern.sub(marked_config, content) else: # Insert before the final closing brace - last_brace = content.rfind('}') + last_brace = content.rfind("}") if last_brace != -1: before_brace = content[:last_brace].rstrip() - needs_comma = before_brace and not before_brace.endswith('{') and not before_brace.endswith(',') - comma = ',' if needs_comma else '' - new_content = ( - content[:last_brace].rstrip() + - comma + '\n\n ' + - marked_config + '\n' + - content[last_brace:] - ) + needs_comma = before_brace and not before_brace.endswith("{") and not before_brace.endswith(",") + comma = "," if needs_comma else "" + new_content = content[:last_brace].rstrip() + comma + "\n\n " + marked_config + "\n" + content[last_brace:] else: - new_content = content + '\n' + marked_config + new_content = content + "\n" + marked_config else: - new_content = f'{{\n {marked_config}\n}}\n' + new_content = f"{{\n {marked_config}\n}}\n" settings_path.write_text(new_content) cons.print(f"[green]Updated[/green] {settings_path}") diff --git a/toolchain/mfc/init.py b/toolchain/mfc/init.py index d339d7c9df..3f811e30f7 100644 --- a/toolchain/mfc/init.py +++ b/toolchain/mfc/init.py @@ -3,14 +3,13 @@ import os import shutil -from .printer import cons from .common import MFC_EXAMPLE_DIRPATH, MFCException +from .printer import cons from .state import ARG - # Built-in minimal templates BUILTIN_TEMPLATES = { - '1D_minimal': '''\ + "1D_minimal": '''\ #!/usr/bin/env python3 """ 1D Minimal Case Template @@ -120,8 +119,7 @@ "fluid_pp(1)%pi_inf": 0.0, })) ''', - - '2D_minimal': '''\ + "2D_minimal": '''\ #!/usr/bin/env python3 """ 2D Minimal Case Template @@ -239,8 +237,7 @@ "fluid_pp(1)%pi_inf": 0.0, })) ''', - - '3D_minimal': '''\ + "3D_minimal": '''\ #!/usr/bin/env python3 """ 3D Minimal Case Template @@ -379,7 +376,7 @@ def get_available_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')): + if os.path.isdir(example_path) and os.path.isfile(os.path.join(example_path, "case.py")): templates.append(f"example:{name}") return templates @@ -392,10 +389,10 @@ def list_templates(): 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, '') + "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() @@ -405,14 +402,14 @@ def list_templates(): 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')): + 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']: + 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='') + 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: @@ -437,9 +434,9 @@ def create_case(name: str, template: str): # 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') + case_path = os.path.join(output_dir, "case.py") - with open(case_path, 'w') as f: + with open(case_path, "w") as f: f.write(BUILTIN_TEMPLATES[template]) os.chmod(case_path, 0o755) # Make executable @@ -453,7 +450,7 @@ def create_case(name: str, template: str): cons.print() # Check if it's an example template - elif template.startswith('example:'): + elif template.startswith("example:"): example_name = template[8:] # Remove 'example:' prefix example_path = os.path.join(MFC_EXAMPLE_DIRPATH, example_name) @@ -472,12 +469,9 @@ def create_case(name: str, template: str): cons.print() else: - available = ', '.join(list(BUILTIN_TEMPLATES.keys())[:3]) + 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 new --list' to see all available templates." + f"Unknown template: {template}\nAvailable built-in templates: {available}\nOr use 'example:' to copy from examples.\nRun './mfc.sh new --list' to see all available templates." ) @@ -492,12 +486,12 @@ def init(): if not name: # Show full help like ./mfc.sh new -h - # pylint: disable=import-outside-toplevel import sys - from .user_guide import print_command_help - from .cli.commands import MFC_CLI_SCHEMA + from .cli.argparse_gen import generate_parser + from .cli.commands import MFC_CLI_SCHEMA from .state import MFCConfig + from .user_guide import print_command_help print_command_help("new", show_argparse=False) _, subparser_map = generate_parser(MFC_CLI_SCHEMA, MFCConfig()) diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index b6e195ecc3..a2444e4a9c 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -36,22 +36,37 @@ # Parameter-like names to skip (not actual MFC parameters) PARAM_SKIP = re.compile( r"^(src/|toolchain/|\.github|docs/|examples/|tests/)" # file paths - r"|^\.(?:true|false)\.$" # Fortran logicals - r"|^\d" # numeric values - r"|^[A-Z]" # constants/types (uppercase start) + r"|^\.(?:true|false)\.$" # Fortran logicals + r"|^\d" # numeric values + r"|^[A-Z]" # constants/types (uppercase start) ) # Backtick tokens in case.md that are not real parameters (analytical shorthands, # stress tensor component names, prose identifiers, hardcoded constants) CASE_MD_SKIP = { # Analytical shorthand variables (stretching formulas, "Analytical Definition" table) - "eps", "lx", "ly", "lz", "xc", "yc", "zc", "x_cb", + "eps", + "lx", + "ly", + "lz", + "xc", + "yc", + "zc", + "x_cb", # Stress tensor component names (descriptive, not params) - "tau_xx", "tau_xy", "tau_xz", "tau_yy", "tau_yz", "tau_zz", + "tau_xx", + "tau_xy", + "tau_xz", + "tau_yy", + "tau_yz", + "tau_zz", # Prose identifiers (example names, math symbols) - "scaling", "c_h", "thickness", + "scaling", + "c_h", + "thickness", # Hardcoded Fortran constants (not case-file params) - "init_dir", "zeros_default", + "init_dir", + "zeros_default", } # Docs to check for parameter references, with per-file skip sets @@ -79,10 +94,7 @@ def check_docs(repo_root: Path) -> list[str]: # Strip trailing punctuation that may have leaked in path_str = path_str.rstrip(".,;:!?") if not (repo_root / path_str).exists(): - errors.append( - f" {doc} references '{path_str}' but it does not exist." - " Fix: update the path or remove the reference" - ) + errors.append(f" {doc} references '{path_str}' but it does not exist. Fix: update the path or remove the reference") return errors @@ -111,10 +123,7 @@ def check_cite_keys(repo_root: Path) -> list[str]: for match in CITE_RE.finditer(text): key = match.group(1) if key.lower() not in valid_keys: - errors.append( - f" {rel} uses \\cite {key} but no bib entry found." - " Fix: add entry to docs/references.bib or fix the key" - ) + errors.append(f" {rel} uses \\cite {key} but no bib entry found. Fix: add entry to docs/references.bib or fix the key") return errors @@ -138,14 +147,14 @@ def _is_valid_param(param: str, valid_params: set, sub_params: set) -> bool: if "(" in param or ")" in param: return True # Skip indexed refs like patch_icpp(i)%vel(j) - base = param.split("%")[0] if "%" in param else param + base = param.split("%", maxsplit=1)[0] if "%" in param else param if base in valid_params or base in sub_params: return True # Compound params (with %): validate both family prefix and attribute if "%" in param: - sub = param.split("%")[-1] + sub = param.rsplit("%", maxsplit=1)[-1] family_ok = any(p.startswith(base + "%") for p in valid_params) return family_ok and sub in sub_params @@ -156,14 +165,14 @@ def _is_valid_param(param: str, valid_params: set, sub_params: set) -> bool: return False -def check_param_refs(repo_root: Path) -> list[str]: # pylint: disable=too-many-locals +def check_param_refs(repo_root: Path) -> list[str]: """Check that parameter names in documentation exist in the MFC registry.""" # Import REGISTRY from the toolchain toolchain_dir = str(repo_root / "toolchain") if toolchain_dir not in sys.path: sys.path.insert(0, toolchain_dir) try: - from mfc.params import REGISTRY # pylint: disable=import-outside-toplevel + from mfc.params import REGISTRY except ImportError: print(" Warning: could not import REGISTRY, skipping parameter check") return [] @@ -209,10 +218,7 @@ def check_param_refs(repo_root: Path) -> list[str]: # pylint: disable=too-many- # Normalize %% to % for lookup normalized = param.replace("%%", "%") if not _is_valid_param(normalized, valid_params, sub_params): - errors.append( - f" {doc_rel} references parameter '{param}' not in REGISTRY." - " Fix: check spelling or add to definitions.py" - ) + errors.append(f" {doc_rel} references parameter '{param}' not in REGISTRY. Fix: check spelling or add to definitions.py") return errors @@ -242,18 +248,12 @@ def check_math_syntax(repo_root: Path) -> list[str]: cleaned = re.sub(r"\\f\[.*?\\f\]", "", cleaned) if "$$" in cleaned: - errors.append( - f" {rel}:{i} uses $$...$$ display math." - " Fix: replace $$ with \\f[ and \\f]" - ) + errors.append(f" {rel}:{i} uses $$...$$ display math. Fix: replace $$ with \\f[ and \\f]") continue for m in re.finditer(r"\$([^$\n]+?)\$", cleaned): if re.search(r"\\[a-zA-Z]", m.group(1)): - errors.append( - f" {rel}:{i} uses $...$ with LaTeX commands." - " Fix: replace $ delimiters with \\f$ and \\f$" - ) + errors.append(f" {rel}:{i} uses $...$ with LaTeX commands. Fix: replace $ delimiters with \\f$ and \\f$") break # one error per line return errors @@ -261,18 +261,18 @@ def check_math_syntax(repo_root: Path) -> list[str]: def _gitignored_docs(repo_root: Path) -> set[str]: """Return set of gitignored doc file basenames.""" - import subprocess # pylint: disable=import-outside-toplevel + import subprocess doc_dir = repo_root / "docs" / "documentation" try: result = subprocess.run( ["git", "ls-files", "--ignored", "--exclude-standard", "-o"], - capture_output=True, text=True, cwd=repo_root, check=False, + capture_output=True, + text=True, + cwd=repo_root, + check=False, ) - return { - Path(f).name for f in result.stdout.splitlines() - if f.startswith(str(doc_dir.relative_to(repo_root))) - } + return {Path(f).name for f in result.stdout.splitlines() if f.startswith(str(doc_dir.relative_to(repo_root)))} except FileNotFoundError: return set() @@ -312,12 +312,7 @@ def check_section_anchors(repo_root: Path) -> list[str]: continue for m in re.finditer(r"\]\(#([\w-]+)\)", line): if m.group(1) not in anchors: - errors.append( - f" {rel}:{i} links to #{m.group(1)}" - f" but no {{#{m.group(1)}}} anchor exists." - f" Fix: add {{#{m.group(1)}}} to the target" - " section header" - ) + errors.append(f" {rel}:{i} links to #{m.group(1)} but no {{#{m.group(1)}}} anchor exists. Fix: add {{#{m.group(1)}}} to the target section header") return errors @@ -355,10 +350,7 @@ def check_doxygen_percent(repo_root: Path) -> list[str]: span = m.group(1) or m.group(2) if bad_pct_re.search(span): fixed = bad_pct_re.sub("%%", span) - errors.append( - f" {rel}:{i} Doxygen will eat the % in `{span}`." - f" Fix: `{fixed}`" - ) + errors.append(f" {rel}:{i} Doxygen will eat the % in `{span}`. Fix: `{fixed}`") return errors @@ -396,10 +388,7 @@ def check_page_refs(repo_root: Path) -> list[str]: for match in REF_RE.finditer(text): ref_target = match.group(1) if ref_target not in page_ids: - errors.append( - f" {rel} uses @ref {ref_target} but no @page with that ID exists." - " Fix: check the page ID or add @page declaration" - ) + errors.append(f" {rel} uses @ref {ref_target} but no @page with that ID exists. Fix: check the page ID or add @page declaration") return errors @@ -410,8 +399,8 @@ def check_physics_docs_coverage(repo_root: Path) -> list[str]: if toolchain_dir not in sys.path: sys.path.insert(0, toolchain_dir) try: - from mfc.case_validator import PHYSICS_DOCS # pylint: disable=import-outside-toplevel - from mfc.params.ast_analyzer import analyze_case_validator # pylint: disable=import-outside-toplevel + from mfc.case_validator import PHYSICS_DOCS + from mfc.params.ast_analyzer import analyze_case_validator except ImportError: return [] @@ -419,28 +408,28 @@ def check_physics_docs_coverage(repo_root: Path) -> list[str]: # references, and explanation) to case_validator.py to remove from this set. skip = { # Structural/mechanical checks (no physics meaning) - "check_parameter_types", # type validation - "check_output_format", # output format selection - "check_restart", # restart file logistics + "check_parameter_types", # type validation + "check_output_format", # output format selection + "check_restart", # restart file logistics "check_parallel_io_pre_process", # parallel I/O settings - "check_build_flags", # build-flag compatibility (no physics meaning) + "check_build_flags", # build-flag compatibility (no physics meaning) "check_geometry_precision_simulation", # build-flag compatibility (no physics meaning) - "check_misc_pre_process", # miscellaneous pre-process flags - "check_bc_patches", # boundary patch geometry - "check_grid_stretching", # grid stretching parameters - "check_qbmm_pre_process", # QBMM pre-process settings + "check_misc_pre_process", # miscellaneous pre-process flags + "check_bc_patches", # boundary patch geometry + "check_grid_stretching", # grid stretching parameters + "check_qbmm_pre_process", # QBMM pre-process settings "check_probe_integral_output", # probe/integral output settings - "check_finite_difference", # fd_order value validation - "check_flux_limiter", # output dimension requirements - "check_liutex_post", # output dimension requirements - "check_momentum_post", # output dimension requirements - "check_velocity_post", # output dimension requirements + "check_finite_difference", # fd_order value validation + "check_flux_limiter", # output dimension requirements + "check_liutex_post", # output dimension requirements + "check_momentum_post", # output dimension requirements + "check_velocity_post", # output dimension requirements "check_surface_tension_post", # output feature dependency - "check_no_flow_variables", # output variable selection - "check_partial_domain", # output format settings - "check_perturb_density", # parameter pairing validation - "check_qm", # output dimension requirements - "check_chemistry", # runtime Cantera validation only + "check_no_flow_variables", # output variable selection + "check_partial_domain", # output format settings + "check_perturb_density", # parameter pairing validation + "check_qm", # output dimension requirements + "check_chemistry", # runtime Cantera validation only # Awaiting proper physics documentation (math, references, explanation) "check_adaptive_time_stepping", "check_adv_n", @@ -474,11 +463,7 @@ def check_physics_docs_coverage(repo_root: Path) -> list[str]: continue if method in skip: continue - errors.append( - f" {method} has validation rules but no PHYSICS_DOCS entry." - " Fix: add entry to PHYSICS_DOCS in case_validator.py" - " or add to skip set in lint_docs.py" - ) + errors.append(f" {method} has validation rules but no PHYSICS_DOCS entry. Fix: add entry to PHYSICS_DOCS in case_validator.py or add to skip set in lint_docs.py") return errors @@ -508,17 +493,11 @@ def check_identifier_refs(repo_root: Path) -> list[str]: continue source_path = repo_root / source_file if not source_path.exists(): - errors.append( - f" contributing.md references `{identifier}` in {source_file}" - f" but {source_file} does not exist" - ) + errors.append(f" contributing.md references `{identifier}` in {source_file} but {source_file} does not exist") continue source_text = source_path.read_text(encoding="utf-8") if identifier not in source_text: - errors.append( - f" contributing.md references `{identifier}` but it was not" - f" found in {source_file}. Fix: update the docs or the identifier" - ) + errors.append(f" contributing.md references `{identifier}` but it was not found in {source_file}. Fix: update the docs or the identifier") return errors @@ -533,7 +512,7 @@ def check_cli_refs(repo_root: Path) -> list[str]: if toolchain_dir not in sys.path: sys.path.insert(0, toolchain_dir) try: - from mfc.cli.commands import MFC_CLI_SCHEMA # pylint: disable=import-outside-toplevel + from mfc.cli.commands import MFC_CLI_SCHEMA except ImportError: return [] @@ -554,11 +533,7 @@ def check_cli_refs(repo_root: Path) -> list[str]: seen.add(cmd) continue seen.add(cmd) - errors.append( - f" running.md references './mfc.sh {cmd}' but '{cmd}'" - " is not a known CLI command." - " Fix: update the command name or remove the reference" - ) + errors.append(f" running.md references './mfc.sh {cmd}' but '{cmd}' is not a known CLI command. Fix: update the command name or remove the reference") return errors @@ -590,36 +565,23 @@ def check_unpaired_math(repo_root: Path) -> list[str]: # Count \f$ occurrences (should be even per line for inline math) inline_count = len(re.findall(r"\\f\$", line)) if inline_count % 2 != 0: - errors.append( - f" {rel}:{i} has {inline_count} \\f$ delimiter(s) (odd)." - " Fix: ensure every \\f$ has a matching closing \\f$" - ) + errors.append(f" {rel}:{i} has {inline_count} \\f$ delimiter(s) (odd). Fix: ensure every \\f$ has a matching closing \\f$") # Track \f[ / \f] balance opens = len(re.findall(r"\\f\[", line)) closes = len(re.findall(r"\\f\]", line)) for _ in range(opens): if display_math_open: - errors.append( - f" {rel}:{i} opens \\f[ but previous \\f[" - f" from line {display_math_open} is still open." - " Fix: add missing \\f]" - ) + errors.append(f" {rel}:{i} opens \\f[ but previous \\f[ from line {display_math_open} is still open. Fix: add missing \\f]") display_math_open = i for _ in range(closes): if not display_math_open: - errors.append( - f" {rel}:{i} has \\f] without a preceding \\f[." - " Fix: add missing \\f[ or remove extra \\f]" - ) + errors.append(f" {rel}:{i} has \\f] without a preceding \\f[. Fix: add missing \\f[ or remove extra \\f]") else: display_math_open = 0 if display_math_open: - errors.append( - f" {rel}:{display_math_open} opens \\f[ that is never closed." - " Fix: add \\f] to close the display math block" - ) + errors.append(f" {rel}:{display_math_open} opens \\f[ that is never closed. Fix: add \\f] to close the display math block") return errors @@ -627,19 +589,45 @@ def check_unpaired_math(repo_root: Path) -> list[str]: # Doxygen block commands that are incorrectly processed inside backtick # code spans (known Doxygen bug, see github.com/doxygen/doxygen/issues/6054). _DOXYGEN_BLOCK_CMDS = { - "code", "endcode", "verbatim", "endverbatim", - "dot", "enddot", "msc", "endmsc", - "startuml", "enduml", - "latexonly", "endlatexonly", "htmlonly", "endhtmlonly", - "xmlonly", "endxmlonly", "rtfonly", "endrtfonly", - "manonly", "endmanonly", "docbookonly", "enddocbookonly", - "todo", "deprecated", "bug", "test", - "note", "warning", "attention", "remark", - "brief", "details", "param", "return", "returns", + "code", + "endcode", + "verbatim", + "endverbatim", + "dot", + "enddot", + "msc", + "endmsc", + "startuml", + "enduml", + "latexonly", + "endlatexonly", + "htmlonly", + "endhtmlonly", + "xmlonly", + "endxmlonly", + "rtfonly", + "endrtfonly", + "manonly", + "endmanonly", + "docbookonly", + "enddocbookonly", + "todo", + "deprecated", + "bug", + "test", + "note", + "warning", + "attention", + "remark", + "brief", + "details", + "param", + "return", + "returns", } -def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: # pylint: disable=too-many-locals +def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: """Check for Doxygen @/\\ commands inside backtick code spans. Doxygen processes certain block commands even inside backtick code @@ -674,11 +662,7 @@ def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]: # pylint cmd_match = doxy_cmd_re.search(span) if cmd_match: cmd = cmd_match.group(0) - errors.append( - f" {rel}:{i} backtick span contains Doxygen" - f" command '{cmd}' which may be processed." - " Fix: use a fenced code block or rephrase" - ) + errors.append(f" {rel}:{i} backtick span contains Doxygen command '{cmd}' which may be processed. Fix: use a fenced code block or rephrase") return errors @@ -713,11 +697,7 @@ def check_single_quote_in_backtick(repo_root: Path) -> list[str]: for m in single_bt_re.finditer(line): span = m.group(1) if "'" in span: - errors.append( - f" {rel}:{i} single-backtick span `{span}` contains" - " a single quote, which Doxygen treats as ending the" - f" span. Fix: use double backticks ``{span}``" - ) + errors.append(f" {rel}:{i} single-backtick span `{span}` contains a single quote, which Doxygen treats as ending the span. Fix: use double backticks ``{span}``") return errors @@ -734,7 +714,7 @@ def check_single_quote_in_backtick(repo_root: Path) -> list[str]: } -def check_amsmath_in_doxygen_math(repo_root: Path) -> list[str]: # pylint: disable=too-many-locals +def check_amsmath_in_doxygen_math(repo_root: Path) -> list[str]: """Flag AMSmath-only commands in Doxygen math that may not render.""" doc_dir = repo_root / "docs" / "documentation" if not doc_dir.exists(): @@ -766,10 +746,7 @@ def check_amsmath_in_doxygen_math(repo_root: Path) -> list[str]: # pylint: disa for m in inline_re.finditer(line): for cm in ams_re.finditer(m.group(1)): alt = _AMSMATH_ONLY_CMDS[cm.group(1)] - errors.append( - f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in" - f" math. Fix: use \\{alt} instead" - ) + errors.append(f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in math. Fix: use \\{alt} instead") # Check display math lines between \f[ and \f] if "\\f[" in line: @@ -777,10 +754,7 @@ def check_amsmath_in_doxygen_math(repo_root: Path) -> list[str]: # pylint: disa if in_display: for cm in ams_re.finditer(line): alt = _AMSMATH_ONLY_CMDS[cm.group(1)] - errors.append( - f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in" - f" math. Fix: use \\{alt} instead" - ) + errors.append(f" {rel}:{i} uses \\{cm.group(1)} (AMSmath) in math. Fix: use \\{alt} instead") if "\\f]" in line: in_display = False @@ -823,10 +797,7 @@ def check_module_briefs(repo_root: Path) -> list[str]: if not found_brief: rel = fpp.relative_to(repo_root) - errors.append( - f" {rel} has no module-level !> @brief before the module declaration." - " Fix: add '!> @brief ' on the line before 'module ...'" - ) + errors.append(f" {rel} has no module-level !> @brief before the module declaration. Fix: add '!> @brief ' on the line before 'module ...'") return errors @@ -856,7 +827,7 @@ def check_module_categories(repo_root: Path) -> list[str]: cats = ", ".join(e["category"] for e in categories) errors.append( f" {rel}: module {name} is not in docs/module_categories.json.\n" - f" Fix: open docs/module_categories.json and add \"{name}\" to one of: {cats}.\n" + f' Fix: open docs/module_categories.json and add "{name}" to one of: {cats}.\n' f" This ensures it appears on the Code Architecture page." ) diff --git a/toolchain/mfc/lock.py b/toolchain/mfc/lock.py index a4e0514dcd..02a8732f9b 100644 --- a/toolchain/mfc/lock.py +++ b/toolchain/mfc/lock.py @@ -1,16 +1,16 @@ -import os, dataclasses +import dataclasses +import os -from . import state, common -from .state import MFCConfig +from . import common, state from .printer import cons - +from .state import MFCConfig MFC_LOCK_CURRENT_VERSION: int = 8 @dataclasses.dataclass class MFCLockData: - config: MFCConfig + config: MFCConfig version: int @@ -18,12 +18,11 @@ class MFCLockData: def init(): - # pylint: disable=global-statement - global data + global data # noqa: PLW0603 if not os.path.exists(common.MFC_LOCK_FILEPATH): config = MFCConfig() - data = MFCLockData(config, MFC_LOCK_CURRENT_VERSION) + data = MFCLockData(config, MFC_LOCK_CURRENT_VERSION) state.gCFG = config common.create_file(common.MFC_LOCK_FILEPATH) @@ -33,8 +32,7 @@ def init(): def load(): - # pylint: disable=global-statement - global data + global data # noqa: PLW0603 d = common.file_load_yaml(common.MFC_LOCK_FILEPATH) @@ -48,20 +46,18 @@ def load(): """) config = MFCConfig.from_dict(d["config"]) - data = MFCLockData(config, d["version"]) + data = MFCLockData(config, d["version"]) state.gCFG = config def write(): - # pylint: disable=global-statement, global-variable-not-assigned - global data + global data # noqa: PLW0603 common.file_dump_yaml(common.MFC_LOCK_FILEPATH, dataclasses.asdict(data)) def switch(to: MFCConfig): - # pylint: disable=global-statement, global-variable-not-assigned - global data + global data # noqa: PLW0603 if to == data.config: return @@ -70,5 +66,5 @@ def switch(to: MFCConfig): cons.print("") data.config = to - state.gCFG = to + state.gCFG = to write() diff --git a/toolchain/mfc/packer/errors.py b/toolchain/mfc/packer/errors.py index 66ab8d4463..ab889208f8 100644 --- a/toolchain/mfc/packer/errors.py +++ b/toolchain/mfc/packer/errors.py @@ -1,4 +1,6 @@ -import dataclasses, math +import dataclasses +import math + @dataclasses.dataclass(repr=False) class Error: diff --git a/toolchain/mfc/packer/pack.py b/toolchain/mfc/packer/pack.py index 1ad1a76834..0f778b1ba9 100644 --- a/toolchain/mfc/packer/pack.py +++ b/toolchain/mfc/packer/pack.py @@ -1,13 +1,16 @@ -import dataclasses, typing, sys, os, re, math +import dataclasses +import math +import os +import re +import sys +import typing from datetime import datetime from pathlib import Path -from .. import common -from ..run import input -from ..build import get_configured_targets -from ..state import CFG - - +from .. import common +from ..build import get_configured_targets +from ..run import input +from ..state import CFG # This class maps to the data contained in one file in D/ diff --git a/toolchain/mfc/packer/packer.py b/toolchain/mfc/packer/packer.py index 0ae6671e45..b7145c3d9f 100644 --- a/toolchain/mfc/packer/packer.py +++ b/toolchain/mfc/packer/packer.py @@ -1,11 +1,13 @@ -import typing, os.path +import os.path +import typing +from ..common import MFCException from ..printer import cons from ..state import ARG, ARGS -from . import pack as _pack from . import errors +from . import pack as _pack from . import tol as packtol -from ..common import MFCException + def load(packpath: str) -> _pack.Pack: return _pack.load(packpath) diff --git a/toolchain/mfc/packer/tol.py b/toolchain/mfc/packer/tol.py index 2ec80f61e7..4ec9b0faf5 100644 --- a/toolchain/mfc/packer/tol.py +++ b/toolchain/mfc/packer/tol.py @@ -1,7 +1,8 @@ -import math, typing +import math +import typing -from .pack import Pack -from .errors import compute_error, AverageError, Error +from .errors import AverageError, Error, compute_error +from .pack import Pack Tolerance = Error @@ -46,7 +47,6 @@ def _format_error_diagnostics(max_abs_info, max_rel_info) -> str: return diagnostic_msg -# pylint: disable=too-many-return-statements def compare(candidate: Pack, golden: Pack, tol: Tolerance) -> typing.Tuple[Error, str]: # Keep track of the average error avg_err = AverageError() @@ -97,7 +97,9 @@ def raise_err_with_failing_diagnostics(msg: str): return avg_err.get(), None -def find_maximum_errors_among_failing(candidate: Pack, golden: Pack, tol: Tolerance) -> typing.Tuple[typing.Optional[typing.Tuple[str, int, float, float, float, float]], typing.Optional[typing.Tuple[str, int, float, float, float, float]]]: +def find_maximum_errors_among_failing( + candidate: Pack, golden: Pack, tol: Tolerance, +) -> typing.Tuple[typing.Optional[typing.Tuple[str, int, float, float, float, float]], typing.Optional[typing.Tuple[str, int, float, float, float, float]]]: """ Scan all files to find the maximum absolute and relative errors among FAILING variables only. A variable fails if is_close(error, tol) returns False. diff --git a/toolchain/mfc/params/__init__.py b/toolchain/mfc/params/__init__.py index 57e1912276..d905a578e5 100644 --- a/toolchain/mfc/params/__init__.py +++ b/toolchain/mfc/params/__init__.py @@ -19,13 +19,12 @@ register new parameters will raise RegistryFrozenError. """ -from .registry import REGISTRY, RegistryFrozenError -from .schema import ParamDef, ParamType - # IMPORTANT: This import populates REGISTRY with all parameter definitions # and freezes it. It must come after REGISTRY is imported and must not be removed. -from . import definitions # noqa: F401 pylint: disable=unused-import +from . import definitions # noqa: F401 from .definitions import CONSTRAINTS, DEPENDENCIES, get_value_label +from .registry import REGISTRY, RegistryFrozenError +from .schema import ParamDef, ParamType __all__ = [ 'REGISTRY', 'RegistryFrozenError', 'ParamDef', 'ParamType', diff --git a/toolchain/mfc/params/ast_analyzer.py b/toolchain/mfc/params/ast_analyzer.py index 1bd841f1a7..cfc96d581a 100644 --- a/toolchain/mfc/params/ast_analyzer.py +++ b/toolchain/mfc/params/ast_analyzer.py @@ -11,11 +11,10 @@ import ast import re +from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Set -from collections import defaultdict - # --------------------------------------------------------------------------- # Data structures @@ -56,7 +55,7 @@ def _extract_message(node: ast.AST) -> Optional[str]: # Unparse the expression to get a readable approximation try: parts.append(ast.unparse(value.value)) - except Exception: # pylint: disable=broad-except + except Exception: parts.append("?") else: parts.append("?") @@ -77,7 +76,7 @@ def _resolve_fstring(node: ast.JoinedStr, subs: Dict[str, str]) -> Optional[str] else: try: parts.append(ast.unparse(v.value)) - except Exception: # pylint: disable=broad-except + except Exception: parts.append("?") else: parts.append("?") @@ -106,7 +105,7 @@ def _is_self_get(call: ast.Call) -> bool: # AST analysis: methods, call graph, rules # --------------------------------------------------------------------------- -class CaseValidatorAnalyzer(ast.NodeVisitor): # pylint: disable=too-many-instance-attributes +class CaseValidatorAnalyzer(ast.NodeVisitor): """ Analyzes the CaseValidator class: @@ -187,7 +186,7 @@ def _enrich_rules_with_if_guards(self, func: ast.FunctionDef, For each if-block, extract guard params from the test condition and add them to every rule whose lineno falls within the block's line range. """ - for node in ast.walk(func): # pylint: disable=too-many-nested-blocks + for node in ast.walk(func): if not isinstance(node, ast.If): continue # Extract params from the if-test condition @@ -215,7 +214,7 @@ def _enrich_rules_with_if_guards(self, func: ast.FunctionDef, rule.params.append(gp) break - def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: # pylint: disable=too-many-nested-blocks + def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: """ Look for assignments like: igr = self.get('igr', 'F') == 'T' @@ -225,7 +224,7 @@ def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: # py Uses ast.walk to find assignments at any nesting depth (inside if/for/with blocks). """ m: Dict[str, str] = {} - for node in ast.walk(func): # pylint: disable=too-many-nested-blocks + for node in ast.walk(func): if isinstance(node, ast.Assign): # Handle both direct calls and comparisons value = node.value @@ -235,7 +234,7 @@ def _build_local_param_map(self, func: ast.FunctionDef) -> Dict[str, str]: # py if isinstance(value, ast.Call): call = value - if ( # pylint: disable=too-many-boolean-expressions + if ( isinstance(call.func, ast.Attribute) and isinstance(call.func.value, ast.Name) and call.func.value.id == "self" @@ -354,7 +353,7 @@ def _resolve_loop_gets(stmts: list, subs: Dict[str, str]) -> Dict[str, str]: m[target.id] = param_name return m - def _create_loop_rules(self, stmts: list, method_name: str, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals + def _create_loop_rules(self, stmts: list, method_name: str, local_map: Dict[str, str], subs: Dict[str, str], loop_guard: Optional[str] = None): """Create Rules for self.prohibit()/self.warn() calls found in loop body statements.""" @@ -434,7 +433,7 @@ def visit_Call(self, node: ast.Call): # detect self.prohibit(, "") and self.warn(, "") # Skip calls already handled by loop expansion - if ( # pylint: disable=too-many-boolean-expressions + if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "self" @@ -493,7 +492,7 @@ def _extract_params(self, condition: ast.AST) -> Set[str]: # direct self.get('param_name') if isinstance(node, ast.Call): - if ( # pylint: disable=too-many-boolean-expressions + if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "self" @@ -518,7 +517,7 @@ def _extract_method_guard(func: ast.FunctionDef, local_param_map: Dict[str, str] return The guarded variable's param is the trigger for all rules in that method. """ - for stmt in func.body: # pylint: disable=too-many-nested-blocks + for stmt in func.body: if not isinstance(stmt, ast.If): continue @@ -582,7 +581,7 @@ def _extract_trigger_from_condition(condition: ast.AST, local_param_map: Dict[st if node.id in alias_map and alias_map[node.id]: return alias_map[node.id][0] if isinstance(node, ast.Call): - if ( # pylint: disable=too-many-boolean-expressions + if ( isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name) and node.func.value.id == "self" @@ -646,7 +645,7 @@ def classify_message(msg: str) -> str: """ text = msg.lower() - if ( # pylint: disable=too-many-boolean-expressions + if ( "not compatible" in text or "does not support" in text or "cannot be used" in text @@ -660,7 +659,7 @@ def classify_message(msg: str) -> str: ): return "incompatibility" - if ( # pylint: disable=too-many-boolean-expressions + if ( "requires" in text or "must be set if" in text or "must be specified" in text @@ -671,7 +670,7 @@ def classify_message(msg: str) -> str: ): return "requirement" - if ( # pylint: disable=too-many-boolean-expressions + if ( "must be between" in text or "must be positive" in text or "must be non-negative" in text diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index 4b9c98cd7f..0a3e02bff9 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -3,12 +3,13 @@ Single file containing all ~3,300 parameter definitions using loops. This replaces the definitions/ directory. -""" # pylint: disable=too-many-lines +""" import re -from typing import Dict, Any -from .schema import ParamDef, ParamType +from typing import Any, Dict + from .registry import REGISTRY +from .schema import ParamDef, ParamType # Index limits NP, NF, NI, NA, NPR, NB = 10, 10, 1000, 4, 10, 10 # patches, fluids, ibs, acoustic, probes, bc_patches @@ -465,7 +466,7 @@ def _lookup_hint(name): def _validate_constraint(param_name: str, constraint: Dict[str, Any]) -> None: """Validate a constraint dict has valid keys with 'did you mean?' suggestions.""" # Import here to avoid circular import at module load time - from .suggest import invalid_key_error # pylint: disable=import-outside-toplevel + from .suggest import invalid_key_error invalid_keys = set(constraint.keys()) - _VALID_CONSTRAINT_KEYS if invalid_keys: @@ -501,7 +502,7 @@ def _validate_constraint(param_name: str, constraint: Dict[str, Any]) -> None: def _validate_dependency(param_name: str, dependency: Dict[str, Any]) -> None: """Validate a dependency dict has valid structure with 'did you mean?' suggestions.""" # Import here to avoid circular import at module load time - from .suggest import invalid_key_error # pylint: disable=import-outside-toplevel + from .suggest import invalid_key_error invalid_keys = set(dependency.keys()) - _VALID_DEPENDENCY_KEYS if invalid_keys: @@ -808,7 +809,7 @@ def get_value_label(param_name: str, value: int) -> str: }, } -def _r(name, ptype, tags=None, desc=None, hint=None, math=None): # pylint: disable=too-many-arguments,too-many-positional-arguments +def _r(name, ptype, tags=None, desc=None, hint=None, math=None): """Register a parameter with optional feature tags and description.""" if hint is None: hint = _lookup_hint(name) @@ -831,7 +832,7 @@ def _r(name, ptype, tags=None, desc=None, hint=None, math=None): # pylint: disa )) -def _load(): # pylint: disable=too-many-locals,too-many-statements +def _load(): """Load all parameter definitions.""" INT, REAL, LOG, STR = ParamType.INT, ParamType.REAL, ParamType.LOG, ParamType.STR A_REAL = ParamType.ANALYTIC_REAL diff --git a/toolchain/mfc/params/descriptions.py b/toolchain/mfc/params/descriptions.py index 801e094732..b0ccf7965e 100644 --- a/toolchain/mfc/params/descriptions.py +++ b/toolchain/mfc/params/descriptions.py @@ -517,7 +517,7 @@ def get_description(param_name: str) -> str: return template.format(*match.groups()) # 3. Auto-generated description from registry (set by _auto_describe at registration) - from . import REGISTRY # pylint: disable=import-outside-toplevel + from . import REGISTRY param = REGISTRY.all_params.get(param_name) if param and param.description: return param.description @@ -526,7 +526,7 @@ def get_description(param_name: str) -> str: return _infer_from_naming(param_name) -def _infer_from_naming(param_name: str) -> str: # pylint: disable=too-many-return-statements,too-many-branches +def _infer_from_naming(param_name: str) -> str: """Infer description from naming conventions.""" name = param_name @@ -650,7 +650,7 @@ def get_math_symbol(param_name: str) -> str: Looks up the math_symbol field from the parameter registry (single source of truth). Symbols are defined via math= in the _r() calls in definitions.py. """ - from . import REGISTRY # pylint: disable=import-outside-toplevel + from . import REGISTRY param = REGISTRY.all_params.get(param_name) return param.math_symbol if param else "" diff --git a/toolchain/mfc/params/generators/__init__.py b/toolchain/mfc/params/generators/__init__.py index b06572cf5c..bbc8445727 100644 --- a/toolchain/mfc/params/generators/__init__.py +++ b/toolchain/mfc/params/generators/__init__.py @@ -1,6 +1,6 @@ """Code Generators for Parameter Schema.""" -from .json_schema_gen import generate_json_schema from .docs_gen import generate_parameter_docs +from .json_schema_gen import generate_json_schema __all__ = ['generate_json_schema', 'generate_parameter_docs'] diff --git a/toolchain/mfc/params/generators/docs_gen.py b/toolchain/mfc/params/generators/docs_gen.py index 2e2f23e3e9..9a84958ff9 100644 --- a/toolchain/mfc/params/generators/docs_gen.py +++ b/toolchain/mfc/params/generators/docs_gen.py @@ -7,15 +7,15 @@ from __future__ import annotations -from typing import Any, Dict, List, Tuple -from collections import defaultdict import re +from collections import defaultdict +from typing import Any, Dict, List, Tuple -from ..schema import ParamType -from ..registry import REGISTRY -from ..descriptions import get_description, get_math_symbol +from .. import definitions # noqa: F401 from ..ast_analyzer import analyze_case_validator, classify_message -from .. import definitions # noqa: F401 pylint: disable=unused-import +from ..descriptions import get_description, get_math_symbol +from ..registry import REGISTRY +from ..schema import ParamType def _get_family(name: str) -> str: @@ -187,7 +187,7 @@ def _escape_pct_outside_backticks(text: str) -> str: def _get_param_pattern(): - global _PARAM_PATTERN # noqa: PLW0603 pylint: disable=global-statement + global _PARAM_PATTERN # noqa: PLW0603 if _PARAM_PATTERN is None: _PARAM_PATTERN = _build_param_name_pattern() return _PARAM_PATTERN @@ -195,7 +195,7 @@ def _get_param_pattern(): def _build_reverse_dep_map() -> Dict[str, List[Tuple[str, str]]]: """Build map from target param -> [(relation, source_param), ...] from DEPENDENCIES.""" - from ..definitions import DEPENDENCIES # pylint: disable=import-outside-toplevel + from ..definitions import DEPENDENCIES reverse: Dict[str, List[Tuple[str, str]]] = {} for param, dep in DEPENDENCIES.items(): if "when_true" in dep: @@ -217,13 +217,13 @@ def _build_reverse_dep_map() -> Dict[str, List[Tuple[str, str]]]: def _get_reverse_deps(): - global _REVERSE_DEPS # noqa: PLW0603 pylint: disable=global-statement + global _REVERSE_DEPS # noqa: PLW0603 if _REVERSE_DEPS is None: _REVERSE_DEPS = _build_reverse_dep_map() return _REVERSE_DEPS -def _format_tag_annotation(param_name: str, param) -> str: # pylint: disable=too-many-locals +def _format_tag_annotation(param_name: str, param) -> str: """ Return a short annotation for params with no schema constraints and no AST rules. @@ -270,7 +270,7 @@ def _format_tag_annotation(param_name: str, param) -> str: # pylint: disable=to return param.hint # 5. Tag-based label (from TAG_DISPLAY_NAMES in definitions.py) - from ..definitions import TAG_DISPLAY_NAMES # pylint: disable=import-outside-toplevel + from ..definitions import TAG_DISPLAY_NAMES for tag, display_name in TAG_DISPLAY_NAMES.items(): if tag in param.tags: return f"{display_name} parameter" @@ -278,7 +278,7 @@ def _format_tag_annotation(param_name: str, param) -> str: # pylint: disable=to return "" -def _format_validator_rules(param_name: str, by_trigger: Dict[str, list], # pylint: disable=too-many-locals +def _format_validator_rules(param_name: str, by_trigger: Dict[str, list], by_param: Dict[str, list] | None = None) -> str: """Format AST-extracted validator rules for a parameter's Constraints column. @@ -343,7 +343,7 @@ def _format_validator_rules(param_name: str, by_trigger: Dict[str, list], # pyl return "; ".join(parts) -def generate_parameter_docs() -> str: # pylint: disable=too-many-locals,too-many-statements +def generate_parameter_docs() -> str: """Generate markdown documentation for all parameters.""" # AST-extract rules from case_validator.py analysis = analyze_case_validator() diff --git a/toolchain/mfc/params/generators/json_schema_gen.py b/toolchain/mfc/params/generators/json_schema_gen.py index 21a8feb8b1..9ef6800e54 100644 --- a/toolchain/mfc/params/generators/json_schema_gen.py +++ b/toolchain/mfc/params/generators/json_schema_gen.py @@ -3,13 +3,13 @@ Generates VS Code / PyCharm compatible JSON Schema for case file auto-completion. """ -# pylint: disable=import-outside-toplevel import json -from typing import Dict, Any -from ..schema import ParamType +from typing import Any, Dict + +from .. import definitions # noqa: F401 from ..registry import REGISTRY -from .. import definitions # noqa: F401 pylint: disable=unused-import +from ..schema import ParamType def _param_type_to_json_schema(param_type: ParamType, constraints: Dict = None) -> Dict[str, Any]: diff --git a/toolchain/mfc/params/namelist_parser.py b/toolchain/mfc/params/namelist_parser.py index ae4876ca02..fe907befd3 100644 --- a/toolchain/mfc/params/namelist_parser.py +++ b/toolchain/mfc/params/namelist_parser.py @@ -13,7 +13,6 @@ from pathlib import Path from typing import Dict, Set - # Fallback parameters for when Fortran source files are not available. # Generated from the namelist definitions in src/*/m_start_up.fpp. # To regenerate: python3 toolchain/mfc/params/namelist_parser.py diff --git a/toolchain/mfc/params/registry.py b/toolchain/mfc/params/registry.py index f6f66b3176..8868f799ae 100644 --- a/toolchain/mfc/params/registry.py +++ b/toolchain/mfc/params/registry.py @@ -25,10 +25,10 @@ register new parameters after freezing will raise RuntimeError. """ -from typing import Dict, Set, Mapping, Any -from types import MappingProxyType from collections import defaultdict from functools import lru_cache +from types import MappingProxyType +from typing import Any, Dict, Mapping, Set from .schema import ParamDef @@ -179,12 +179,12 @@ def get_validator(self): @lru_cache(maxsize=1) -def _get_cached_validator(registry_id: int): # pylint: disable=unused-argument +def _get_cached_validator(registry_id: int): """Cache the validator at module level (registry is immutable after freeze). Note: registry_id is used as cache key to invalidate when registry changes. """ - import fastjsonschema # pylint: disable=import-outside-toplevel + import fastjsonschema return fastjsonschema.compile(REGISTRY.get_json_schema()) diff --git a/toolchain/mfc/params/schema.py b/toolchain/mfc/params/schema.py index 08ef309254..4799bf6cc7 100644 --- a/toolchain/mfc/params/schema.py +++ b/toolchain/mfc/params/schema.py @@ -6,7 +6,7 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Set, Any, Optional, Dict, List +from typing import Any, Dict, List, Optional, Set from .errors import constraint_error @@ -40,7 +40,7 @@ def json_schema(self) -> Dict[str, Any]: @dataclass -class ParamDef: # pylint: disable=too-many-instance-attributes +class ParamDef: """ Definition of a single MFC parameter. diff --git a/toolchain/mfc/params/suggest.py b/toolchain/mfc/params/suggest.py index 6f46107b33..4308ca7cf7 100644 --- a/toolchain/mfc/params/suggest.py +++ b/toolchain/mfc/params/suggest.py @@ -11,12 +11,12 @@ module initialization, catching developer typos in CONSTRAINTS/DEPENDENCIES dicts. """ -from typing import List, Iterable from functools import lru_cache +from typing import Iterable, List # Import rapidfuzz - falls back gracefully if not installed try: - from rapidfuzz import process, fuzz + from rapidfuzz import fuzz, process RAPIDFUZZ_AVAILABLE = True except ImportError: RAPIDFUZZ_AVAILABLE = False @@ -104,7 +104,7 @@ def suggest_parameter(unknown_param: str) -> List[str]: List of similar valid parameter names. """ # Import here to avoid circular import (registry imports definitions which may use suggest) - from .registry import REGISTRY # pylint: disable=import-outside-toplevel + from .registry import REGISTRY return suggest_similar(unknown_param, REGISTRY.all_params.keys()) diff --git a/toolchain/mfc/params/validate.py b/toolchain/mfc/params/validate.py index 04548b4066..84d871af94 100644 --- a/toolchain/mfc/params/validate.py +++ b/toolchain/mfc/params/validate.py @@ -27,8 +27,12 @@ 3. Physics validation (via case_validator.py) """ -from typing import Dict, Any, List, Optional, Tuple -from .registry import REGISTRY +from typing import Any, Dict, List, Optional, Tuple + +# Note: definitions is imported by params/__init__.py to populate REGISTRY. +# This redundant import ensures REGISTRY is populated even if this module +# is imported directly (e.g., during testing). +from . import definitions # noqa: F401 from .errors import ( dependency_error, dependency_recommendation, @@ -36,11 +40,8 @@ format_error_list, unknown_param_error, ) +from .registry import REGISTRY from .suggest import suggest_parameter -# Note: definitions is imported by params/__init__.py to populate REGISTRY. -# This redundant import ensures REGISTRY is populated even if this module -# is imported directly (e.g., during testing). -from . import definitions # noqa: F401 pylint: disable=unused-import def check_unknown_params(params: Dict[str, Any]) -> List[str]: @@ -96,7 +97,7 @@ def validate_constraints(params: Dict[str, Any]) -> List[str]: return errors -def _check_condition( # pylint: disable=too-many-arguments,too-many-positional-arguments +def _check_condition( name: str, condition: Dict[str, Any], condition_label: Optional[str], @@ -127,7 +128,7 @@ def _check_condition( # pylint: disable=too-many-arguments,too-many-positional- )) -def check_dependencies(params: Dict[str, Any]) -> Tuple[List[str], List[str]]: # pylint: disable=too-many-branches +def check_dependencies(params: Dict[str, Any]) -> Tuple[List[str], List[str]]: """ Check parameter dependencies. diff --git a/toolchain/mfc/params_cmd.py b/toolchain/mfc/params_cmd.py index a25b104c26..967541f819 100644 --- a/toolchain/mfc/params_cmd.py +++ b/toolchain/mfc/params_cmd.py @@ -3,17 +3,19 @@ Provides CLI access to search and explore MFC's ~3,300 case parameters. """ -# pylint: disable=import-outside-toplevel import re -from .state import ARG + from .printer import cons +from .state import ARG def params(): """Execute the params command based on CLI arguments.""" - from .params import REGISTRY - from .params import definitions # noqa: F401 pylint: disable=unused-import + from .params import ( + REGISTRY, + definitions, # noqa: F401 + ) query = ARG("query") type_filter = ARG("param_type") @@ -46,7 +48,7 @@ def params(): cons.print(" Use './mfc.sh params -F' to see all feature groups") -def _collapse_indexed_params(matches): # pylint: disable=too-many-locals,too-many-branches,too-many-statements +def _collapse_indexed_params(matches): """ Collapse indexed parameters into patterns. @@ -300,7 +302,7 @@ def _show_families(registry, limit): cons.print("[yellow]Tip:[/yellow] Use './mfc.sh params ' to see parameters in a family") -def _search_params(registry, query, type_filter, limit, describe=False, search_descriptions=True): # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals +def _search_params(registry, query, type_filter, limit, describe=False, search_descriptions=True): """Search for parameters matching a query.""" from .params.descriptions import get_description @@ -348,7 +350,7 @@ def _search_params(registry, query, type_filter, limit, describe=False, search_d cons.print(f" [dim]... {len(collapsed) - limit} more patterns (use -n {len(collapsed)} to show all)[/dim]") -def _show_collapsed_results(collapsed, describe=False): # pylint: disable=too-many-branches +def _show_collapsed_results(collapsed, describe=False): """Show collapsed search results.""" from .params.descriptions import get_description, get_pattern_description @@ -390,11 +392,10 @@ def _show_collapsed_results(collapsed, describe=False): # pylint: disable=too-m name, param, count, range_str = item if count > 1: cons.print(f" {name:<40} {param.param_type.name:12} {count:>4} {range_str}") + elif has_ranges: + cons.print(f" {name:<40} {param.param_type.name:12} {count:>4}") else: - if has_ranges: - cons.print(f" {name:<40} {param.param_type.name:12} {count:>4}") - else: - cons.print(f" {name:<40} {param.param_type.name:12}") + cons.print(f" {name:<40} {param.param_type.name:12}") else: name, param, count = item if has_ranges: diff --git a/toolchain/mfc/params_tests/coverage.py b/toolchain/mfc/params_tests/coverage.py index 5d80fbafc6..629d67e89c 100644 --- a/toolchain/mfc/params_tests/coverage.py +++ b/toolchain/mfc/params_tests/coverage.py @@ -7,9 +7,9 @@ import ast import json -from pathlib import Path -from typing import Dict, List, Any from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List @dataclass diff --git a/toolchain/mfc/params_tests/inventory.py b/toolchain/mfc/params_tests/inventory.py index 96a88d124d..6e6dae16f3 100644 --- a/toolchain/mfc/params_tests/inventory.py +++ b/toolchain/mfc/params_tests/inventory.py @@ -4,14 +4,14 @@ Exports all MFC parameters with their types and tags to JSON for analysis. """ -import re import json +import re from pathlib import Path -from typing import Dict, Any +from typing import Any, Dict -from ..run.case_dicts import ALL from ..params import REGISTRY from ..params.schema import ParamType +from ..run.case_dicts import ALL def get_param_type_name(param_type) -> str: diff --git a/toolchain/mfc/params_tests/mutation_tests.py b/toolchain/mfc/params_tests/mutation_tests.py index 15bbfa0885..ffe6a77fb5 100644 --- a/toolchain/mfc/params_tests/mutation_tests.py +++ b/toolchain/mfc/params_tests/mutation_tests.py @@ -7,9 +7,9 @@ import json import subprocess -from pathlib import Path -from typing import Dict, Any, List, Tuple from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Tuple from ..case_validator import CaseValidator @@ -271,7 +271,7 @@ def print_mutation_report(): for r in results["missed_details"][:10]: print(f" {r.case_name}") print(f" {r.param_name}: {r.original_value} -> {r.mutated_value}") - print(f" No validation error raised!") + print(" No validation error raised!") print() diff --git a/toolchain/mfc/params_tests/negative_tests.py b/toolchain/mfc/params_tests/negative_tests.py index 683c3f41c3..14b5d09186 100644 --- a/toolchain/mfc/params_tests/negative_tests.py +++ b/toolchain/mfc/params_tests/negative_tests.py @@ -5,8 +5,8 @@ to ensure each constraint is properly enforced. """ -from typing import Dict, Any, List from dataclasses import dataclass +from typing import Any, Dict, List from ..case_validator import CaseValidator diff --git a/toolchain/mfc/params_tests/runner.py b/toolchain/mfc/params_tests/runner.py index cc980cfbcc..e4ef876ccd 100644 --- a/toolchain/mfc/params_tests/runner.py +++ b/toolchain/mfc/params_tests/runner.py @@ -3,30 +3,15 @@ Main entry point for building and verifying the parameter validation test suite. """ -# pylint: disable=import-outside-toplevel -import sys -import json import argparse +import json +import sys from pathlib import Path -from .inventory import ( - export_parameter_inventory, - save_inventory, - print_inventory_summary -) -from .snapshot import ( - capture_all_examples, - save_snapshots, - load_snapshots, - compare_snapshots, - print_comparison_report -) -from .coverage import ( - generate_coverage_report, - print_coverage_report, - save_coverage_report -) +from .coverage import generate_coverage_report, print_coverage_report, save_coverage_report +from .inventory import export_parameter_inventory, print_inventory_summary, save_inventory +from .snapshot import capture_all_examples, compare_snapshots, load_snapshots, print_comparison_report, save_snapshots def get_data_dir() -> Path: @@ -180,7 +165,7 @@ def show_summary(): inventory = json.load(f) print("\nParameter Inventory:") print(f" Total parameters: {inventory['metadata']['total_parameters']}") - print(f" By stage:") + print(" By stage:") print(f" Common: {inventory['metadata']['common_count']}") print(f" Pre-process: {inventory['metadata']['pre_process_count']}") print(f" Simulation: {inventory['metadata']['simulation_count']}") diff --git a/toolchain/mfc/params_tests/snapshot.py b/toolchain/mfc/params_tests/snapshot.py index c5c54f3112..438d9662a6 100644 --- a/toolchain/mfc/params_tests/snapshot.py +++ b/toolchain/mfc/params_tests/snapshot.py @@ -5,12 +5,12 @@ This allows us to verify that refactoring doesn't change validation behavior. """ -import json import hashlib +import json import subprocess +from dataclasses import asdict, dataclass from pathlib import Path -from typing import Dict, Any, List, Optional -from dataclasses import dataclass, asdict +from typing import Any, Dict, List, Optional from ..case_validator import CaseValidator diff --git a/toolchain/mfc/params_tests/test_definitions.py b/toolchain/mfc/params_tests/test_definitions.py index 20e0774526..c590e1e83c 100644 --- a/toolchain/mfc/params_tests/test_definitions.py +++ b/toolchain/mfc/params_tests/test_definitions.py @@ -5,15 +5,16 @@ """ import unittest + from ..params import REGISTRY -from ..params.schema import ParamType from ..params.definitions import ( + CASE_OPT_PARAMS, CONSTRAINTS, DEPENDENCIES, - CASE_OPT_PARAMS, _validate_constraint, _validate_dependency, ) +from ..params.schema import ParamType class TestParameterDefinitions(unittest.TestCase): diff --git a/toolchain/mfc/params_tests/test_integration.py b/toolchain/mfc/params_tests/test_integration.py index 8a921310ed..45006647f4 100644 --- a/toolchain/mfc/params_tests/test_integration.py +++ b/toolchain/mfc/params_tests/test_integration.py @@ -4,9 +4,9 @@ Tests that the parameter registry integrates correctly with case_dicts.py and provides correct JSON schema generation. """ -# pylint: disable=import-outside-toplevel import unittest + from ..params import REGISTRY from ..params.schema import ParamType diff --git a/toolchain/mfc/params_tests/test_registry.py b/toolchain/mfc/params_tests/test_registry.py index f6dd34bada..5b0f7b1335 100644 --- a/toolchain/mfc/params_tests/test_registry.py +++ b/toolchain/mfc/params_tests/test_registry.py @@ -3,9 +3,9 @@ Tests registry functionality, freezing, and tag queries. """ -# pylint: disable=import-outside-toplevel import unittest + from ..params.registry import ParamRegistry, RegistryFrozenError from ..params.schema import ParamDef, ParamType diff --git a/toolchain/mfc/params_tests/test_validate.py b/toolchain/mfc/params_tests/test_validate.py index 0f2cb69d19..061470bf24 100644 --- a/toolchain/mfc/params_tests/test_validate.py +++ b/toolchain/mfc/params_tests/test_validate.py @@ -5,14 +5,15 @@ """ import unittest + +from ..params.suggest import RAPIDFUZZ_AVAILABLE from ..params.validate import ( - validate_constraints, check_dependencies, check_unknown_params, - validate_case, format_validation_results, + validate_case, + validate_constraints, ) -from ..params.suggest import RAPIDFUZZ_AVAILABLE class TestValidateConstraints(unittest.TestCase): diff --git a/toolchain/mfc/printer.py b/toolchain/mfc/printer.py index e74d8ec01c..26822425bd 100644 --- a/toolchain/mfc/printer.py +++ b/toolchain/mfc/printer.py @@ -1,6 +1,7 @@ import typing -import rich, rich.console +import rich +import rich.console class MFCPrinter: diff --git a/toolchain/mfc/run/case_dicts.py b/toolchain/mfc/run/case_dicts.py index 31157ea8b4..5e7006128f 100644 --- a/toolchain/mfc/run/case_dicts.py +++ b/toolchain/mfc/run/case_dicts.py @@ -12,38 +12,44 @@ get_validator(): Returns compiled JSON schema validator get_input_dict_keys(): Get parameter keys for a target """ -# pylint: disable=import-outside-toplevel import re + from ..state import ARG + def _load_all_params(): """Load all parameters as {name: ParamType} dict.""" from ..params import REGISTRY + return {name: param.param_type for name, param in REGISTRY.all_params.items()} def _load_case_optimization_params(): """Get params that can be hard-coded for GPU optimization.""" from ..params import REGISTRY + return [name for name, param in REGISTRY.all_params.items() if param.case_optimization] def _build_schema(): """Build JSON schema from registry.""" from ..params import REGISTRY + return REGISTRY.get_json_schema() def _get_validator_func(): """Get the cached validator from registry.""" from ..params import REGISTRY + return REGISTRY.get_validator() def _get_target_params(): """Get valid params for each target by parsing Fortran namelists.""" from ..params.namelist_parser import get_target_params + return get_target_params() @@ -79,7 +85,7 @@ def _is_param_valid_for_target(param_name: str, target_name: str) -> bool: # e.g., "patch_icpp(1)%geometry" -> "patch_icpp" # e.g., "fluid_pp(2)%gamma" -> "fluid_pp" # e.g., "acoustic(1)%loc(1)" -> "acoustic" - match = re.match(r'^([a-zA-Z_][a-zA-Z0-9_]*)', param_name) + match = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)", param_name) if match: base_name = match.group(1) return base_name in target_params diff --git a/toolchain/mfc/run/input.py b/toolchain/mfc/run/input.py index c594ad17da..ee1208bf6a 100644 --- a/toolchain/mfc/run/input.py +++ b/toolchain/mfc/run/input.py @@ -1,27 +1,32 @@ -import os, json, glob, typing, dataclasses +import dataclasses +import glob +import json +import os +import typing + +from .. import case_validator, common +from ..case import Case # Note: pyrometheus and cantera are imported lazily in the methods that need them # to avoid slow startup times for commands that don't use chemistry features # Note: build is imported lazily to avoid circular import with build.py - from ..printer import cons -from .. import common -from ..state import ARGS, ARG, gpuConfigOptions -from ..case import Case -from .. import case_validator +from ..state import ARG, ARGS, gpuConfigOptions + @dataclasses.dataclass(init=False) class MFCInputFile(Case): filename: str - dirpath: str + dirpath: str def __init__(self, filename: str, dirpath: str, params: dict) -> None: super().__init__(params) self.filename = filename - self.dirpath = dirpath + self.dirpath = dirpath def generate_inp(self, target) -> None: - from .. import build # pylint: disable=import-outside-toplevel + from .. import build + target = build.get_target(target) # Save .inp input file @@ -38,9 +43,9 @@ def __save_fpp(self, target, contents: str) -> None: def get_cantera_solution(self): # Lazy import to avoid slow startup for commands that don't need chemistry - import cantera as ct # pylint: disable=import-outside-toplevel + import cantera as ct - if self.params.get("chemistry", 'F') == 'T': + if self.params.get("chemistry", "F") == "T": cantera_file = self.params["cantera_file"] candidates = [ @@ -64,12 +69,12 @@ def get_cantera_solution(self): def generate_fpp(self, target) -> None: # Lazy import to avoid slow startup for commands that don't need chemistry - import pyrometheus as pyro # pylint: disable=import-outside-toplevel + import pyrometheus as pyro if target.isDependency: return - cons.print(f"Generating [magenta]case.fpp[/magenta].") + cons.print("Generating [magenta]case.fpp[/magenta].") cons.indent() # Case FPP file @@ -80,34 +85,27 @@ def generate_fpp(self, target) -> None: common.create_directory(modules_dir) # Determine the real type based on the single precision flag - real_type = 'real(sp)' if (ARG('single') or ARG('mixed')) else 'real(dp)' + real_type = "real(sp)" if (ARG("single") or ARG("mixed")) else "real(dp)" if ARG("gpu") == gpuConfigOptions.MP.value: - directive_str = 'mp' + directive_str = "mp" elif ARG("gpu") == gpuConfigOptions.ACC.value: - directive_str = 'acc' + directive_str = "acc" else: directive_str = None # Write the generated Fortran code to the m_thermochem.f90 file with the chosen precision sol = self.get_cantera_solution() - thermochem_code = pyro.FortranCodeGenerator().generate( - "m_thermochem", - sol, - pyro.CodeGenerationOptions(scalar_type = real_type, directive_offload = directive_str) - ) + thermochem_code = pyro.FortranCodeGenerator().generate("m_thermochem", sol, pyro.CodeGenerationOptions(scalar_type=real_type, directive_offload=directive_str)) # CCE 19.0.0 workaround: pyrometheus generates !DIR$ INLINEALWAYS for Cray+ACC # but omits !$acc routine seq, so thermochem routines are not registered as # OpenACC device routines. Replace with plain !$acc routine seq (no INLINEALWAYS). # This patch can be removed once pyrometheus upstream correctly emits !$acc routine seq # for Cray+OpenACC (the broken macro originates in pyrometheus's code generator). - if directive_str == 'acc': - old_macro = ( - "#ifdef _CRAYFTN\n#define GPU_ROUTINE(name) !DIR$ INLINEALWAYS name\n" - "#else\n#define GPU_ROUTINE(name) !$acc routine seq\n#endif" - ) + if directive_str == "acc": + old_macro = "#ifdef _CRAYFTN\n#define GPU_ROUTINE(name) !DIR$ INLINEALWAYS name\n#else\n#define GPU_ROUTINE(name) !$acc routine seq\n#endif" new_macro = "#define GPU_ROUTINE(name) !$acc routine seq" patched = thermochem_code.replace(old_macro, new_macro) if patched == thermochem_code: @@ -115,28 +113,24 @@ def generate_fpp(self, target) -> None: pass # pyrometheus already emits the correct form; no patch needed else: raise common.MFCException( - "CCE 19.0.0 workaround: pyrometheus output format changed — " - "Cray+ACC GPU_ROUTINE macro patch did not apply. " - "Update the pattern in toolchain/mfc/run/input.py." + "CCE 19.0.0 workaround: pyrometheus output format changed — Cray+ACC GPU_ROUTINE macro patch did not apply. Update the pattern in toolchain/mfc/run/input.py." ) else: - cons.print("[yellow]Warning: Applied CCE 19.0.0 workaround patch to pyrometheus-generated " - "m_thermochem.f90 (replaced _CRAYFTN GPU_ROUTINE macro with !$acc routine seq). " - "Remove this patch once pyrometheus emits correct Cray+ACC directives upstream.[/yellow]") + cons.print( + "[yellow]Warning: Applied CCE 19.0.0 workaround patch to pyrometheus-generated " + "m_thermochem.f90 (replaced _CRAYFTN GPU_ROUTINE macro with !$acc routine seq). " + "Remove this patch once pyrometheus emits correct Cray+ACC directives upstream.[/yellow]" + ) thermochem_code = patched - common.file_write( - os.path.join(modules_dir, "m_thermochem.f90"), - thermochem_code, - True - ) + common.file_write(os.path.join(modules_dir, "m_thermochem.f90"), thermochem_code, True) cons.unindent() - def validate_constraints(self, target) -> None: """Validate case parameter constraints for a given target stage""" - from .. import build # pylint: disable=import-outside-toplevel + from .. import build + target_obj = build.get_target(target) stage = target_obj.name @@ -161,20 +155,18 @@ def generate(self, target) -> None: self.generate_fpp(target) def clean(self, _targets) -> None: - from .. import build # pylint: disable=import-outside-toplevel + from .. import build + targets = [build.get_target(target) for target in _targets] files = set() - dirs = set() + dirs = set() - files = set([ - "equations.dat", "run_time.inf", "time_data.dat", - "io_time_data.dat", "fort.1", "pre_time_data.dat" - ] + [f"{target.name}.inp" for target in targets]) + files = set(["equations.dat", "run_time.inf", "time_data.dat", "io_time_data.dat", "fort.1", "pre_time_data.dat"] + [f"{target.name}.inp" for target in targets]) if build.PRE_PROCESS in targets: files = files | set(glob.glob(os.path.join(self.dirpath, "D", "*.000000.dat"))) - dirs = dirs | set(glob.glob(os.path.join(self.dirpath, "p_all", "p*", "0"))) + dirs = dirs | set(glob.glob(os.path.join(self.dirpath, "p_all", "p*", "0"))) if build.SIMULATION in targets: restarts = set(glob.glob(os.path.join(self.dirpath, "restart_data", "*.dat"))) @@ -191,14 +183,12 @@ def clean(self, _targets) -> None: dirs.add("silo_hdf5") for relfile in files: - if not os.path.isfile(relfile): - relfile = os.path.join(self.dirpath, relfile) - common.delete_file(relfile) + filepath = relfile if os.path.isfile(relfile) else os.path.join(self.dirpath, relfile) + common.delete_file(filepath) for reldir in dirs: - if not os.path.isdir(reldir): - reldir = os.path.join(self.dirpath, reldir) - common.delete_directory(reldir) + dirpath = reldir if os.path.isdir(reldir) else os.path.join(self.dirpath, reldir) + common.delete_directory(dirpath) # Load the input file @@ -216,7 +206,7 @@ def load(filepath: str = None, args: typing.List[str] = None, empty_data: dict = if do_print: cons.print(f"Acquiring [bold magenta]{filename}[/bold magenta]...") - dirpath: str = os.path.abspath(os.path.dirname(filename)) + dirpath: str = os.path.abspath(os.path.dirname(filename)) dictionary: dict = {} if not os.path.exists(filename): @@ -230,8 +220,9 @@ def load(filepath: str = None, args: typing.List[str] = None, empty_data: dict = elif filename.endswith(".json"): json_str = common.file_read(filename) elif filename.endswith((".yaml", ".yml")): - import yaml # pylint: disable=import-outside-toplevel - with open(filename, 'r') as f: + import yaml + + with open(filename, "r") as f: dictionary = yaml.safe_load(f) json_str = json.dumps(dictionary) else: diff --git a/toolchain/mfc/run/queues.py b/toolchain/mfc/run/queues.py index 920c47f8d0..ef40f8a89e 100644 --- a/toolchain/mfc/run/queues.py +++ b/toolchain/mfc/run/queues.py @@ -1,6 +1,9 @@ -import os, typing, dataclasses +import dataclasses +import os +import typing + +from mfc import common -from mfc import common from ..state import ARG @@ -26,7 +29,7 @@ def is_active(self) -> bool: return True def gen_submit_cmd(self, filepath: str) -> typing.List[str]: - if os.name == 'nt': + if os.name == "nt": return [filepath] return ["/bin/bash", filepath] @@ -78,7 +81,8 @@ def gen_submit_cmd(self, filepath: str) -> None: return cmd + [filepath] -BATCH_SYSTEMS = [ LSFSystem(), SLURMSystem(), PBSSystem() ] +BATCH_SYSTEMS = [LSFSystem(), SLURMSystem(), PBSSystem()] + def get_system() -> QueueSystem: if ARG("engine") == "interactive": diff --git a/toolchain/mfc/run/run.py b/toolchain/mfc/run/run.py index 95747c3500..8157f0090d 100644 --- a/toolchain/mfc/run/run.py +++ b/toolchain/mfc/run/run.py @@ -1,18 +1,19 @@ -import re, os, sys, typing, dataclasses, shlex - +import dataclasses +import os +import re +import shlex +import sys +import typing from glob import glob -from mako.lookup import TemplateLookup +from mako.lookup import TemplateLookup from mako.template import Template -from ..build import get_targets, build, REQUIRED_TARGETS, SIMULATION +from ..build import REQUIRED_TARGETS, SIMULATION, build, get_targets +from ..common import MFC_ROOT_DIR, MFC_TEMPLATE_DIR, MFCException, does_command_exist, file_dump_yaml, file_read, file_write, format_list_to_string, isspace, system from ..printer import cons -from ..state import ARG, ARGS, CFG, gpuConfigOptions -from ..common import MFCException, isspace, file_read, does_command_exist -from ..common import MFC_TEMPLATE_DIR, file_write, system, MFC_ROOT_DIR -from ..common import format_list_to_string, file_dump_yaml - -from . import queues, input +from ..state import ARG, ARGS, CFG, gpuConfigOptions +from . import input, queues def __validate_job_options() -> None: @@ -28,7 +29,7 @@ def __validate_job_options() -> None: if not isspace(ARG("email")): # https://stackoverflow.com/questions/8022530/how-to-check-for-valid-email-address if not re.match(r"\"?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)\"?", ARG("email")): - raise MFCException(f'RUN: {ARG("email")} is not a valid e-mail address.') + raise MFCException(f"RUN: {ARG('email')} is not a valid e-mail address.") def __profiler_prepend() -> typing.List[str]: @@ -36,8 +37,7 @@ def __profiler_prepend() -> typing.List[str]: if not does_command_exist("ncu"): raise MFCException("Failed to locate [bold green]NVIDIA Nsight Compute[/bold green] (ncu).") - return ["ncu", "--nvtx", "--mode=launch-and-attach", - "--cache-control=none", "--clock-control=none"] + ARG("ncu") + return ["ncu", "--nvtx", "--mode=launch-and-attach", "--cache-control=none", "--clock-control=none"] + ARG("ncu") if ARG("nsys") is not None: if not does_command_exist("nsys"): @@ -49,7 +49,7 @@ def __profiler_prepend() -> typing.List[str]: if not does_command_exist("rocprof-compute"): raise MFCException("Failed to locate [bold red]ROCM rocprof-compute[/bold red] (rocprof-compute).") - return ["rocprof-compute", "profile", "-n", ARG("name").replace('-', '_').replace('.', '_')] + ARG("rcu") + ["--"] + return ["rocprof-compute", "profile", "-n", ARG("name").replace("-", "_").replace(".", "_")] + ARG("rcu") + ["--"] if ARG("rsys") is not None: if not does_command_exist("rocprof"): @@ -61,23 +61,17 @@ def __profiler_prepend() -> typing.List[str]: def get_baked_templates() -> dict: - return { - os.path.splitext(os.path.basename(f))[0] : file_read(f) - for f in glob(os.path.join(MFC_TEMPLATE_DIR, "*.mako")) - } + return {os.path.splitext(os.path.basename(f))[0]: file_read(f) for f in glob(os.path.join(MFC_TEMPLATE_DIR, "*.mako"))} def __job_script_filepath() -> str: - return os.path.abspath(os.sep.join([ - os.path.dirname(ARG("input")), - f"{ARG('name')}.{'bat' if os.name == 'nt' else 'sh'}" - ])) + return os.path.abspath(os.sep.join([os.path.dirname(ARG("input")), f"{ARG('name')}.{'bat' if os.name == 'nt' else 'sh'}"])) def __get_template() -> Template: computer = ARG("computer") - lookup = TemplateLookup(directories=[MFC_TEMPLATE_DIR, os.path.join(MFC_TEMPLATE_DIR, "include")]) - baked = get_baked_templates() + lookup = TemplateLookup(directories=[MFC_TEMPLATE_DIR, os.path.join(MFC_TEMPLATE_DIR, "include")]) + baked = get_baked_templates() if (content := baked.get(computer)) is not None: cons.print(f"Using baked-in template for [magenta]{computer}[/magenta].") @@ -92,29 +86,24 @@ def __get_template() -> Template: def __generate_job_script(targets, case: input.MFCInputFile): env = {} - if ARG('gpus') is not None: - gpu_ids = ','.join([str(_) for _ in ARG('gpus')]) - env.update({ - 'CUDA_VISIBLE_DEVICES': gpu_ids, - 'HIP_VISIBLE_DEVICES': gpu_ids - }) + if ARG("gpus") is not None: + gpu_ids = ",".join([str(_) for _ in ARG("gpus")]) + env.update({"CUDA_VISIBLE_DEVICES": gpu_ids, "HIP_VISIBLE_DEVICES": gpu_ids}) # Compute GPU mode booleans for templates - gpu_mode = ARG('gpu') + gpu_mode = ARG("gpu") # Validate gpu_mode is one of the expected values valid_gpu_modes = {e.value for e in gpuConfigOptions} if gpu_mode not in valid_gpu_modes: - raise MFCException( - f"Invalid GPU mode '{gpu_mode}'. Must be one of: {', '.join(sorted(valid_gpu_modes))}" - ) + raise MFCException(f"Invalid GPU mode '{gpu_mode}'. Must be one of: {', '.join(sorted(valid_gpu_modes))}") gpu_enabled = gpu_mode != gpuConfigOptions.NONE.value gpu_acc = gpu_mode == gpuConfigOptions.ACC.value gpu_mp = gpu_mode == gpuConfigOptions.MP.value content = __get_template().render( - **{**ARGS(), 'targets': targets}, + **{**ARGS(), "targets": targets}, ARG=ARG, env=env, case=case, @@ -124,7 +113,7 @@ def __generate_job_script(targets, case: input.MFCInputFile): profiler=shlex.join(__profiler_prepend()), gpu_enabled=gpu_enabled, gpu_acc=gpu_acc, - gpu_mp=gpu_mp + gpu_mp=gpu_mp, ) file_write(__job_script_filepath(), content) @@ -144,7 +133,7 @@ def __execute_job_script(qsystem: queues.QueueSystem): # in the correct directory. cmd = qsystem.gen_submit_cmd(__job_script_filepath()) - verbosity = ARG('verbose') + verbosity = ARG("verbose") # At verbosity >= 1, show the command being executed if verbosity >= 1: @@ -159,13 +148,13 @@ def __execute_job_script(qsystem: queues.QueueSystem): raise MFCException(f"Submitting batch file for {qsystem.name} failed. It can be found here: {__job_script_filepath()}. Please check the file for errors.") -def run(targets = None, case = None): +def run(targets=None, case=None): targets = get_targets(list(REQUIRED_TARGETS) + (targets or ARG("targets"))) - case = case or input.load(ARG("input"), ARG("--")) + case = case or input.load(ARG("input"), ARG("--")) build(targets) - verbosity = ARG('verbose') + verbosity = ARG("verbose") cons.print("[bold]Run[/bold]") cons.indent() @@ -195,10 +184,7 @@ def run(targets = None, case = None): if not ARG("dry_run"): if ARG("output_summary") is not None: - file_dump_yaml(ARG("output_summary"), { - "invocation": sys.argv[1:], - "lock": dataclasses.asdict(CFG()) - }) + file_dump_yaml(ARG("output_summary"), {"invocation": sys.argv[1:], "lock": dataclasses.asdict(CFG())}) if verbosity >= 1: cons.print() diff --git a/toolchain/mfc/sched.py b/toolchain/mfc/sched.py index 7869468fff..f0618705a1 100644 --- a/toolchain/mfc/sched.py +++ b/toolchain/mfc/sched.py @@ -1,24 +1,30 @@ -import time, typing, threading, dataclasses -import rich, rich.progress +import dataclasses +import threading +import time import traceback +import typing + +import rich +import rich.progress from .printer import cons # Thresholds for long-running test notifications # Interactive mode: dimension-aware thresholds INTERACTIVE_THRESHOLDS = { - 1: 30.0, # 1D: 30 seconds - 2: 60.0, # 2D: 1 minute + 1: 30.0, # 1D: 30 seconds + 2: 60.0, # 2D: 1 minute 3: 120.0, # 3D: 2 minutes } # Headless mode: fixed time-based thresholds (regardless of dimensionality) HEADLESS_THRESHOLDS = ( - (2 * 60, "[italic yellow]Still running[/italic yellow] (>2min)"), - (10 * 60, "[italic yellow]Still running[/italic yellow] (>10min)"), - (30 * 60, "[bold red]Still running[/bold red] (>30min, may be hanging)"), + (2 * 60, "[italic yellow]Still running[/italic yellow] (>2min)"), + (10 * 60, "[italic yellow]Still running[/italic yellow] (>10min)"), + (30 * 60, "[bold red]Still running[/bold red] (>30min, may be hanging)"), ) + class WorkerThread(threading.Thread): def __init__(self, *args, **kwargs): self.exc = None @@ -39,46 +45,47 @@ def run(self): @dataclasses.dataclass -class WorkerThreadHolder: # pylint: disable=too-many-instance-attributes - thread: threading.Thread - ppn: int - load: float +class WorkerThreadHolder: + thread: threading.Thread + ppn: int + load: float devices: typing.Optional[typing.Set[int]] - task: typing.Optional['Task'] = None - start: float = 0.0 + task: typing.Optional["Task"] = None + start: float = 0.0 # Track which milestones we've already logged notified_interactive: bool = False # First notification in interactive mode (time varies by dimensionality) - notified_2m: bool = False # Headless mode: 2 minute milestone + notified_2m: bool = False # Headless mode: 2 minute milestone notified_10m: bool = False # Headless mode: 10 minute milestone notified_30m: bool = False # Headless mode: 30 minute milestone @dataclasses.dataclass class Task: - ppn: int + ppn: int func: typing.Callable args: typing.List[typing.Any] load: float -def sched(tasks: typing.List[Task], nThreads: int, devices: typing.Optional[typing.Set[int]] = None) -> None: # pylint: disable=too-many-locals,too-many-branches,too-many-statements + +def sched(tasks: typing.List[Task], nThreads: int, devices: typing.Optional[typing.Set[int]] = None) -> None: nAvailable: int = nThreads - threads: typing.List[WorkerThreadHolder] = [] + threads: typing.List[WorkerThreadHolder] = [] - sched.LOAD = { id: 0.0 for id in devices or [] } + sched.LOAD = {id: 0.0 for id in devices or []} def get_case_dimensionality(case: typing.Any) -> int: """ Determine if a test case is 1D, 2D, or 3D based on grid parameters. - + Grid parameters (m, n, p) represent the number of cells in x, y, z directions. Returns 3 if p != 0, 2 if n != 0, otherwise 1. Defaults to 1D if params unavailable. """ - if not hasattr(case, 'params'): + if not hasattr(case, "params"): return 1 # Default to 1D if we can't determine params = case.params - p = params.get('p', 0) - n = params.get('n', 0) + p = params.get("p", 0) + n = params.get("n", 0) if p != 0: return 3 # 3D @@ -89,17 +96,13 @@ def get_case_dimensionality(case: typing.Any) -> int: def get_threshold_for_case(case: typing.Any) -> float: """ Get the dimension-aware time threshold (in seconds) for interactive mode notifications. - + Returns 30s for 1D, 60s for 2D, 120s for 3D tests. """ dim = get_case_dimensionality(case) return INTERACTIVE_THRESHOLDS.get(dim, INTERACTIVE_THRESHOLDS[1]) - def notify_long_running_threads( # pylint: disable=too-many-branches - progress: rich.progress.Progress, - running_tracker: typing.Optional[rich.progress.TaskID], - interactive: bool - ) -> None: + def notify_long_running_threads(progress: rich.progress.Progress, running_tracker: typing.Optional[rich.progress.TaskID], interactive: bool) -> None: """ Monitor and notify about long-running tests. @@ -116,7 +119,7 @@ def notify_long_running_threads( # pylint: disable=too-many-branches elapsed = now - holder.start case = holder.task.args[0] if holder.task and holder.task.args else None - case_uuid = case.get_uuid() if hasattr(case, "get_uuid") else "unknown" + case_uuid = case.get_uuid() if hasattr(case, "get_uuid") else "unknown" case_trace = getattr(case, "trace", "") # --- interactive: dimension-aware thresholds --- @@ -130,37 +133,25 @@ def notify_long_running_threads( # pylint: disable=too-many-branches if not holder.notified_interactive: dim = get_case_dimensionality(case) dim_label = f"{dim}D" - time_label = f"{int(threshold)}s" if threshold < 60 else f"{threshold/60:.0f}min" - cons.print( - f" [italic yellow]Still running[/italic yellow] ({dim_label}, >{time_label}) " - f"[bold magenta]{case_uuid}[/bold magenta] {case_trace}" - ) + time_label = f"{int(threshold)}s" if threshold < 60 else f"{threshold / 60:.0f}min" + cons.print(f" [italic yellow]Still running[/italic yellow] ({dim_label}, >{time_label}) [bold magenta]{case_uuid}[/bold magenta] {case_trace}") holder.notified_interactive = True # --- headless: milestone notifications at 2, 10, 30 minutes --- else: # 2 minutes if (not holder.notified_2m) and elapsed >= 2 * 60: - cons.print( - f" {HEADLESS_THRESHOLDS[0][1]} " - f"[bold magenta]{case_uuid}[/bold magenta] {case_trace}" - ) + cons.print(f" {HEADLESS_THRESHOLDS[0][1]} [bold magenta]{case_uuid}[/bold magenta] {case_trace}") holder.notified_2m = True # 10 minutes if (not holder.notified_10m) and elapsed >= 10 * 60: - cons.print( - f" {HEADLESS_THRESHOLDS[1][1]} " - f"[bold magenta]{case_uuid}[/bold magenta] {case_trace}" - ) + cons.print(f" {HEADLESS_THRESHOLDS[1][1]} [bold magenta]{case_uuid}[/bold magenta] {case_trace}") holder.notified_10m = True # 30 minutes if (not holder.notified_30m) and elapsed >= 30 * 60: - cons.print( - f" {HEADLESS_THRESHOLDS[2][1]} " - f"[bold magenta]{case_uuid}[/bold magenta] {case_trace}" - ) + cons.print(f" {HEADLESS_THRESHOLDS[2][1]} [bold magenta]{case_uuid}[/bold magenta] {case_trace}") holder.notified_30m = True # update the interactive "Running" row @@ -188,13 +179,11 @@ def join_first_dead_thread(progress, complete_tracker, interactive: bool) -> Non # Double-check that thread actually finished joining if threadHolder.thread.is_alive(): # Thread didn't finish within timeout - this is a serious issue - raise RuntimeError(f"Thread {threadID} failed to join within 30 seconds timeout. " - f"Thread may be hung or in an inconsistent state.") + raise RuntimeError(f"Thread {threadID} failed to join within 30 seconds timeout. Thread may be hung or in an inconsistent state.") except Exception as join_exc: # Handle join-specific exceptions with more context - raise RuntimeError(f"Failed to join thread {threadID}: {join_exc}. " - f"This may indicate a system threading issue or hung test case.") from join_exc + raise RuntimeError(f"Failed to join thread {threadID}: {join_exc}. This may indicate a system threading issue or hung test case.") from join_exc # Check for and propagate any exceptions that occurred in the worker thread if threadHolder.thread.exc is not None: @@ -208,12 +197,9 @@ def join_first_dead_thread(progress, complete_tracker, interactive: bool) -> Non if interactive and threadHolder.notified_interactive: elapsed = time.time() - threadHolder.start case = threadHolder.task.args[0] if threadHolder.task and threadHolder.task.args else None - case_uuid = case.get_uuid() if hasattr(case, "get_uuid") else "unknown" + case_uuid = case.get_uuid() if hasattr(case, "get_uuid") else "unknown" case_trace = getattr(case, "trace", "") - cons.print( - f" [italic green]Completed[/italic green] (after {elapsed:.1f}s) " - f"[bold magenta]{case_uuid}[/bold magenta] {case_trace}" - ) + cons.print(f" [italic green]Completed[/italic green] (after {elapsed:.1f}s) [bold magenta]{case_uuid}[/bold magenta] {case_trace}") nAvailable += threadHolder.ppn for device in threadHolder.devices or set(): @@ -226,10 +212,10 @@ def join_first_dead_thread(progress, complete_tracker, interactive: bool) -> Non break with rich.progress.Progress(console=cons.raw, transient=True) as progress: - interactive = cons.raw.is_terminal - queue_tracker = progress.add_task("Queued ", total=len(tasks)) + interactive = cons.raw.is_terminal + queue_tracker = progress.add_task("Queued ", total=len(tasks)) complete_tracker = progress.add_task("Completed ", total=len(tasks)) - running_tracker = progress.add_task("Running ", total=None) if interactive else None + running_tracker = progress.add_task("Running ", total=None) if interactive else None # Queue Tests for task in tasks: @@ -288,4 +274,5 @@ def join_first_dead_thread(progress, complete_tracker, interactive: bool) -> Non # Do not overwhelm this core with this loop time.sleep(0.05) + sched.LOAD = {} diff --git a/toolchain/mfc/state.py b/toolchain/mfc/state.py index 826a48cb4f..37aacc88e1 100644 --- a/toolchain/mfc/state.py +++ b/toolchain/mfc/state.py @@ -1,28 +1,33 @@ -import typing, dataclasses +import dataclasses +import typing from enum import Enum, unique + @unique class gpuConfigOptions(Enum): - NONE = 'no' - ACC = 'acc' - MP = 'mp' + NONE = "no" + ACC = "acc" + MP = "mp" + @dataclasses.dataclass class MFCConfig: - # pylint: disable=too-many-instance-attributes - mpi: bool = True - gpu: str = gpuConfigOptions.NONE.value - debug: bool = False - gcov: bool = False - unified: bool = False - single: bool = False - mixed: bool = False + mpi: bool = True + gpu: str = gpuConfigOptions.NONE.value + debug: bool = False + gcov: bool = False + unified: bool = False + single: bool = False + mixed: bool = False fastmath: bool = False + def __hash__(self): + return hash(tuple(getattr(self, f.name) for f in dataclasses.fields(self))) + @staticmethod def from_dict(d: dict): - """ Create a MFCConfig object from a dictionary with the same keys - as the fields of MFCConfig """ + """Create a MFCConfig object from a dictionary with the same keys + as the fields of MFCConfig""" r = MFCConfig() for field in dataclasses.fields(MFCConfig): @@ -34,29 +39,31 @@ def items(self) -> typing.Iterable[typing.Tuple[str, typing.Any]]: return dataclasses.asdict(self).items() def make_options(self) -> typing.List[str]: - """ Returns a list of options that could be passed to mfc.sh again. - Example: --no-debug --mpi --no-gpu --no-gcov --no-unified""" + """Returns a list of options that could be passed to mfc.sh again. + Example: --no-debug --mpi --no-gpu --no-gcov --no-unified""" options = [] for k, v in self.items(): - if k == 'gpu': + if k == "gpu": options.append(f"--{v}-{k}") else: options.append(f"--{'no-' if not v else ''}{k}") return options def make_slug(self) -> str: - """ Sort the items by key, then join them with underscores. This uniquely - identifies the configuration. Example: no-debug_no-gpu_no_mpi_no-gcov """ + """Sort the items by key, then join them with underscores. This uniquely + identifies the configuration. Example: no-debug_no-gpu_no_mpi_no-gcov""" options = [] for k, v in sorted(self.items(), key=lambda x: x[0]): - if k == 'gpu': + if k == "gpu": options.append(f"--{v}-{k}") else: options.append(f"--{'no-' if not v else ''}{k}") - return '_'.join(options) + return "_".join(options) def __eq__(self, other) -> bool: - """ Check if two MFCConfig objects are equal, field by field. """ + """Check if two MFCConfig objects are equal, field by field.""" + if not isinstance(other, MFCConfig): + return NotImplemented for field in dataclasses.fields(self): if getattr(self, field.name) != getattr(other, field.name): return False @@ -64,9 +71,9 @@ def __eq__(self, other) -> bool: return True def __str__(self) -> str: - """ Returns a string like "mpi=No & gpu=No & debug=No & gcov=No & unified=No" """ + """Returns a string like "mpi=No & gpu=No & debug=No & gcov=No & unified=No" """ strings = [] - for k,v in self.items(): + for k, v in self.items(): if isinstance(v, bool): strings.append(f"{k}={'Yes' if v else 'No'}") elif isinstance(v, str): @@ -76,15 +83,15 @@ def __str__(self) -> str: else: strings.append(f"{k}={v.__str__()}") - return ' & '.join(strings) + return " & ".join(strings) gCFG: MFCConfig = MFCConfig() -gARG: dict = {"rdma_mpi": False} +gARG: dict = {"rdma_mpi": False} -def ARG(arg: str, dflt = None) -> typing.Any: - # pylint: disable=global-variable-not-assigned - global gARG + +def ARG(arg: str, dflt=None) -> typing.Any: + global gARG # noqa: PLW0603 if arg in gARG: return gARG[arg] if dflt is not None: @@ -92,12 +99,12 @@ def ARG(arg: str, dflt = None) -> typing.Any: raise KeyError(f"{arg} is not an argument.") + def ARGS() -> dict: - # pylint: disable=global-variable-not-assigned - global gARG + global gARG # noqa: PLW0603 return gARG + def CFG() -> MFCConfig: - # pylint: disable=global-variable-not-assigned - global gCFG + global gCFG # noqa: PLW0603 return gCFG diff --git a/toolchain/mfc/test/case.py b/toolchain/mfc/test/case.py index c5ffdd301a..6a46c50387 100644 --- a/toolchain/mfc/test/case.py +++ b/toolchain/mfc/test/case.py @@ -1,117 +1,117 @@ -import os, glob, hashlib, binascii, subprocess, itertools, dataclasses, shutil - -from typing import List, Set, Union, Callable, Optional - -from .. import case, common -from ..state import ARG -from ..run import input +import binascii +import dataclasses +import glob +import hashlib +import itertools +import os +import shutil +import subprocess +from typing import Callable, List, Optional, Set, Union + +from .. import case, common from ..build import MFCTarget, get_target +from ..run import input +from ..state import ARG Tend = 0.25 -Nt = 50 +Nt = 50 mydt = 0.0005 BASE_CFG = { - 'run_time_info' : 'T', - 'm' : 0, - 'n' : 0, - 'p' : 0, - 'dt' : mydt, - 't_step_start' : 0, - 't_step_stop' : int(Nt), - 't_step_save' : int(Nt), - 'num_patches' : 3, - 'model_eqns' : 2, - 'alt_soundspeed' : 'F', - 'num_fluids' : 1, - 'mpp_lim' : 'F', - 'mixture_err' : 'F', - 'time_stepper' : 3, - 'recon_type' : 1, - 'weno_order' : 5, - 'weno_eps' : 1.E-16, - 'mapped_weno' : 'F', - 'null_weights' : 'F', - 'mp_weno' : 'F', - 'riemann_solver' : 2, - 'wave_speeds' : 1, - 'avg_state' : 2, - 'format' : 1, - 'precision' : 2, - - 'patch_icpp(1)%pres' : 1.0, - 'patch_icpp(1)%alpha_rho(1)' : 1.E+00, - 'patch_icpp(1)%alpha(1)' : 1., - - 'patch_icpp(2)%pres' : 0.5, - 'patch_icpp(2)%alpha_rho(1)' : 0.5, - 'patch_icpp(2)%alpha(1)' : 1., - - 'patch_icpp(3)%pres' : 0.1, - 'patch_icpp(3)%alpha_rho(1)' : 0.125, - 'patch_icpp(3)%alpha(1)' : 1., - - 'fluid_pp(1)%gamma' : 1.E+00/(1.4-1.E+00), - 'fluid_pp(1)%pi_inf' : 0.0, - 'fluid_pp(1)%cv' : 0.0, - 'fluid_pp(1)%qv' : 0.0, - 'fluid_pp(1)%qvp' : 0.0, - 'bubbles_euler' : 'F', - 'pref' : 101325.0, - 'rhoref' : 1000.0, - 'bubble_model' : 3, - 'polytropic' : 'T', - 'polydisperse' : 'F', - 'thermal' : 3, - - 'patch_icpp(1)%r0' : 1, - 'patch_icpp(1)%v0' : 0, - 'patch_icpp(2)%r0' : 1, - 'patch_icpp(2)%v0' : 0, - 'patch_icpp(3)%r0' : 1, - 'patch_icpp(3)%v0' : 0, - - 'qbmm' : 'F', - 'dist_type' : 2, - 'poly_sigma' : 0.3, - 'sigR' : 0.1, - 'sigV' : 0.1, - 'rhoRV' : 0.0, - - 'acoustic_source' : 'F', - 'num_source' : 1, - 'acoustic(1)%loc(1)' : 0.5, - 'acoustic(1)%mag' : 0.2, - 'acoustic(1)%length' : 0.25, - 'acoustic(1)%dir' : 1.0, - 'acoustic(1)%npulse' : 1, - 'acoustic(1)%pulse' : 1, - 'rdma_mpi' : 'F', - - 'bubbles_lagrange' : 'F', - 'lag_params%nBubs_glb' : 1, - 'lag_params%solver_approach' : 0, - 'lag_params%cluster_type' : 2, - 'lag_params%pressure_corrector' : 'F', - 'lag_params%smooth_type' : 1, - 'lag_params%epsilonb' : 1.0, - 'lag_params%heatTransfer_model' : 'F', - 'lag_params%massTransfer_model' : 'F', - 'lag_params%valmaxvoid' : 0.9, + "run_time_info": "T", + "m": 0, + "n": 0, + "p": 0, + "dt": mydt, + "t_step_start": 0, + "t_step_stop": int(Nt), + "t_step_save": int(Nt), + "num_patches": 3, + "model_eqns": 2, + "alt_soundspeed": "F", + "num_fluids": 1, + "mpp_lim": "F", + "mixture_err": "F", + "time_stepper": 3, + "recon_type": 1, + "weno_order": 5, + "weno_eps": 1.0e-16, + "mapped_weno": "F", + "null_weights": "F", + "mp_weno": "F", + "riemann_solver": 2, + "wave_speeds": 1, + "avg_state": 2, + "format": 1, + "precision": 2, + "patch_icpp(1)%pres": 1.0, + "patch_icpp(1)%alpha_rho(1)": 1.0e00, + "patch_icpp(1)%alpha(1)": 1.0, + "patch_icpp(2)%pres": 0.5, + "patch_icpp(2)%alpha_rho(1)": 0.5, + "patch_icpp(2)%alpha(1)": 1.0, + "patch_icpp(3)%pres": 0.1, + "patch_icpp(3)%alpha_rho(1)": 0.125, + "patch_icpp(3)%alpha(1)": 1.0, + "fluid_pp(1)%gamma": 1.0e00 / (1.4 - 1.0e00), + "fluid_pp(1)%pi_inf": 0.0, + "fluid_pp(1)%cv": 0.0, + "fluid_pp(1)%qv": 0.0, + "fluid_pp(1)%qvp": 0.0, + "bubbles_euler": "F", + "pref": 101325.0, + "rhoref": 1000.0, + "bubble_model": 3, + "polytropic": "T", + "polydisperse": "F", + "thermal": 3, + "patch_icpp(1)%r0": 1, + "patch_icpp(1)%v0": 0, + "patch_icpp(2)%r0": 1, + "patch_icpp(2)%v0": 0, + "patch_icpp(3)%r0": 1, + "patch_icpp(3)%v0": 0, + "qbmm": "F", + "dist_type": 2, + "poly_sigma": 0.3, + "sigR": 0.1, + "sigV": 0.1, + "rhoRV": 0.0, + "acoustic_source": "F", + "num_source": 1, + "acoustic(1)%loc(1)": 0.5, + "acoustic(1)%mag": 0.2, + "acoustic(1)%length": 0.25, + "acoustic(1)%dir": 1.0, + "acoustic(1)%npulse": 1, + "acoustic(1)%pulse": 1, + "rdma_mpi": "F", + "bubbles_lagrange": "F", + "lag_params%nBubs_glb": 1, + "lag_params%solver_approach": 0, + "lag_params%cluster_type": 2, + "lag_params%pressure_corrector": "F", + "lag_params%smooth_type": 1, + "lag_params%epsilonb": 1.0, + "lag_params%heatTransfer_model": "F", + "lag_params%massTransfer_model": "F", + "lag_params%valmaxvoid": 0.9, } + def trace_to_uuid(trace: str) -> str: return hex(binascii.crc32(hashlib.sha1(str(trace).encode()).digest())).upper()[2:].zfill(8) + @dataclasses.dataclass(init=False) class TestCase(case.Case): - ppn: int - trace: str + ppn: int + trace: str override_tol: Optional[float] = None def __init__(self, trace: str, mods: dict, ppn: int = None, override_tol: float = None) -> None: - self.trace = trace - self.ppn = ppn or 1 + self.trace = trace + self.ppn = ppn or 1 self.override_tol = override_tol super().__init__({**BASE_CFG.copy(), **mods}) @@ -121,22 +121,19 @@ def run(self, targets: List[Union[str, MFCTarget]], gpus: Set[int]) -> subproces else: gpus_select = [] - filepath = f'{self.get_dirpath()}/case.py' - tasks = ["-n", str(self.ppn)] - jobs = ["-j", str(ARG("jobs"))] if ARG("case_optimization") else [] - case_optimization = ["--case-optimization"] if ARG("case_optimization") else [] + filepath = f"{self.get_dirpath()}/case.py" + tasks = ["-n", str(self.ppn)] + jobs = ["-j", str(ARG("jobs"))] if ARG("case_optimization") else [] + case_optimization = ["--case-optimization"] if ARG("case_optimization") else [] - if self.params.get("bubbles_lagrange", 'F') == 'T': + if self.params.get("bubbles_lagrange", "F") == "T": input_bubbles_lagrange(self) - mfc_script = ".\\mfc.bat" if os.name == 'nt' else "./mfc.sh" + mfc_script = ".\\mfc.bat" if os.name == "nt" else "./mfc.sh" - target_names = [ get_target(t).name for t in targets ] + target_names = [get_target(t).name for t in targets] - command = [ - mfc_script, "run", filepath, "--no-build", *tasks, *case_optimization, - *jobs, "-t", *target_names, *gpus_select, *ARG("--") - ] + command = [mfc_script, "run", filepath, "--no-build", *tasks, *case_optimization, *jobs, "-t", *target_names, *gpus_select, *ARG("--")] return common.system(command, print_cmd=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -151,8 +148,8 @@ def get_dirpath(self): def get_filepath(self): filepath = os.path.join(self.get_dirpath(), "case.py") - if os.name == 'nt': - return filepath.replace('\\', '\\\\') + if os.name == "nt": + return filepath.replace("\\", "\\\\") return filepath def delete_output(self): @@ -169,7 +166,7 @@ def delete_output(self): common.delete_directory(os.path.join(dirpath, "p_all")) common.delete_directory(os.path.join(dirpath, "silo_hdf5")) common.delete_directory(os.path.join(dirpath, "restart_data")) - if self.params.get("bubbles_lagrange", 'F') == 'T': + if self.params.get("bubbles_lagrange", "F") == "T": common.delete_directory(os.path.join(dirpath, "input")) common.delete_directory(os.path.join(dirpath, "lag_bubbles_post_process")) @@ -181,7 +178,9 @@ def create_directory(self): common.create_directory(dirpath) - common.file_write(self.get_filepath(), f"""\ + common.file_write( + self.get_filepath(), + f"""\ #!/usr/bin/env python3 # # {self.get_filepath()}: @@ -225,18 +224,15 @@ def create_directory(self): mods['prim_vars_wrt'] = 'F' print(json.dumps({{**case, **mods}})) -""") +""", + ) def __str__(self) -> str: return f"tests/[bold magenta]{self.get_uuid()}[/bold magenta]: {self.trace}" def to_input_file(self) -> input.MFCInputFile: - return input.MFCInputFile( - os.path.basename(self.get_filepath()), - self.get_dirpath(), - self.get_parameters()) + return input.MFCInputFile(os.path.basename(self.get_filepath()), self.get_dirpath(), self.get_parameters()) - # pylint: disable=too-many-return-statements def compute_tolerance(self) -> float: if self.override_tol: return self.override_tol @@ -248,35 +244,36 @@ def compute_tolerance(self) -> float: tolerance = 1e-3 elif "Cylindrical" in self.trace.split(" -> "): tolerance = 1e-9 - elif self.params.get("hypoelasticity", 'F') == 'T': + elif self.params.get("hypoelasticity", "F") == "T": tolerance = 1e-7 - elif self.params.get("mixlayer_perturb", 'F') == 'T': + elif self.params.get("mixlayer_perturb", "F") == "T": tolerance = 1e-7 - elif any(self.params.get(key, 'F') == 'T' for key in ['relax', 'ib', 'qbmm', 'bubbles_euler', 'bubbles_lagrange']): + elif any(self.params.get(key, "F") == "T" for key in ["relax", "ib", "qbmm", "bubbles_euler", "bubbles_lagrange"]): tolerance = 1e-10 elif self.params.get("low_Mach") in [1, 2]: tolerance = 1e-10 - elif self.params.get("acoustic_source", 'F') == 'T': + elif self.params.get("acoustic_source", "F") == "T": if self.params.get("acoustic(1)%pulse") == 3: # Square wave return 1e-1 if single else 1e-5 tolerance = 3e-12 elif self.params.get("weno_order") == 7: tolerance = 1e-9 - elif self.params.get("mhd", 'F') == 'T': + elif self.params.get("mhd", "F") == "T": tolerance = 1e-8 elif "Axisymmetric" in self.trace.split(" -> "): tolerance = 1e-11 return 1e8 * tolerance if single else tolerance + @dataclasses.dataclass class TestCaseBuilder: - trace: str - mods: dict - path: str - args: List[str] - ppn: int - functor: Optional[Callable] + trace: str + mods: dict + path: str + args: List[str] + ppn: int + functor: Optional[Callable] override_tol: Optional[float] = None def get_uuid(self) -> str: @@ -291,10 +288,10 @@ def to_case(self) -> TestCase: if not isinstance(value, str): continue - for path in [value, os.path.join(os.path.dirname(self.path), value)]: - path = os.path.abspath(path) - if os.path.exists(path): - dictionary[key] = path + for candidate in [value, os.path.join(os.path.dirname(self.path), value)]: + abspath = os.path.abspath(candidate) + if os.path.exists(abspath): + dictionary[key] = abspath if self.mods: dictionary.update(self.mods) @@ -307,8 +304,8 @@ def to_case(self) -> TestCase: @dataclasses.dataclass class CaseGeneratorStack: - trace: list # list of strs - mods: list # list of dicts + trace: list # list of strs + mods: list # list of dicts def __init__(self) -> None: self.trace, self.mods = [], [] @@ -324,12 +321,10 @@ def pop(self) -> None: return (self.mods.pop(), self.trace.pop()) -# pylint: disable=too-many-arguments, too-many-positional-arguments def define_case_f(trace: str, path: str, args: List[str] = None, ppn: int = None, mods: dict = None, functor: Callable = None, override_tol: float = None) -> TestCaseBuilder: return TestCaseBuilder(trace, mods or {}, path, args or [], ppn or 1, functor, override_tol) -# pylint: disable=too-many-arguments, too-many-positional-arguments def define_case_d(stack: CaseGeneratorStack, newTrace: str, newMods: dict, ppn: int = None, functor: Callable = None, override_tol: float = None) -> TestCaseBuilder: mods: dict = {} @@ -346,29 +341,32 @@ def define_case_d(stack: CaseGeneratorStack, newTrace: str, newMods: dict, ppn: if not common.isspace(trace): traces.append(trace) - return TestCaseBuilder(' -> '.join(traces), mods, None, None, ppn or 1, functor, override_tol) + return TestCaseBuilder(" -> ".join(traces), mods, None, None, ppn or 1, functor, override_tol) + def input_bubbles_lagrange(self): if "lagrange_bubblescreen" in self.trace: - copy_input_lagrange(f'/3D_lagrange_bubblescreen',f'{self.get_dirpath()}') + copy_input_lagrange("/3D_lagrange_bubblescreen", f"{self.get_dirpath()}") elif "lagrange_shbubcollapse" in self.trace: - copy_input_lagrange(f'/3D_lagrange_shbubcollapse',f'{self.get_dirpath()}') + copy_input_lagrange("/3D_lagrange_shbubcollapse", f"{self.get_dirpath()}") else: - create_input_lagrange(f'{self.get_dirpath()}') + create_input_lagrange(f"{self.get_dirpath()}") + def create_input_lagrange(path_test): - folder_path_lagrange = path_test + '/input' - file_path_lagrange = folder_path_lagrange + '/lag_bubbles.dat' + folder_path_lagrange = path_test + "/input" + file_path_lagrange = folder_path_lagrange + "/lag_bubbles.dat" if not os.path.exists(folder_path_lagrange): os.mkdir(folder_path_lagrange) with open(file_path_lagrange, "w") as file: - file.write('0.5\t0.5\t0.5\t0.0\t0.0\t0.0\t8.0e-03\t0.0') + file.write("0.5\t0.5\t0.5\t0.0\t0.0\t0.0\t8.0e-03\t0.0") + def copy_input_lagrange(path_example_input, path_test): - folder_path_dest = path_test + '/input/' - fite_path_dest = folder_path_dest + 'lag_bubbles.dat' - file_path_src = common.MFC_EXAMPLE_DIRPATH + path_example_input + '/input/lag_bubbles.dat' + folder_path_dest = path_test + "/input/" + fite_path_dest = folder_path_dest + "lag_bubbles.dat" + file_path_src = common.MFC_EXAMPLE_DIRPATH + path_example_input + "/input/lag_bubbles.dat" if not os.path.exists(folder_path_dest): os.mkdir(folder_path_dest) diff --git a/toolchain/mfc/test/cases.py b/toolchain/mfc/test/cases.py index 7835981151..052fa8d92a 100644 --- a/toolchain/mfc/test/cases.py +++ b/toolchain/mfc/test/cases.py @@ -1,17 +1,17 @@ -# pylint: disable=too-many-lines +import itertools import os import typing -import itertools from mfc import common -from .case import Nt, define_case_d, define_case_f, CaseGeneratorStack, TestCaseBuilder + from ..state import ARG +from .case import CaseGeneratorStack, Nt, TestCaseBuilder, define_case_d, define_case_f def get_bc_mods(bc: int, dimInfo): params = {} for dimCmp in dimInfo[0]: - params.update({f'bc_{dimCmp}%beg': bc, f'bc_{dimCmp}%end': bc}) + params.update({f"bc_{dimCmp}%beg": bc, f"bc_{dimCmp}%end": bc}) return params @@ -19,43 +19,57 @@ def get_bc_mods(bc: int, dimInfo): def get_dimensions(): r = [] - for dimInfo in [(["x"], {'m': 299, 'n': 0, 'p': 0}, {"geometry": 1}), - (["x", "y"], {'m': 49, 'n': 39, 'p': 0}, {"geometry": 3}), - (["x", "y", "z"], {'m': 24, 'n': 24, 'p': 24}, {"geometry": 9})]: + for dimInfo in [(["x"], {"m": 299, "n": 0, "p": 0}, {"geometry": 1}), (["x", "y"], {"m": 49, "n": 39, "p": 0}, {"geometry": 3}), (["x", "y", "z"], {"m": 24, "n": 24, "p": 24}, {"geometry": 9})]: dimParams = {**dimInfo[1]} for dimCmp in dimInfo[0]: - dimParams.update({ - f"{dimCmp}_domain%beg": 0.E+00, f"{dimCmp}_domain%end": 1.E+00 - }) + dimParams.update({f"{dimCmp}_domain%beg": 0.0e00, f"{dimCmp}_domain%end": 1.0e00}) dimParams.update(get_bc_mods(-3, dimInfo)) - for patchID in range(1, 3+1): + for patchID in range(1, 3 + 1): dimParams[f"patch_icpp({patchID})%geometry"] = dimInfo[2].get("geometry") if "z" in dimInfo[0]: - dimParams.update({ - f"patch_icpp({1})%z_centroid": 0.05, f"patch_icpp({1})%length_z": 0.1, - f"patch_icpp({2})%z_centroid": 0.45, f"patch_icpp({2})%length_z": 0.7, - f"patch_icpp({3})%z_centroid": 0.9, f"patch_icpp({3})%length_z": 0.2, - f"patch_icpp({patchID})%y_centroid": 0.5, f"patch_icpp({patchID})%length_y": 1, - f"patch_icpp({patchID})%x_centroid": 0.5, f"patch_icpp({patchID})%length_x": 1 - }) + dimParams.update( + { + f"patch_icpp({1})%z_centroid": 0.05, + f"patch_icpp({1})%length_z": 0.1, + f"patch_icpp({2})%z_centroid": 0.45, + f"patch_icpp({2})%length_z": 0.7, + f"patch_icpp({3})%z_centroid": 0.9, + f"patch_icpp({3})%length_z": 0.2, + f"patch_icpp({patchID})%y_centroid": 0.5, + f"patch_icpp({patchID})%length_y": 1, + f"patch_icpp({patchID})%x_centroid": 0.5, + f"patch_icpp({patchID})%length_x": 1, + } + ) elif "y" in dimInfo[0]: - dimParams.update({ - f"patch_icpp({1})%y_centroid": 0.05, f"patch_icpp({1})%length_y": 0.1, - f"patch_icpp({2})%y_centroid": 0.45, f"patch_icpp({2})%length_y": 0.7, - f"patch_icpp({3})%y_centroid": 0.9, f"patch_icpp({3})%length_y": 0.2, - f"patch_icpp({patchID})%x_centroid": 0.5, f"patch_icpp({patchID})%length_x": 1 - }) + dimParams.update( + { + f"patch_icpp({1})%y_centroid": 0.05, + f"patch_icpp({1})%length_y": 0.1, + f"patch_icpp({2})%y_centroid": 0.45, + f"patch_icpp({2})%length_y": 0.7, + f"patch_icpp({3})%y_centroid": 0.9, + f"patch_icpp({3})%length_y": 0.2, + f"patch_icpp({patchID})%x_centroid": 0.5, + f"patch_icpp({patchID})%length_x": 1, + } + ) else: - dimParams.update({ - f"patch_icpp({1})%x_centroid": 0.05, f"patch_icpp({1})%length_x": 0.1, - f"patch_icpp({2})%x_centroid": 0.45, f"patch_icpp({2})%length_x": 0.7, - f"patch_icpp({3})%x_centroid": 0.9, f"patch_icpp({3})%length_x": 0.2 - }) + dimParams.update( + { + f"patch_icpp({1})%x_centroid": 0.05, + f"patch_icpp({1})%length_x": 0.1, + f"patch_icpp({2})%x_centroid": 0.45, + f"patch_icpp({2})%length_x": 0.7, + f"patch_icpp({3})%x_centroid": 0.9, + f"patch_icpp({3})%length_x": 0.2, + } + ) if "x" in dimInfo[0]: dimParams[f"patch_icpp({patchID})%vel(1)"] = 0.0 @@ -70,8 +84,6 @@ def get_dimensions(): return r -# pylint: disable=too-many-locals, too-many-statements - def list_cases() -> typing.List[TestCaseBuilder]: stack, cases = CaseGeneratorStack(), [] @@ -82,68 +94,180 @@ def alter_bcs(dimInfo): def alter_grcbc(dimInfo): if len(dimInfo[0]) == 1: - stack.push('', {'patch_icpp(1)%vel(1)': 1.0, 'patch_icpp(2)%vel(1)': 1.0, 'patch_icpp(3)%vel(1)': 1.0, - 'bc_x%beg': -7, 'bc_x%end': -8, 'bc_x%grcbc_in': 'T', 'bc_x%grcbc_out': 'T', 'bc_x%grcbc_vel_out': 'T', - 'bc_x%vel_in(1)': 1.0, 'bc_x%vel_in(2)': 0.0, 'bc_x%vel_in(3)': 0.0, 'bc_x%vel_out(1)': 1.0, 'bc_x%vel_out(2)': 0.0, 'bc_x%vel_out(3)': 0.0, - 'bc_x%pres_in': 1.0, 'bc_x%pres_out': 1.0, 'bc_x%alpha_in(1)': 1.0, 'bc_x%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc x"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(1)": 1.0, + "patch_icpp(2)%vel(1)": 1.0, + "patch_icpp(3)%vel(1)": 1.0, + "bc_x%beg": -7, + "bc_x%end": -8, + "bc_x%grcbc_in": "T", + "bc_x%grcbc_out": "T", + "bc_x%grcbc_vel_out": "T", + "bc_x%vel_in(1)": 1.0, + "bc_x%vel_in(2)": 0.0, + "bc_x%vel_in(3)": 0.0, + "bc_x%vel_out(1)": 1.0, + "bc_x%vel_out(2)": 0.0, + "bc_x%vel_out(3)": 0.0, + "bc_x%pres_in": 1.0, + "bc_x%pres_out": 1.0, + "bc_x%alpha_in(1)": 1.0, + "bc_x%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc x"], {})) stack.pop() elif len(dimInfo[0]) == 2: - stack.push('', {'patch_icpp(1)%vel(1)': 1.0, 'patch_icpp(2)%vel(1)': 1.0, 'patch_icpp(3)%vel(1)': 1.0, - 'bc_x%beg': -7, 'bc_x%end': -8, 'bc_x%grcbc_in': 'T', 'bc_x%grcbc_out': 'T', 'bc_x%grcbc_vel_out': 'T', - 'bc_x%vel_in(1)': 1.0, 'bc_x%vel_in(2)': 0.0, 'bc_x%vel_in(3)': 0.0, 'bc_x%vel_out(1)': 1.0, 'bc_x%vel_out(2)': 0.0, 'bc_x%vel_out(3)': 0.0, - 'bc_x%pres_in': 1.0, 'bc_x%pres_out': 1.0, 'bc_x%alpha_in(1)': 1.0, 'bc_x%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc x"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(1)": 1.0, + "patch_icpp(2)%vel(1)": 1.0, + "patch_icpp(3)%vel(1)": 1.0, + "bc_x%beg": -7, + "bc_x%end": -8, + "bc_x%grcbc_in": "T", + "bc_x%grcbc_out": "T", + "bc_x%grcbc_vel_out": "T", + "bc_x%vel_in(1)": 1.0, + "bc_x%vel_in(2)": 0.0, + "bc_x%vel_in(3)": 0.0, + "bc_x%vel_out(1)": 1.0, + "bc_x%vel_out(2)": 0.0, + "bc_x%vel_out(3)": 0.0, + "bc_x%pres_in": 1.0, + "bc_x%pres_out": 1.0, + "bc_x%alpha_in(1)": 1.0, + "bc_x%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc x"], {})) stack.pop() - stack.push('', {'patch_icpp(1)%vel(2)': 1.0, 'patch_icpp(2)%vel(2)': 1.0, 'patch_icpp(3)%vel(2)': 1.0, - 'bc_y%beg': -7, 'bc_y%end': -8, 'bc_y%grcbc_in': 'T', 'bc_y%grcbc_out': 'T', 'bc_y%grcbc_vel_out': 'T', - 'bc_y%vel_in(1)': 0.0, 'bc_y%vel_in(2)': 1.0, 'bc_y%vel_in(3)': 0.0, 'bc_y%vel_out(1)': 0.0, 'bc_y%vel_out(2)': 1.0, 'bc_y%vel_out(3)': 0.0, - 'bc_y%pres_in': 1.0, 'bc_y%pres_out': 1.0, 'bc_y%alpha_in(1)': 1.0, 'bc_y%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc y"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(2)": 1.0, + "patch_icpp(2)%vel(2)": 1.0, + "patch_icpp(3)%vel(2)": 1.0, + "bc_y%beg": -7, + "bc_y%end": -8, + "bc_y%grcbc_in": "T", + "bc_y%grcbc_out": "T", + "bc_y%grcbc_vel_out": "T", + "bc_y%vel_in(1)": 0.0, + "bc_y%vel_in(2)": 1.0, + "bc_y%vel_in(3)": 0.0, + "bc_y%vel_out(1)": 0.0, + "bc_y%vel_out(2)": 1.0, + "bc_y%vel_out(3)": 0.0, + "bc_y%pres_in": 1.0, + "bc_y%pres_out": 1.0, + "bc_y%alpha_in(1)": 1.0, + "bc_y%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc y"], {})) stack.pop() elif len(dimInfo[0]) == 3: - stack.push('', {'patch_icpp(1)%vel(1)': 1.0, 'patch_icpp(2)%vel(1)': 1.0, 'patch_icpp(3)%vel(1)': 1.0, - 'bc_x%beg': -7, 'bc_x%end': -8, 'bc_x%grcbc_in': 'T', 'bc_x%grcbc_out': 'T', 'bc_x%grcbc_vel_out': 'T', - 'bc_x%vel_in(1)': 1.0, 'bc_x%vel_in(2)': 0.0, 'bc_x%vel_in(3)': 0.0, 'bc_x%vel_out(1)': 1.0, 'bc_x%vel_out(2)': 0.0, 'bc_x%vel_out(3)': 0.0, - 'bc_x%pres_in': 1.0, 'bc_x%pres_out': 1.0, 'bc_x%alpha_in(1)': 1.0, 'bc_x%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc x"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(1)": 1.0, + "patch_icpp(2)%vel(1)": 1.0, + "patch_icpp(3)%vel(1)": 1.0, + "bc_x%beg": -7, + "bc_x%end": -8, + "bc_x%grcbc_in": "T", + "bc_x%grcbc_out": "T", + "bc_x%grcbc_vel_out": "T", + "bc_x%vel_in(1)": 1.0, + "bc_x%vel_in(2)": 0.0, + "bc_x%vel_in(3)": 0.0, + "bc_x%vel_out(1)": 1.0, + "bc_x%vel_out(2)": 0.0, + "bc_x%vel_out(3)": 0.0, + "bc_x%pres_in": 1.0, + "bc_x%pres_out": 1.0, + "bc_x%alpha_in(1)": 1.0, + "bc_x%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc x"], {})) stack.pop() - stack.push('', {'patch_icpp(1)%vel(2)': 1.0, 'patch_icpp(2)%vel(2)': 1.0, 'patch_icpp(3)%vel(2)': 1.0, - 'bc_y%beg': -7, 'bc_y%end': -8, 'bc_y%grcbc_in': 'T', 'bc_y%grcbc_out': 'T', 'bc_y%grcbc_vel_out': 'T', - 'bc_y%vel_in(1)': 0.0, 'bc_y%vel_in(2)': 1.0, 'bc_y%vel_in(3)': 0.0, 'bc_y%vel_out(1)': 0.0, 'bc_y%vel_out(2)': 1.0, 'bc_y%vel_out(3)': 0.0, - 'bc_y%pres_in': 1.0, 'bc_y%pres_out': 1.0, 'bc_y%alpha_in(1)': 1.0, 'bc_y%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc y"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(2)": 1.0, + "patch_icpp(2)%vel(2)": 1.0, + "patch_icpp(3)%vel(2)": 1.0, + "bc_y%beg": -7, + "bc_y%end": -8, + "bc_y%grcbc_in": "T", + "bc_y%grcbc_out": "T", + "bc_y%grcbc_vel_out": "T", + "bc_y%vel_in(1)": 0.0, + "bc_y%vel_in(2)": 1.0, + "bc_y%vel_in(3)": 0.0, + "bc_y%vel_out(1)": 0.0, + "bc_y%vel_out(2)": 1.0, + "bc_y%vel_out(3)": 0.0, + "bc_y%pres_in": 1.0, + "bc_y%pres_out": 1.0, + "bc_y%alpha_in(1)": 1.0, + "bc_y%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc y"], {})) stack.pop() - stack.push('', {'patch_icpp(1)%vel(3)': 1.0, 'patch_icpp(2)%vel(3)': 1.0, 'patch_icpp(3)%vel(3)': 1.0, - 'bc_z%beg': -7, 'bc_z%end': -8, 'bc_z%grcbc_in': 'T', 'bc_z%grcbc_out': 'T', 'bc_z%grcbc_vel_out': 'T', - 'bc_z%vel_in(1)': 0.0, 'bc_z%vel_in(2)': 0.0, 'bc_z%vel_in(3)': 1.0, 'bc_z%vel_out(1)': 0.0, 'bc_z%vel_out(2)': 0.0, 'bc_z%vel_out(3)': 1.0, - 'bc_z%pres_in': 1.0, 'bc_z%pres_out': 1.0, 'bc_z%alpha_in(1)': 1.0, 'bc_z%alpha_rho_in(1)': 1.0}) - cases.append(define_case_d(stack, [f"grcbc z"], {})) + stack.push( + "", + { + "patch_icpp(1)%vel(3)": 1.0, + "patch_icpp(2)%vel(3)": 1.0, + "patch_icpp(3)%vel(3)": 1.0, + "bc_z%beg": -7, + "bc_z%end": -8, + "bc_z%grcbc_in": "T", + "bc_z%grcbc_out": "T", + "bc_z%grcbc_vel_out": "T", + "bc_z%vel_in(1)": 0.0, + "bc_z%vel_in(2)": 0.0, + "bc_z%vel_in(3)": 1.0, + "bc_z%vel_out(1)": 0.0, + "bc_z%vel_out(2)": 0.0, + "bc_z%vel_out(3)": 1.0, + "bc_z%pres_in": 1.0, + "bc_z%pres_out": 1.0, + "bc_z%alpha_in(1)": 1.0, + "bc_z%alpha_rho_in(1)": 1.0, + }, + ) + cases.append(define_case_d(stack, ["grcbc z"], {})) stack.pop() def alter_capillary(): - stack.push('', {'patch_icpp(1)%cf_val': 1, 'patch_icpp(2)%cf_val': 0, 'patch_icpp(3)%cf_val': 1, - 'sigma': 1, 'model_eqns': 3, 'surface_tension': 'T'}) - cases.append(define_case_d(stack, [f"capillary=T", "model_eqns=3"], {})) + stack.push("", {"patch_icpp(1)%cf_val": 1, "patch_icpp(2)%cf_val": 0, "patch_icpp(3)%cf_val": 1, "sigma": 1, "model_eqns": 3, "surface_tension": "T"}) + cases.append(define_case_d(stack, ["capillary=T", "model_eqns=3"], {})) stack.pop() def alter_weno(dimInfo): for weno_order in [3, 5, 7]: - stack.push(f"weno_order={weno_order}", {'weno_order': weno_order}) - for mapped_weno, wenoz, teno, mp_weno in itertools.product('FT', repeat=4): - - if sum(var == 'T' for var in [mapped_weno, wenoz, teno, mp_weno]) > 1: + stack.push(f"weno_order={weno_order}", {"weno_order": weno_order}) + for mapped_weno, wenoz, teno, mp_weno in itertools.product("FT", repeat=4): + if sum(var == "T" for var in [mapped_weno, wenoz, teno, mp_weno]) > 1: continue - if mp_weno == 'T' and weno_order != 5: + if mp_weno == "T" and weno_order != 5: continue - if teno == 'T' and weno_order == 3: + if teno == "T" and weno_order == 3: continue - trace = [f"{var}={val}" for var, val in zip(["mapped_weno", "wenoz", "teno", "mp_weno"], [mapped_weno, wenoz, teno, mp_weno]) if val == 'T'] - data = {var: 'T' for var, val in zip(["mapped_weno", "wenoz", "teno", "mp_weno"], [mapped_weno, wenoz, teno, mp_weno]) if val == 'T'} + trace = [f"{var}={val}" for var, val in zip(["mapped_weno", "wenoz", "teno", "mp_weno"], [mapped_weno, wenoz, teno, mp_weno]) if val == "T"] + data = {var: "T" for var, val in zip(["mapped_weno", "wenoz", "teno", "mp_weno"], [mapped_weno, wenoz, teno, mp_weno]) if val == "T"} if "teno" in data: data["teno_CT"] = 1e-6 @@ -151,26 +275,24 @@ def alter_weno(dimInfo): data["wenoz_q"] = 3.0 if weno_order == 7: - data = {**data, 'weno_eps': 1e-6} # increase damping for stability + data = {**data, "weno_eps": 1e-6} # increase damping for stability if "z" in dimInfo[0]: - data = {**data, 'm': 35, 'n': 35, 'p': 35} + data = {**data, "m": 35, "n": 35, "p": 35} cases.append(define_case_d(stack, trace, data)) stack.pop() def alter_igr(): - stack.push('IGR', {'igr': 'T', 'alf_factor': 10, 'num_igr_iters': 10, - 'elliptic_smoothing': 'T', 'elliptic_smoothing_iters': 10, - 'num_igr_warm_start_iters': 10}) + stack.push("IGR", {"igr": "T", "alf_factor": 10, "num_igr_iters": 10, "elliptic_smoothing": "T", "elliptic_smoothing_iters": 10, "num_igr_warm_start_iters": 10}) for order in [3, 5]: - stack.push(f"igr_order={order}", {'igr_order': order}) + stack.push(f"igr_order={order}", {"igr_order": order}) - cases.append(define_case_d(stack, 'Jacobi', {'igr_iter_solver': 1})) + cases.append(define_case_d(stack, "Jacobi", {"igr_iter_solver": 1})) if order == 5: - cases.append(define_case_d(stack, 'Gauss Seidel', {'igr_iter_solver': 2})) + cases.append(define_case_d(stack, "Gauss Seidel", {"igr_iter_solver": 2})) stack.pop() @@ -178,50 +300,50 @@ def alter_igr(): def alter_muscl(): for muscl_order in [1, 2]: - stack.push(f"muscl_order={muscl_order}", {'muscl_order': muscl_order, 'recon_type': 2, 'weno_order': 0}) + stack.push(f"muscl_order={muscl_order}", {"muscl_order": muscl_order, "recon_type": 2, "weno_order": 0}) if muscl_order == 1: for int_comp in ["T", "F"]: - cases.append(define_case_d(stack, f"int_comp={int_comp}", {'int_comp': int_comp})) + cases.append(define_case_d(stack, f"int_comp={int_comp}", {"int_comp": int_comp})) elif muscl_order == 2: for int_comp in ["T", "F"]: - stack.push(f"int_comp={int_comp}", {'int_comp': int_comp}) - cases.append(define_case_d(stack, f"muscl_lim=1", {'muscl_lim': 1})) + stack.push(f"int_comp={int_comp}", {"int_comp": int_comp}) + cases.append(define_case_d(stack, "muscl_lim=1", {"muscl_lim": 1})) stack.pop() for muscl_lim in [2, 3, 4, 5]: - cases.append(define_case_d(stack, f"muscl_lim={muscl_lim}", {'muscl_lim': muscl_lim})) + cases.append(define_case_d(stack, f"muscl_lim={muscl_lim}", {"muscl_lim": muscl_lim})) stack.pop() def alter_riemann_solvers(num_fluids): for riemann_solver in [1, 5, 2]: - stack.push(f"riemann_solver={riemann_solver}", {'riemann_solver': riemann_solver}) + stack.push(f"riemann_solver={riemann_solver}", {"riemann_solver": riemann_solver}) - cases.append(define_case_d(stack, "mixture_err", {'mixture_err': 'T'})) + cases.append(define_case_d(stack, "mixture_err", {"mixture_err": "T"})) if riemann_solver in (1, 2): - cases.append(define_case_d(stack, "avg_state=1", {'avg_state': 1})) - cases.append(define_case_d(stack, "wave_speeds=2", {'wave_speeds': 2})) + cases.append(define_case_d(stack, "avg_state=1", {"avg_state": 1})) + cases.append(define_case_d(stack, "wave_speeds=2", {"wave_speeds": 2})) if riemann_solver == 2: - cases.append(define_case_d(stack, "model_eqns=3", {'model_eqns': 3})) + cases.append(define_case_d(stack, "model_eqns=3", {"model_eqns": 3})) if num_fluids == 2: if riemann_solver == 2: - cases.append(define_case_d(stack, 'alt_soundspeed', {'alt_soundspeed': 'T'})) + cases.append(define_case_d(stack, "alt_soundspeed", {"alt_soundspeed": "T"})) - cases.append(define_case_d(stack, 'mpp_lim', {'mpp_lim': 'T'})) + cases.append(define_case_d(stack, "mpp_lim", {"mpp_lim": "T"})) stack.pop() def alter_low_Mach_correction(): - stack.push('', {'fluid_pp(1)%gamma': 0.16, 'fluid_pp(1)%pi_inf': 3515.0, 'dt': 1e-7}) + stack.push("", {"fluid_pp(1)%gamma": 0.16, "fluid_pp(1)%pi_inf": 3515.0, "dt": 1e-7}) - stack.push(f"riemann_solver=1", {'riemann_solver': 1}) - cases.append(define_case_d(stack, 'low_Mach=1', {'low_Mach': 1})) + stack.push("riemann_solver=1", {"riemann_solver": 1}) + cases.append(define_case_d(stack, "low_Mach=1", {"low_Mach": 1})) stack.pop() - stack.push(f"riemann_solver=2", {'riemann_solver': 2}) - cases.append(define_case_d(stack, 'low_Mach=1', {'low_Mach': 1})) - cases.append(define_case_d(stack, 'low_Mach=2', {'low_Mach': 2})) + stack.push("riemann_solver=2", {"riemann_solver": 2}) + cases.append(define_case_d(stack, "low_Mach=1", {"low_Mach": 1})) + cases.append(define_case_d(stack, "low_Mach=2", {"low_Mach": 2})) stack.pop() stack.pop() @@ -231,13 +353,25 @@ def alter_num_fluids(dimInfo): stack.push(f"{num_fluids} Fluid(s)", {"num_fluids": num_fluids}) if num_fluids == 2: - stack.push("", { - 'fluid_pp(2)%gamma': 2.5, 'fluid_pp(2)%pi_inf': 0.0, 'patch_icpp(1)%alpha_rho(1)': 0.81, - 'patch_icpp(1)%alpha(1)': 0.9, 'patch_icpp(1)%alpha_rho(2)': 0.19, 'patch_icpp(1)%alpha(2)': 0.1, - 'patch_icpp(2)%alpha_rho(1)': 0.25, 'patch_icpp(2)%alpha(1)': 0.5, 'patch_icpp(2)%alpha_rho(2)': 0.25, - 'patch_icpp(2)%alpha(2)': 0.5, 'patch_icpp(3)%alpha_rho(1)': 0.08, 'patch_icpp(3)%alpha(1)': 0.2, - 'patch_icpp(3)%alpha_rho(2)': 0.0225, 'patch_icpp(3)%alpha(2)': 0.8 - }) + stack.push( + "", + { + "fluid_pp(2)%gamma": 2.5, + "fluid_pp(2)%pi_inf": 0.0, + "patch_icpp(1)%alpha_rho(1)": 0.81, + "patch_icpp(1)%alpha(1)": 0.9, + "patch_icpp(1)%alpha_rho(2)": 0.19, + "patch_icpp(1)%alpha(2)": 0.1, + "patch_icpp(2)%alpha_rho(1)": 0.25, + "patch_icpp(2)%alpha(1)": 0.5, + "patch_icpp(2)%alpha_rho(2)": 0.25, + "patch_icpp(2)%alpha(2)": 0.5, + "patch_icpp(3)%alpha_rho(1)": 0.08, + "patch_icpp(3)%alpha(1)": 0.2, + "patch_icpp(3)%alpha_rho(2)": 0.0225, + "patch_icpp(3)%alpha(2)": 0.8, + }, + ) if len(dimInfo[0]) > 1: alter_capillary() @@ -249,44 +383,41 @@ def alter_num_fluids(dimInfo): alter_igr() if num_fluids == 1: - - stack.push("Viscous", { - 'fluid_pp(1)%Re(1)': 0.0001, 'dt': 1e-11, 'patch_icpp(1)%vel(1)': 1.0, - 'viscous': 'T'}) + stack.push("Viscous", {"fluid_pp(1)%Re(1)": 0.0001, "dt": 1e-11, "patch_icpp(1)%vel(1)": 1.0, "viscous": "T"}) alter_ib(dimInfo, six_eqn_model=True, viscous=True) if len(dimInfo[0]) > 1: alter_igr() - cases.append(define_case_d(stack, "", {'weno_Re_flux': 'F'})) - cases.append(define_case_d(stack, "weno_Re_flux", {'weno_Re_flux': 'T'})) - cases.append(define_case_d(stack, "riemann_solver=5", {'riemann_solver': 5})) + cases.append(define_case_d(stack, "", {"weno_Re_flux": "F"})) + cases.append(define_case_d(stack, "weno_Re_flux", {"weno_Re_flux": "T"})) + cases.append(define_case_d(stack, "riemann_solver=5", {"riemann_solver": 5})) - for weno_Re_flux in ['T']: - stack.push("weno_Re_flux" if weno_Re_flux == 'T' else '', {'weno_Re_flux': 'T'}) - cases.append(define_case_d(stack, "weno_avg", {'weno_avg': 'T'})) + for weno_Re_flux in ["T"]: + stack.push("weno_Re_flux" if weno_Re_flux == "T" else "", {"weno_Re_flux": "T"}) + cases.append(define_case_d(stack, "weno_avg", {"weno_avg": "T"})) stack.pop() stack.pop() if num_fluids == 2: - stack.push("Viscous", { - 'fluid_pp(1)%Re(1)': 0.001, 'fluid_pp(1)%Re(2)': 0.001, - 'fluid_pp(2)%Re(1)': 0.001, 'fluid_pp(2)%Re(2)': 0.001, 'dt': 1e-11, - 'patch_icpp(1)%vel(1)': 1.0, 'viscous': 'T'}) + stack.push( + "Viscous", + {"fluid_pp(1)%Re(1)": 0.001, "fluid_pp(1)%Re(2)": 0.001, "fluid_pp(2)%Re(1)": 0.001, "fluid_pp(2)%Re(2)": 0.001, "dt": 1e-11, "patch_icpp(1)%vel(1)": 1.0, "viscous": "T"}, + ) alter_ib(dimInfo, six_eqn_model=True, viscous=True) if len(dimInfo[0]) > 1: alter_igr() - cases.append(define_case_d(stack, "", {'weno_Re_flux': 'F'})) - cases.append(define_case_d(stack, "weno_Re_flux", {'weno_Re_flux': 'T'})) - cases.append(define_case_d(stack, "riemann_solver=5", {'riemann_solver': 5})) - for weno_Re_flux in ['T']: - stack.push("weno_Re_flux" if weno_Re_flux == 'T' else '', {'weno_Re_flux': 'T'}) - cases.append(define_case_d(stack, "weno_avg", {'weno_avg': 'T'})) + cases.append(define_case_d(stack, "", {"weno_Re_flux": "F"})) + cases.append(define_case_d(stack, "weno_Re_flux", {"weno_Re_flux": "T"})) + cases.append(define_case_d(stack, "riemann_solver=5", {"riemann_solver": 5})) + for weno_Re_flux in ["T"]: + stack.push("weno_Re_flux" if weno_Re_flux == "T" else "", {"weno_Re_flux": "T"}) + cases.append(define_case_d(stack, "weno_avg", {"weno_avg": "T"})) stack.pop() stack.pop() @@ -295,74 +426,120 @@ def alter_num_fluids(dimInfo): stack.pop() def alter_2d(): - stack.push("Axisymmetric", { - 'num_fluids': 2, 'bc_y%beg': -2, 'cyl_coord': 'T', - 'fluid_pp(2)%gamma': 2.5, 'fluid_pp(2)%pi_inf': 0.0, 'patch_icpp(1)%alpha_rho(1)': 0.81, - 'patch_icpp(1)%alpha(1)': 0.9, 'patch_icpp(1)%alpha_rho(2)': 0.19, 'patch_icpp(1)%alpha(2)': 0.1, - 'patch_icpp(2)%alpha_rho(1)': 0.25, 'patch_icpp(2)%alpha(1)': 0.5, 'patch_icpp(2)%alpha_rho(2)': 0.25, - 'patch_icpp(2)%alpha(2)': 0.5, 'patch_icpp(3)%alpha_rho(1)': 0.08, 'patch_icpp(3)%alpha(1)': 0.2, - 'patch_icpp(3)%alpha_rho(2)': 0.0225, 'patch_icpp(3)%alpha(2)': 0.8, 'patch_icpp(1)%vel(1)': 0.0 - }) - - cases.append(define_case_d(stack, "model_eqns=2", {'model_eqns': 2})) - cases.append(define_case_d(stack, "model_eqns=3", {'model_eqns': 3})) - cases.append(define_case_d(stack, "HLL", {'riemann_solver': 1})) - - stack.push("Viscous", { - 'fluid_pp(1)%Re(1)': 0.0001, 'fluid_pp(1)%Re(2)': 0.0001, - 'fluid_pp(2)%Re(1)': 0.0001, 'fluid_pp(2)%Re(2)': 0.0001, 'dt': 1e-11, - 'viscous': 'T'}) - - cases.append(define_case_d(stack, "", {'weno_Re_flux': 'F'})) - cases.append(define_case_d(stack, "weno_Re_flux", {'weno_Re_flux': 'T'})) - for weno_Re_flux in ['T']: - stack.push("weno_Re_flux" if weno_Re_flux == 'T' else '', {'weno_Re_flux': 'T'}) - cases.append(define_case_d(stack, "weno_avg", {'weno_avg': 'T'})) + stack.push( + "Axisymmetric", + { + "num_fluids": 2, + "bc_y%beg": -2, + "cyl_coord": "T", + "fluid_pp(2)%gamma": 2.5, + "fluid_pp(2)%pi_inf": 0.0, + "patch_icpp(1)%alpha_rho(1)": 0.81, + "patch_icpp(1)%alpha(1)": 0.9, + "patch_icpp(1)%alpha_rho(2)": 0.19, + "patch_icpp(1)%alpha(2)": 0.1, + "patch_icpp(2)%alpha_rho(1)": 0.25, + "patch_icpp(2)%alpha(1)": 0.5, + "patch_icpp(2)%alpha_rho(2)": 0.25, + "patch_icpp(2)%alpha(2)": 0.5, + "patch_icpp(3)%alpha_rho(1)": 0.08, + "patch_icpp(3)%alpha(1)": 0.2, + "patch_icpp(3)%alpha_rho(2)": 0.0225, + "patch_icpp(3)%alpha(2)": 0.8, + "patch_icpp(1)%vel(1)": 0.0, + }, + ) + + cases.append(define_case_d(stack, "model_eqns=2", {"model_eqns": 2})) + cases.append(define_case_d(stack, "model_eqns=3", {"model_eqns": 3})) + cases.append(define_case_d(stack, "HLL", {"riemann_solver": 1})) + + stack.push("Viscous", {"fluid_pp(1)%Re(1)": 0.0001, "fluid_pp(1)%Re(2)": 0.0001, "fluid_pp(2)%Re(1)": 0.0001, "fluid_pp(2)%Re(2)": 0.0001, "dt": 1e-11, "viscous": "T"}) + + cases.append(define_case_d(stack, "", {"weno_Re_flux": "F"})) + cases.append(define_case_d(stack, "weno_Re_flux", {"weno_Re_flux": "T"})) + for weno_Re_flux in ["T"]: + stack.push("weno_Re_flux" if weno_Re_flux == "T" else "", {"weno_Re_flux": "T"}) + cases.append(define_case_d(stack, "weno_avg", {"weno_avg": "T"})) stack.pop() stack.pop() stack.pop() def alter_3d(): - stack.push("Cylindrical", { - 'bc_y%beg': -14, 'bc_z%beg': -1, 'bc_z%end': -1, 'cyl_coord': 'T', 'x_domain%beg': 0.E+00, - 'x_domain%end': 5.E+00, 'y_domain%beg': 0.E+00, 'y_domain%end': 1.E+00, 'z_domain%beg': 0.E+00, - 'z_domain%end': 2.0*3.141592653589793E+00, 'm': 29, 'n': 29, 'p': 29, - 'patch_icpp(1)%geometry': 10, 'patch_icpp(1)%x_centroid': 0.5, 'patch_icpp(1)%y_centroid': 0.E+00, - 'patch_icpp(1)%z_centroid': 0.E+00, 'patch_icpp(1)%radius': 1.0, 'patch_icpp(1)%length_x': 1.0, - 'patch_icpp(1)%length_y': -1E+6, 'patch_icpp(1)%length_z': -1E+6, - 'patch_icpp(2)%geometry': 10, 'patch_icpp(2)%x_centroid': 2.5, 'patch_icpp(2)%y_centroid': 0.E+00, - 'patch_icpp(2)%z_centroid': 0.E+00, 'patch_icpp(2)%radius': 1.0, 'patch_icpp(2)%length_x': 3.0, - 'patch_icpp(2)%length_y': -1E+6, 'patch_icpp(2)%length_z': -1E+6, - 'patch_icpp(3)%geometry': 10, 'patch_icpp(3)%x_centroid': 4.5, 'patch_icpp(3)%y_centroid': 0.E+00, - 'patch_icpp(3)%z_centroid': 0.E+00, 'patch_icpp(3)%radius': 1.0, 'patch_icpp(3)%length_x': 1.0, - 'patch_icpp(3)%length_y': -1E+6, 'patch_icpp(3)%length_z': -1E+6, 'patch_icpp(1)%vel(1)': 0.0, - 'num_fluids': 2, - 'fluid_pp(2)%gamma': 2.5, 'fluid_pp(2)%pi_inf': 0.0, 'patch_icpp(1)%alpha_rho(1)': 0.81, - 'patch_icpp(1)%alpha(1)': 0.9, 'patch_icpp(1)%alpha_rho(2)': 0.19, 'patch_icpp(1)%alpha(2)': 0.1, - 'patch_icpp(2)%alpha_rho(1)': 0.25, 'patch_icpp(2)%alpha(1)': 0.5, 'patch_icpp(2)%alpha_rho(2)': 0.25, - 'patch_icpp(2)%alpha(2)': 0.5, 'patch_icpp(3)%alpha_rho(1)': 0.08, 'patch_icpp(3)%alpha(1)': 0.2, - 'patch_icpp(3)%alpha_rho(2)': 0.0225, 'patch_icpp(3)%alpha(2)': 0.8 - }) - - cases.append(define_case_d(stack, "model_eqns=2", {'model_eqns': 2})) - - stack.push('cfl_adap_dt=T', {'cfl_adap_dt': 'T', 'cfl_target': 0.08, 't_save': 0.1, 'n_start': 0, 't_stop': 0.1}) - cases.append(define_case_d(stack, '', {})) + stack.push( + "Cylindrical", + { + "bc_y%beg": -14, + "bc_z%beg": -1, + "bc_z%end": -1, + "cyl_coord": "T", + "x_domain%beg": 0.0e00, + "x_domain%end": 5.0e00, + "y_domain%beg": 0.0e00, + "y_domain%end": 1.0e00, + "z_domain%beg": 0.0e00, + "z_domain%end": 2.0 * 3.141592653589793e00, + "m": 29, + "n": 29, + "p": 29, + "patch_icpp(1)%geometry": 10, + "patch_icpp(1)%x_centroid": 0.5, + "patch_icpp(1)%y_centroid": 0.0e00, + "patch_icpp(1)%z_centroid": 0.0e00, + "patch_icpp(1)%radius": 1.0, + "patch_icpp(1)%length_x": 1.0, + "patch_icpp(1)%length_y": -1e6, + "patch_icpp(1)%length_z": -1e6, + "patch_icpp(2)%geometry": 10, + "patch_icpp(2)%x_centroid": 2.5, + "patch_icpp(2)%y_centroid": 0.0e00, + "patch_icpp(2)%z_centroid": 0.0e00, + "patch_icpp(2)%radius": 1.0, + "patch_icpp(2)%length_x": 3.0, + "patch_icpp(2)%length_y": -1e6, + "patch_icpp(2)%length_z": -1e6, + "patch_icpp(3)%geometry": 10, + "patch_icpp(3)%x_centroid": 4.5, + "patch_icpp(3)%y_centroid": 0.0e00, + "patch_icpp(3)%z_centroid": 0.0e00, + "patch_icpp(3)%radius": 1.0, + "patch_icpp(3)%length_x": 1.0, + "patch_icpp(3)%length_y": -1e6, + "patch_icpp(3)%length_z": -1e6, + "patch_icpp(1)%vel(1)": 0.0, + "num_fluids": 2, + "fluid_pp(2)%gamma": 2.5, + "fluid_pp(2)%pi_inf": 0.0, + "patch_icpp(1)%alpha_rho(1)": 0.81, + "patch_icpp(1)%alpha(1)": 0.9, + "patch_icpp(1)%alpha_rho(2)": 0.19, + "patch_icpp(1)%alpha(2)": 0.1, + "patch_icpp(2)%alpha_rho(1)": 0.25, + "patch_icpp(2)%alpha(1)": 0.5, + "patch_icpp(2)%alpha_rho(2)": 0.25, + "patch_icpp(2)%alpha(2)": 0.5, + "patch_icpp(3)%alpha_rho(1)": 0.08, + "patch_icpp(3)%alpha(1)": 0.2, + "patch_icpp(3)%alpha_rho(2)": 0.0225, + "patch_icpp(3)%alpha(2)": 0.8, + }, + ) + + cases.append(define_case_d(stack, "model_eqns=2", {"model_eqns": 2})) + + stack.push("cfl_adap_dt=T", {"cfl_adap_dt": "T", "cfl_target": 0.08, "t_save": 0.1, "n_start": 0, "t_stop": 0.1}) + cases.append(define_case_d(stack, "", {})) stack.pop() - stack.push("Viscous", { - 'fluid_pp(1)%Re(1)': 0.0001, 'fluid_pp(1)%Re(2)': 0.0001, - 'fluid_pp(2)%Re(1)': 0.0001, 'fluid_pp(2)%Re(2)': 0.0001, 'dt': 1e-10, - 'viscous': 'T' - }) - - cases.append(define_case_d(stack, "", {'weno_Re_flux': 'F'})) - cases.append(define_case_d(stack, "weno_Re_flux", {'weno_Re_flux': 'T'})) - for weno_Re_flux in ['T']: - stack.push("weno_Re_flux" if weno_Re_flux == 'T' else '', {'weno_Re_flux': 'T'}) - cases.append(define_case_d(stack, "weno_avg", {'weno_avg': 'T'})) + stack.push("Viscous", {"fluid_pp(1)%Re(1)": 0.0001, "fluid_pp(1)%Re(2)": 0.0001, "fluid_pp(2)%Re(1)": 0.0001, "fluid_pp(2)%Re(2)": 0.0001, "dt": 1e-10, "viscous": "T"}) + + cases.append(define_case_d(stack, "", {"weno_Re_flux": "F"})) + cases.append(define_case_d(stack, "weno_Re_flux", {"weno_Re_flux": "T"})) + for weno_Re_flux in ["T"]: + stack.push("weno_Re_flux" if weno_Re_flux == "T" else "", {"weno_Re_flux": "T"}) + cases.append(define_case_d(stack, "weno_avg", {"weno_avg": "T"})) stack.pop() stack.pop() @@ -370,179 +547,229 @@ def alter_3d(): def alter_ppn(dimInfo): if len(dimInfo[0]) == 3: - cases.append(define_case_d(stack, '2 MPI Ranks', {'m': 29, 'n': 29, 'p': 49}, ppn=2)) + cases.append(define_case_d(stack, "2 MPI Ranks", {"m": 29, "n": 29, "p": 49}, ppn=2)) if ARG("rdma_mpi"): - cases.append(define_case_d(stack, '2 MPI Ranks -> RDMA MPI', {'m': 29, 'n': 29, 'p': 49, 'rdma_mpi': 'T'}, ppn=2)) - cases.append(define_case_d(stack, '2 MPI Ranks -> IBM Sphere', { - 'm': 29, 'n': 29, 'p': 49, - 'ib': 'T', 'num_ibs': 1, - 'patch_ib(1)%geometry': 8, - 'patch_ib(1)%x_centroid': 0.5, - 'patch_ib(1)%y_centroid': 0.5, - 'patch_ib(1)%z_centroid': 0.5, - 'patch_ib(1)%radius': 0.1, - 'patch_icpp(1)%vel(1)': 0.001, - 'patch_icpp(2)%vel(1)': 0.001, - 'patch_icpp(3)%vel(1)': 0.001, - 'patch_ib(1)%slip': 'F', - }, ppn=2)) + cases.append(define_case_d(stack, "2 MPI Ranks -> RDMA MPI", {"m": 29, "n": 29, "p": 49, "rdma_mpi": "T"}, ppn=2)) + cases.append( + define_case_d( + stack, + "2 MPI Ranks -> IBM Sphere", + { + "m": 29, + "n": 29, + "p": 49, + "ib": "T", + "num_ibs": 1, + "patch_ib(1)%geometry": 8, + "patch_ib(1)%x_centroid": 0.5, + "patch_ib(1)%y_centroid": 0.5, + "patch_ib(1)%z_centroid": 0.5, + "patch_ib(1)%radius": 0.1, + "patch_icpp(1)%vel(1)": 0.001, + "patch_icpp(2)%vel(1)": 0.001, + "patch_icpp(3)%vel(1)": 0.001, + "patch_ib(1)%slip": "F", + }, + ppn=2, + ) + ) else: - cases.append(define_case_d(stack, '2 MPI Ranks', {}, ppn=2)) + cases.append(define_case_d(stack, "2 MPI Ranks", {}, ppn=2)) if ARG("rdma_mpi"): - cases.append(define_case_d(stack, '2 MPI Ranks -> RDMA MPI', {'rdma_mpi': 'T'}, ppn=2)) + cases.append(define_case_d(stack, "2 MPI Ranks -> RDMA MPI", {"rdma_mpi": "T"}, ppn=2)) def alter_ib(dimInfo, six_eqn_model=False, viscous=False): for slip in [True, False]: - stack.push(f'IBM', { - 'ib': 'T', 'num_ibs': 1, - 'patch_ib(1)%x_centroid': 0.5, 'patch_ib(1)%y_centroid': 0.5, - 'patch_ib(1)%radius': 0.1, 'patch_icpp(1)%vel(1)': 0.001, - 'patch_icpp(2)%vel(1)': 0.001, 'patch_icpp(3)%vel(1)': 0.001, - 'patch_ib(1)%slip': 'T' if slip else 'F', - }) + stack.push( + "IBM", + { + "ib": "T", + "num_ibs": 1, + "patch_ib(1)%x_centroid": 0.5, + "patch_ib(1)%y_centroid": 0.5, + "patch_ib(1)%radius": 0.1, + "patch_icpp(1)%vel(1)": 0.001, + "patch_icpp(2)%vel(1)": 0.001, + "patch_icpp(3)%vel(1)": 0.001, + "patch_ib(1)%slip": "T" if slip else "F", + }, + ) suffix = " -> slip" if slip else "" if len(dimInfo[0]) == 3: - cases.append(define_case_d(stack, f'Sphere{suffix}', { - 'patch_ib(1)%z_centroid': 0.5, - 'patch_ib(1)%geometry': 8, - })) - - cases.append(define_case_d(stack, f'Cuboid{suffix}', { - 'patch_ib(1)%z_centroid': 0.5, - 'patch_ib(1)%length_x': 0.1, - 'patch_ib(1)%length_y': 0.1, - 'patch_ib(1)%length_z': 0.1, - 'patch_ib(1)%geometry': 9, - })) - - cases.append(define_case_d(stack, f'Cylinder{suffix}', { - 'patch_ib(1)%z_centroid': 0.5, - 'patch_ib(1)%length_x': 0.1, - 'patch_ib(1)%geometry': 10, - })) + cases.append( + define_case_d( + stack, + f"Sphere{suffix}", + { + "patch_ib(1)%z_centroid": 0.5, + "patch_ib(1)%geometry": 8, + }, + ) + ) + + cases.append( + define_case_d( + stack, + f"Cuboid{suffix}", + { + "patch_ib(1)%z_centroid": 0.5, + "patch_ib(1)%length_x": 0.1, + "patch_ib(1)%length_y": 0.1, + "patch_ib(1)%length_z": 0.1, + "patch_ib(1)%geometry": 9, + }, + ) + ) + + cases.append( + define_case_d( + stack, + f"Cylinder{suffix}", + { + "patch_ib(1)%z_centroid": 0.5, + "patch_ib(1)%length_x": 0.1, + "patch_ib(1)%geometry": 10, + }, + ) + ) elif len(dimInfo[0]) == 2: - cases.append(define_case_d(stack, f'Rectangle{suffix}', { - 'patch_ib(1)%length_x': 0.05, - 'patch_ib(1)%length_y': 0.05, - 'patch_ib(1)%geometry': 3, - })) - cases.append(define_case_d(stack, f'Circle{suffix}', { - 'patch_ib(1)%geometry': 2, - 'n': 49 - })) + cases.append( + define_case_d( + stack, + f"Rectangle{suffix}", + { + "patch_ib(1)%length_x": 0.05, + "patch_ib(1)%length_y": 0.05, + "patch_ib(1)%geometry": 3, + }, + ) + ) + cases.append(define_case_d(stack, f"Circle{suffix}", {"patch_ib(1)%geometry": 2, "n": 49})) if six_eqn_model: - cases.append(define_case_d(stack, f'model_eqns=3{suffix}', { - 'patch_ib(1)%geometry': 2, - 'model_eqns': 3, - 'n': 49, # there is a machine-level precision sensitivity to circles with n=39 - })) + cases.append( + define_case_d( + stack, + f"model_eqns=3{suffix}", + { + "patch_ib(1)%geometry": 2, + "model_eqns": 3, + "n": 49, # there is a machine-level precision sensitivity to circles with n=39 + }, + ) + ) stack.pop() if len(dimInfo[0]) == 2 and not viscous: - cases.append(define_case_d(stack, 'IBM -> Periodic Circle', { - 'ib': 'T', 'num_ibs': 1, - 'bc_x%beg': -1, 'bc_x%end': -1, - 'bc_y%beg': -1, 'bc_y%end': -1, - 'patch_ib(1)%geometry': 2, - 'patch_ib(1)%x_centroid': 0., - 'patch_ib(1)%y_centroid': 0., - 'patch_ib(1)%radius': 0.1, - 'patch_icpp(1)%vel(1)': 0.001, - 'patch_icpp(2)%vel(1)': 0.001, - 'patch_icpp(3)%vel(1)': 0.001, - 'patch_ib(1)%slip': 'F', - 'n': 49 - })) + cases.append( + define_case_d( + stack, + "IBM -> Periodic Circle", + { + "ib": "T", + "num_ibs": 1, + "bc_x%beg": -1, + "bc_x%end": -1, + "bc_y%beg": -1, + "bc_y%end": -1, + "patch_ib(1)%geometry": 2, + "patch_ib(1)%x_centroid": 0.0, + "patch_ib(1)%y_centroid": 0.0, + "patch_ib(1)%radius": 0.1, + "patch_icpp(1)%vel(1)": 0.001, + "patch_icpp(2)%vel(1)": 0.001, + "patch_icpp(3)%vel(1)": 0.001, + "patch_ib(1)%slip": "F", + "n": 49, + }, + ) + ) def ibm_stl(): common_mods = { - 't_step_stop': Nt, 't_step_save': Nt, - 'patch_ib(1)%model_scale(1)': 5., - 'patch_ib(1)%model_scale(2)': 5., - 'patch_ib(1)%model_scale(3)': 5., - 'patch_ib(1)%model_threshold': 0.5, + "t_step_stop": Nt, + "t_step_save": Nt, + "patch_ib(1)%model_scale(1)": 5.0, + "patch_ib(1)%model_scale(2)": 5.0, + "patch_ib(1)%model_scale(3)": 5.0, + "patch_ib(1)%model_threshold": 0.5, } for ndim in range(2, 4): - cases.append(define_case_f( - f'{ndim}D -> IBM -> STL', - f'examples/{ndim}D_ibm_stl_test/case.py', - ['--ndim', str(ndim)], - mods=common_mods - )) + cases.append(define_case_f(f"{ndim}D -> IBM -> STL", f"examples/{ndim}D_ibm_stl_test/case.py", ["--ndim", str(ndim)], mods=common_mods)) + ibm_stl() def alter_acoustic_src(dimInfo): - stack.push("Acoustic Source", {"acoustic_source": 'T', 'acoustic(1)%support': 1, 'dt': 1e-3, 't_step_stop': 50, 't_step_save': 50}) + stack.push("Acoustic Source", {"acoustic_source": "T", "acoustic(1)%support": 1, "dt": 1e-3, "t_step_stop": 50, "t_step_save": 50}) - transducer_params = {'acoustic(1)%loc(1)': 0.2, 'acoustic(1)%foc_length': 0.4, 'acoustic(1)%aperture': 0.6} + transducer_params = {"acoustic(1)%loc(1)": 0.2, "acoustic(1)%foc_length": 0.4, "acoustic(1)%aperture": 0.6} if len(dimInfo[0]) == 1: - for pulse_type in ['Sine', 'Square']: - stack.push(pulse_type, {'acoustic(1)%pulse': 1 if pulse_type == 'Sine' else 3}) - cases.append(define_case_d(stack, 'Frequency', {'acoustic(1)%frequency': 50})) - cases.append(define_case_d(stack, 'Wavelength', {'acoustic(1)%wavelength': 0.02})) - cases.append(define_case_d(stack, 'Delay', {'acoustic(1)%delay': 0.02, 'acoustic(1)%wavelength': 0.02})) - cases.append(define_case_d(stack, 'Number of Pulses', {'acoustic(1)%npulse': 2, 'acoustic(1)%wavelength': 0.01})) + for pulse_type in ["Sine", "Square"]: + stack.push(pulse_type, {"acoustic(1)%pulse": 1 if pulse_type == "Sine" else 3}) + cases.append(define_case_d(stack, "Frequency", {"acoustic(1)%frequency": 50})) + cases.append(define_case_d(stack, "Wavelength", {"acoustic(1)%wavelength": 0.02})) + cases.append(define_case_d(stack, "Delay", {"acoustic(1)%delay": 0.02, "acoustic(1)%wavelength": 0.02})) + cases.append(define_case_d(stack, "Number of Pulses", {"acoustic(1)%npulse": 2, "acoustic(1)%wavelength": 0.01})) stack.pop() - stack.push('Gaussian', {'acoustic(1)%pulse': 2, 'acoustic(1)%delay': 0.02}) - cases.append(define_case_d(stack, 'Sigma Time', {'acoustic(1)%gauss_sigma_time': 0.01})) - cases.append(define_case_d(stack, 'Sigma Dist', {'acoustic(1)%gauss_sigma_dist': 0.01})) - cases.append(define_case_d(stack, 'Dipole', {'acoustic(1)%gauss_sigma_dist': 0.01, 'acoustic(1)%dipole': 'T'})) + stack.push("Gaussian", {"acoustic(1)%pulse": 2, "acoustic(1)%delay": 0.02}) + cases.append(define_case_d(stack, "Sigma Time", {"acoustic(1)%gauss_sigma_time": 0.01})) + cases.append(define_case_d(stack, "Sigma Dist", {"acoustic(1)%gauss_sigma_dist": 0.01})) + cases.append(define_case_d(stack, "Dipole", {"acoustic(1)%gauss_sigma_dist": 0.01, "acoustic(1)%dipole": "T"})) stack.pop() elif len(dimInfo[0]) == 2: - stack.push('', {'acoustic(1)%loc(2)': 0.5, 'acoustic(1)%wavelength': 0.02}) + stack.push("", {"acoustic(1)%loc(2)": 0.5, "acoustic(1)%wavelength": 0.02}) - stack.push('Planar', {}) - stack.push('support=2', {'acoustic(1)%support': 2}) - cases.append(define_case_d(stack, '', {})) - cases.append(define_case_d(stack, 'Dipole', {'acoustic(1)%dipole': 'T'})) + stack.push("Planar", {}) + stack.push("support=2", {"acoustic(1)%support": 2}) + cases.append(define_case_d(stack, "", {})) + cases.append(define_case_d(stack, "Dipole", {"acoustic(1)%dipole": "T"})) stack.pop() stack.pop() - stack.push('Transducer', transducer_params) + stack.push("Transducer", transducer_params) for support in [5, 6]: - stack.push(f'support={support}', {'acoustic(1)%support': support, 'cyl_coord': 'T' if support == 6 else 'F', 'bc_y%beg': -2 if support == 6 else -3}) - cases.append(define_case_d(stack, 'Sine', {})) - cases.append(define_case_d(stack, 'Gaussian', {'acoustic(1)%pulse': 2, 'acoustic(1)%delay': 0.02, 'acoustic(1)%gauss_sigma_dist': 0.01})) - cases.append(define_case_d(stack, 'Delay', {'acoustic(1)%delay': 0.02})) + stack.push(f"support={support}", {"acoustic(1)%support": support, "cyl_coord": "T" if support == 6 else "F", "bc_y%beg": -2 if support == 6 else -3}) + cases.append(define_case_d(stack, "Sine", {})) + cases.append(define_case_d(stack, "Gaussian", {"acoustic(1)%pulse": 2, "acoustic(1)%delay": 0.02, "acoustic(1)%gauss_sigma_dist": 0.01})) + cases.append(define_case_d(stack, "Delay", {"acoustic(1)%delay": 0.02})) stack.pop() stack.pop() - stack.push('Transducer Array', {**transducer_params, 'acoustic(1)%num_elements': 4, 'acoustic(1)%element_spacing_angle': 0.05, 'acoustic(1)%element_on': 0}) - stack.push('support=9', {'acoustic(1)%support': 9}) - cases.append(define_case_d(stack, 'All Elements', {})) - cases.append(define_case_d(stack, 'One element', {'acoustic(1)%element_on': 1})) + stack.push("Transducer Array", {**transducer_params, "acoustic(1)%num_elements": 4, "acoustic(1)%element_spacing_angle": 0.05, "acoustic(1)%element_on": 0}) + stack.push("support=9", {"acoustic(1)%support": 9}) + cases.append(define_case_d(stack, "All Elements", {})) + cases.append(define_case_d(stack, "One element", {"acoustic(1)%element_on": 1})) stack.pop() - cases.append(define_case_d(stack, 'support=10', {'acoustic(1)%support': 10, 'cyl_coord': 'T', 'bc_y%beg': -2})) + cases.append(define_case_d(stack, "support=10", {"acoustic(1)%support": 10, "cyl_coord": "T", "bc_y%beg": -2})) stack.pop() stack.pop() elif len(dimInfo[0]) == 3: - stack.push('', {'acoustic(1)%loc(2)': 0.5, 'acoustic(1)%loc(3)': 0.5, 'acoustic(1)%wavelength': 0.02}) + stack.push("", {"acoustic(1)%loc(2)": 0.5, "acoustic(1)%loc(3)": 0.5, "acoustic(1)%wavelength": 0.02}) - stack.push('Planar', {}) - stack.push('support=3', {'acoustic(1)%support': 3, 'acoustic(1)%height': 0.25}) - cases.append(define_case_d(stack, '', {})) - cases.append(define_case_d(stack, 'Dipole', {'acoustic(1)%dipole': 'T'})) + stack.push("Planar", {}) + stack.push("support=3", {"acoustic(1)%support": 3, "acoustic(1)%height": 0.25}) + cases.append(define_case_d(stack, "", {})) + cases.append(define_case_d(stack, "Dipole", {"acoustic(1)%dipole": "T"})) stack.pop() stack.pop() - stack.push('Transducer', transducer_params) - cases.append(define_case_d(stack, 'support=7', {'acoustic(1)%support': 7})) + stack.push("Transducer", transducer_params) + cases.append(define_case_d(stack, "support=7", {"acoustic(1)%support": 7})) stack.pop() - stack.push('Transducer Array', {**transducer_params, 'acoustic(1)%num_elements': 6, 'acoustic(1)%element_polygon_ratio': 0.7}) - stack.push('support=11', {'acoustic(1)%support': 11}) - cases.append(define_case_d(stack, 'All Elements', {})) - cases.append(define_case_d(stack, 'One element', {'acoustic(1)%element_on': 1})) + stack.push("Transducer Array", {**transducer_params, "acoustic(1)%num_elements": 6, "acoustic(1)%element_polygon_ratio": 0.7}) + stack.push("support=11", {"acoustic(1)%support": 11}) + cases.append(define_case_d(stack, "All Elements", {})) + cases.append(define_case_d(stack, "One element", {"acoustic(1)%element_on": 1})) stack.pop() stack.pop() @@ -552,72 +779,96 @@ def alter_acoustic_src(dimInfo): def alter_bubbles(dimInfo): if len(dimInfo[0]) > 0: - stack.push("Bubbles", {"bubbles_euler": 'T'}) - - stack.push('', { - 'nb': 3, 'fluid_pp(1)%gamma': 0.16, 'fluid_pp(1)%pi_inf': 3515.0, - 'bub_pp%R0ref': 1.0, 'bub_pp%p0ref': 1.0, 'bub_pp%rho0ref': 1.0, 'bub_pp%T0ref': 1.0, - 'bub_pp%ss': 0.07179866765358993, 'bub_pp%pv': 0.02308216136195411, 'bub_pp%vd': 0.2404125083932959, - 'bub_pp%mu_l': 0.009954269975623244, 'bub_pp%mu_v': 8.758168074360729e-05, - 'bub_pp%mu_g': 0.00017881922111898042, 'bub_pp%gam_v': 1.33, 'bub_pp%gam_g': 1.4, - 'bub_pp%M_v': 18.02, 'bub_pp%M_g': 28.97, 'bub_pp%k_v': 0.5583395141263873, - 'bub_pp%k_g': 0.7346421281308791, 'bub_pp%R_v': 1334.8378710170155, 'bub_pp%R_g': 830.2995663005393, - 'patch_icpp(1)%alpha_rho(1)': 0.96, 'patch_icpp(1)%alpha(1)': 4e-02, - 'patch_icpp(2)%alpha_rho(1)': 0.96, 'patch_icpp(2)%alpha(1)': 4e-02, 'patch_icpp(3)%alpha_rho(1)': 0.96, - 'patch_icpp(3)%alpha(1)': 4e-02, 'patch_icpp(1)%pres': 1.0, 'patch_icpp(2)%pres': 1.0, - 'patch_icpp(3)%pres': 1.0, 'acoustic(1)%support': 1, 'acoustic(1)%wavelength': 0.25 - }) - - stack.push('', {"acoustic_source": 'T'}) + stack.push("Bubbles", {"bubbles_euler": "T"}) + + stack.push( + "", + { + "nb": 3, + "fluid_pp(1)%gamma": 0.16, + "fluid_pp(1)%pi_inf": 3515.0, + "bub_pp%R0ref": 1.0, + "bub_pp%p0ref": 1.0, + "bub_pp%rho0ref": 1.0, + "bub_pp%T0ref": 1.0, + "bub_pp%ss": 0.07179866765358993, + "bub_pp%pv": 0.02308216136195411, + "bub_pp%vd": 0.2404125083932959, + "bub_pp%mu_l": 0.009954269975623244, + "bub_pp%mu_v": 8.758168074360729e-05, + "bub_pp%mu_g": 0.00017881922111898042, + "bub_pp%gam_v": 1.33, + "bub_pp%gam_g": 1.4, + "bub_pp%M_v": 18.02, + "bub_pp%M_g": 28.97, + "bub_pp%k_v": 0.5583395141263873, + "bub_pp%k_g": 0.7346421281308791, + "bub_pp%R_v": 1334.8378710170155, + "bub_pp%R_g": 830.2995663005393, + "patch_icpp(1)%alpha_rho(1)": 0.96, + "patch_icpp(1)%alpha(1)": 4e-02, + "patch_icpp(2)%alpha_rho(1)": 0.96, + "patch_icpp(2)%alpha(1)": 4e-02, + "patch_icpp(3)%alpha_rho(1)": 0.96, + "patch_icpp(3)%alpha(1)": 4e-02, + "patch_icpp(1)%pres": 1.0, + "patch_icpp(2)%pres": 1.0, + "patch_icpp(3)%pres": 1.0, + "acoustic(1)%support": 1, + "acoustic(1)%wavelength": 0.25, + }, + ) + + stack.push("", {"acoustic_source": "T"}) if len(dimInfo[0]) >= 2: - stack.push("", {'acoustic(1)%loc(2)': 0.5, 'acoustic(1)%support': 2}) + stack.push("", {"acoustic(1)%loc(2)": 0.5, "acoustic(1)%support": 2}) if len(dimInfo[0]) >= 3: - stack.push("", {'acoustic(1)%support': 3, 'acoustic(1)%height': 1e10}) + stack.push("", {"acoustic(1)%support": 3, "acoustic(1)%height": 1e10}) - for polytropic in ['T', 'F']: - stack.push("Polytropic" if polytropic == 'T' else '', {'polytropic': polytropic}) + for polytropic in ["T", "F"]: + stack.push("Polytropic" if polytropic == "T" else "", {"polytropic": polytropic}) for bubble_model in [3, 2]: - stack.push(f"bubble_model={bubble_model}", {'bubble_model': bubble_model}) + stack.push(f"bubble_model={bubble_model}", {"bubble_model": bubble_model}) - if not (polytropic == 'F' and bubble_model == 3): - cases.append(define_case_d(stack, '', {})) + if not (polytropic == "F" and bubble_model == 3): + cases.append(define_case_d(stack, "", {})) stack.pop() stack.pop() - stack.push('', {'polytropic': 'T', 'bubble_model': 2}) - cases.append(define_case_d(stack, 'nb=1', {'nb': 1})) + stack.push("", {"polytropic": "T", "bubble_model": 2}) + cases.append(define_case_d(stack, "nb=1", {"nb": 1})) - stack.push("adv_n=T", {'adv_n': 'T'}) - cases.append(define_case_d(stack, '', {})) - cases.append(define_case_d(stack, 'adap_dt=T', {'adap_dt': 'T'})) + stack.push("adv_n=T", {"adv_n": "T"}) + cases.append(define_case_d(stack, "", {})) + cases.append(define_case_d(stack, "adap_dt=T", {"adap_dt": "T"})) stack.pop() - stack.push('', {'fluid_pp(1)%pi_inf': 351.5}) - cases.append(define_case_d(stack, 'artificial_Ma', {'pi_fac': 0.1})) + stack.push("", {"fluid_pp(1)%pi_inf": 351.5}) + cases.append(define_case_d(stack, "artificial_Ma", {"pi_fac": 0.1})) stack.pop() - cases.append(define_case_d(stack, 'low_Mach=1', {'low_Mach': 1})) - cases.append(define_case_d(stack, 'low_Mach=2', {'low_Mach': 2})) + cases.append(define_case_d(stack, "low_Mach=1", {"low_Mach": 1})) + cases.append(define_case_d(stack, "low_Mach=2", {"low_Mach": 2})) - stack.push("QBMM", {'qbmm': 'T'}) - cases.append(define_case_d(stack, '', {})) + stack.push("QBMM", {"qbmm": "T"}) + cases.append(define_case_d(stack, "", {})) - stack.push("Non-polytropic", {'polytropic': 'F'}) - cases.append(define_case_d(stack, '', {})) + stack.push("Non-polytropic", {"polytropic": "F"}) + cases.append(define_case_d(stack, "", {})) stack.pop() - stack.push('bubble_model=3', {'bubble_model': 3, 'polytropic': 'T'}) - cases.append(define_case_d(stack, '', {})) + stack.push("bubble_model=3", {"bubble_model": 3, "polytropic": "T"}) + cases.append(define_case_d(stack, "", {})) - stack.push('Non-polytropic', {'polytropic': 'F'}) - cases.append(define_case_d(stack, '', {})) + stack.push("Non-polytropic", {"polytropic": "F"}) + cases.append(define_case_d(stack, "", {})) for _ in range(7): stack.pop() @@ -631,54 +882,91 @@ def alter_bubbles(dimInfo): def alter_hypoelasticity(dimInfo): # Hypoelasticity checks for num_fluids in [1, 2]: - stack.push(f"Hypoelasticity -> {num_fluids} Fluid(s)", { - "hypoelasticity": 'T', "num_fluids": num_fluids, - 'riemann_solver': 1, - 'fd_order': 4, - 'fluid_pp(1)%gamma': 0.3, 'fluid_pp(1)%pi_inf': 7.8E+05, - 'patch_icpp(1)%pres': 1.E+06, 'patch_icpp(1)%alpha_rho(1)': 1000.E+00, - 'patch_icpp(2)%pres': 1.E+05, 'patch_icpp(2)%alpha_rho(1)': 1000.E+00, - 'patch_icpp(3)%pres': 5.E+05, 'patch_icpp(3)%alpha_rho(1)': 1000.E+00, - 'patch_icpp(1)%tau_e(1)': 0.E-00, 'patch_icpp(2)%tau_e(1)': 0.E-00, - 'patch_icpp(3)%tau_e(1)': 0.E-00, 'fluid_pp(1)%G': 1.E+05, - }) + stack.push( + f"Hypoelasticity -> {num_fluids} Fluid(s)", + { + "hypoelasticity": "T", + "num_fluids": num_fluids, + "riemann_solver": 1, + "fd_order": 4, + "fluid_pp(1)%gamma": 0.3, + "fluid_pp(1)%pi_inf": 7.8e05, + "patch_icpp(1)%pres": 1.0e06, + "patch_icpp(1)%alpha_rho(1)": 1000.0e00, + "patch_icpp(2)%pres": 1.0e05, + "patch_icpp(2)%alpha_rho(1)": 1000.0e00, + "patch_icpp(3)%pres": 5.0e05, + "patch_icpp(3)%alpha_rho(1)": 1000.0e00, + "patch_icpp(1)%tau_e(1)": 0.0e-00, + "patch_icpp(2)%tau_e(1)": 0.0e-00, + "patch_icpp(3)%tau_e(1)": 0.0e-00, + "fluid_pp(1)%G": 1.0e05, + }, + ) if num_fluids == 2: - stack.push("", { - 'fluid_pp(2)%gamma': 0.3, 'fluid_pp(2)%pi_inf': 7.8E+05, 'patch_icpp(1)%alpha_rho(1)': 900.E+00, - 'patch_icpp(1)%alpha(1)': 0.9, 'patch_icpp(1)%alpha_rho(2)': 100, 'patch_icpp(1)%alpha(2)': 0.1, - 'patch_icpp(2)%alpha_rho(1)': 100, 'patch_icpp(2)%alpha(1)': 0.1, 'patch_icpp(2)%alpha_rho(2)': 900, - 'patch_icpp(2)%alpha(2)': 0.9, 'patch_icpp(3)%alpha_rho(1)': 900, 'patch_icpp(3)%alpha(1)': 0.9, - 'patch_icpp(3)%alpha_rho(2)': 100, 'patch_icpp(3)%alpha(2)': 0.1, - 'fluid_pp(2)%G': 5.E+04 - }) + stack.push( + "", + { + "fluid_pp(2)%gamma": 0.3, + "fluid_pp(2)%pi_inf": 7.8e05, + "patch_icpp(1)%alpha_rho(1)": 900.0e00, + "patch_icpp(1)%alpha(1)": 0.9, + "patch_icpp(1)%alpha_rho(2)": 100, + "patch_icpp(1)%alpha(2)": 0.1, + "patch_icpp(2)%alpha_rho(1)": 100, + "patch_icpp(2)%alpha(1)": 0.1, + "patch_icpp(2)%alpha_rho(2)": 900, + "patch_icpp(2)%alpha(2)": 0.9, + "patch_icpp(3)%alpha_rho(1)": 900, + "patch_icpp(3)%alpha(1)": 0.9, + "patch_icpp(3)%alpha_rho(2)": 100, + "patch_icpp(3)%alpha(2)": 0.1, + "fluid_pp(2)%G": 5.0e04, + }, + ) if len(dimInfo[0]) >= 2: - stack.push("", { - 'patch_icpp(1)%tau_e(2)': 0.E+00, 'patch_icpp(1)%tau_e(3)': 0.0E+00, - 'patch_icpp(2)%tau_e(2)': 0.E+00, 'patch_icpp(2)%tau_e(3)': 0.0E+00, - 'patch_icpp(3)%tau_e(2)': 0.E+00, 'patch_icpp(3)%tau_e(3)': 0.0E+00 - }) + stack.push( + "", + { + "patch_icpp(1)%tau_e(2)": 0.0e00, + "patch_icpp(1)%tau_e(3)": 0.0e00, + "patch_icpp(2)%tau_e(2)": 0.0e00, + "patch_icpp(2)%tau_e(3)": 0.0e00, + "patch_icpp(3)%tau_e(2)": 0.0e00, + "patch_icpp(3)%tau_e(3)": 0.0e00, + }, + ) if len(dimInfo[0]) == 3: - stack.push("", { - 'patch_icpp(1)%tau_e(4)': 0.E+00, 'patch_icpp(1)%tau_e(5)': 0.0E+00, 'patch_icpp(1)%tau_e(6)': 0.0E+00, - 'patch_icpp(2)%tau_e(4)': 0.E+00, 'patch_icpp(2)%tau_e(5)': 0.0E+00, 'patch_icpp(2)%tau_e(6)': 0.0E+00, - 'patch_icpp(3)%tau_e(4)': 0.E+00, 'patch_icpp(3)%tau_e(5)': 0.0E+00, 'patch_icpp(3)%tau_e(6)': 0.0E+00 - }) - - cases.append(define_case_d(stack, '', {})) - - reflective_params = {'bc_x%beg': -2, 'bc_x%end': -2, 'bc_y%beg': -2, 'bc_y%end': -2} + stack.push( + "", + { + "patch_icpp(1)%tau_e(4)": 0.0e00, + "patch_icpp(1)%tau_e(5)": 0.0e00, + "patch_icpp(1)%tau_e(6)": 0.0e00, + "patch_icpp(2)%tau_e(4)": 0.0e00, + "patch_icpp(2)%tau_e(5)": 0.0e00, + "patch_icpp(2)%tau_e(6)": 0.0e00, + "patch_icpp(3)%tau_e(4)": 0.0e00, + "patch_icpp(3)%tau_e(5)": 0.0e00, + "patch_icpp(3)%tau_e(6)": 0.0e00, + }, + ) + + cases.append(define_case_d(stack, "", {})) + + reflective_params = {"bc_x%beg": -2, "bc_x%end": -2, "bc_y%beg": -2, "bc_y%end": -2} if len(dimInfo[0]) == 3: - reflective_params.update({'bc_z%beg': -2, 'bc_z%end': -2}) + reflective_params.update({"bc_z%beg": -2, "bc_z%end": -2}) if num_fluids == 1: - cases.append(define_case_d(stack, 'cont_damage', {'cont_damage': 'T', 'tau_star': 0.0, 'cont_damage_s': 2.0, 'alpha_bar': 1e-4})) + cases.append(define_case_d(stack, "cont_damage", {"cont_damage": "T", "tau_star": 0.0, "cont_damage_s": 2.0, "alpha_bar": 1e-4})) if len(dimInfo[0]) >= 2: - cases.append(define_case_d(stack, 'bc=-2', reflective_params)) + cases.append(define_case_d(stack, "bc=-2", reflective_params)) if len(dimInfo[0]) == 2: - cases.append(define_case_d(stack, 'Axisymmetric', {**reflective_params, 'cyl_coord': 'T'})) + cases.append(define_case_d(stack, "Axisymmetric", {**reflective_params, "cyl_coord": "T"})) stack.pop() @@ -695,24 +983,18 @@ def alter_hypoelasticity(dimInfo): def alter_body_forces(dimInfo): ndims = len(dimInfo[0]) - stack.push("Bodyforces", { - 'bf_x': 'T', 'k_x': 1, 'w_x': 1, 'p_x': 1, 'g_x': 10 - }) + stack.push("Bodyforces", {"bf_x": "T", "k_x": 1, "w_x": 1, "p_x": 1, "g_x": 10}) if ndims >= 2: - stack.push("", { - 'bf_y': 'T', 'k_y': 1, 'w_y': 1, 'p_y': 1, 'g_y': 10 - }) + stack.push("", {"bf_y": "T", "k_y": 1, "w_y": 1, "p_y": 1, "g_y": 10}) if ndims == 3: - stack.push("", { - 'bf_z': 'T', 'k_z': 1, 'w_z': 1, 'p_z': 1, 'g_z': 10 - }) + stack.push("", {"bf_z": "T", "k_z": 1, "w_z": 1, "p_z": 1, "g_z": 10}) - cases.append(define_case_d(stack, '', {})) + cases.append(define_case_d(stack, "", {})) - stack.push('cfl_adap_dt=T', {'cfl_adap_dt': 'T', 'cfl_target': 0.08, 't_save': 0.025, 'n_start': 0, 't_stop': 0.025}) - cases.append(define_case_d(stack, '', {})) + stack.push("cfl_adap_dt=T", {"cfl_adap_dt": "T", "cfl_target": 0.08, "t_save": 0.025, "n_start": 0, "t_stop": 0.025}) + cases.append(define_case_d(stack, "", {})) stack.pop() @@ -726,36 +1008,80 @@ def alter_body_forces(dimInfo): def alter_mixlayer_perturb(dimInfo): if len(dimInfo[0]) == 3: - cases.append(define_case_d(stack, 'mixlayer_perturb', { - 'm': 24, 'n': 64, 'p': 24, 'dt': 1e-2, - 'num_patches': 1, 'num_fluids': 1, - 'x_domain%beg': 0.0, 'x_domain%end': 20.0, 'bc_x%beg': -1, 'bc_x%end': -1, - 'y_domain%beg': -10.0, 'y_domain%end': 10.0, 'bc_y%beg': -6, 'bc_y%end': -6, - 'z_domain%beg': 0.0, 'z_domain%end': 20.0, 'bc_z%beg': -1, 'bc_z%end': -1, - 'mixlayer_vel_profile': 'T', 'mixlayer_perturb': 'T', - 'weno_Re_flux': 'F', 'weno_avg': 'T', 'wenoz': 'T', - 'fluid_pp(1)%gamma': 2.5, 'fluid_pp(1)%pi_inf': 0.0, - 'fluid_pp(1)%Re(1)': 1.6881644098979287, 'viscous': 'T', - 'patch_icpp(1)%geometry': 9, - 'patch_icpp(1)%x_centroid': 10.0, 'patch_icpp(1)%length_x': 20.0, - 'patch_icpp(1)%y_centroid': 0.0, 'patch_icpp(1)%length_y': 20.0, - 'patch_icpp(1)%z_centroid': 10.0, 'patch_icpp(1)%length_z': 20.0, - 'patch_icpp(1)%vel(1)': 1.0, 'patch_icpp(1)%vel(2)': 0.0, 'patch_icpp(1)%vel(3)': 0.0, - 'patch_icpp(1)%pres': 17.8571428571, 'patch_icpp(1)%alpha_rho(1)': 1.0, 'patch_icpp(1)%alpha(1)': 1.0, - 'patch_icpp(1)%r0': -1e6, 'patch_icpp(1)%v0': -1e6, - 'patch_icpp(2)%geometry': -100, - 'patch_icpp(2)%x_centroid': -1e6, 'patch_icpp(2)%length_x': -1e6, - 'patch_icpp(2)%y_centroid': -1e6, 'patch_icpp(2)%length_y': -1e6, - 'patch_icpp(2)%z_centroid': -1e6, 'patch_icpp(2)%length_z': -1e6, - 'patch_icpp(2)%vel(1)': -1e6, 'patch_icpp(2)%vel(2)': -1e6, 'patch_icpp(2)%vel(3)': -1e6, - 'patch_icpp(2)%r0': -1e6, 'patch_icpp(2)%v0': -1e6, - 'patch_icpp(3)%geometry': -100, - 'patch_icpp(3)%x_centroid': -1e6, 'patch_icpp(3)%length_x': -1e6, - 'patch_icpp(3)%y_centroid': -1e6, 'patch_icpp(3)%length_y': -1e6, - 'patch_icpp(3)%z_centroid': -1e6, 'patch_icpp(3)%length_z': -1e6, - 'patch_icpp(3)%vel(1)': -1e6, 'patch_icpp(3)%vel(2)': -1e6, 'patch_icpp(3)%vel(3)': -1e6, - 'patch_icpp(3)%r0': -1e6, 'patch_icpp(3)%v0': -1e6 - })) + cases.append( + define_case_d( + stack, + "mixlayer_perturb", + { + "m": 24, + "n": 64, + "p": 24, + "dt": 1e-2, + "num_patches": 1, + "num_fluids": 1, + "x_domain%beg": 0.0, + "x_domain%end": 20.0, + "bc_x%beg": -1, + "bc_x%end": -1, + "y_domain%beg": -10.0, + "y_domain%end": 10.0, + "bc_y%beg": -6, + "bc_y%end": -6, + "z_domain%beg": 0.0, + "z_domain%end": 20.0, + "bc_z%beg": -1, + "bc_z%end": -1, + "mixlayer_vel_profile": "T", + "mixlayer_perturb": "T", + "weno_Re_flux": "F", + "weno_avg": "T", + "wenoz": "T", + "fluid_pp(1)%gamma": 2.5, + "fluid_pp(1)%pi_inf": 0.0, + "fluid_pp(1)%Re(1)": 1.6881644098979287, + "viscous": "T", + "patch_icpp(1)%geometry": 9, + "patch_icpp(1)%x_centroid": 10.0, + "patch_icpp(1)%length_x": 20.0, + "patch_icpp(1)%y_centroid": 0.0, + "patch_icpp(1)%length_y": 20.0, + "patch_icpp(1)%z_centroid": 10.0, + "patch_icpp(1)%length_z": 20.0, + "patch_icpp(1)%vel(1)": 1.0, + "patch_icpp(1)%vel(2)": 0.0, + "patch_icpp(1)%vel(3)": 0.0, + "patch_icpp(1)%pres": 17.8571428571, + "patch_icpp(1)%alpha_rho(1)": 1.0, + "patch_icpp(1)%alpha(1)": 1.0, + "patch_icpp(1)%r0": -1e6, + "patch_icpp(1)%v0": -1e6, + "patch_icpp(2)%geometry": -100, + "patch_icpp(2)%x_centroid": -1e6, + "patch_icpp(2)%length_x": -1e6, + "patch_icpp(2)%y_centroid": -1e6, + "patch_icpp(2)%length_y": -1e6, + "patch_icpp(2)%z_centroid": -1e6, + "patch_icpp(2)%length_z": -1e6, + "patch_icpp(2)%vel(1)": -1e6, + "patch_icpp(2)%vel(2)": -1e6, + "patch_icpp(2)%vel(3)": -1e6, + "patch_icpp(2)%r0": -1e6, + "patch_icpp(2)%v0": -1e6, + "patch_icpp(3)%geometry": -100, + "patch_icpp(3)%x_centroid": -1e6, + "patch_icpp(3)%length_x": -1e6, + "patch_icpp(3)%y_centroid": -1e6, + "patch_icpp(3)%length_y": -1e6, + "patch_icpp(3)%z_centroid": -1e6, + "patch_icpp(3)%length_z": -1e6, + "patch_icpp(3)%vel(1)": -1e6, + "patch_icpp(3)%vel(2)": -1e6, + "patch_icpp(3)%vel(3)": -1e6, + "patch_icpp(3)%r0": -1e6, + "patch_icpp(3)%v0": -1e6, + }, + ) + ) def alter_phasechange(dimInfo): ndims = len(dimInfo[0]) @@ -764,61 +1090,99 @@ def alter_phasechange(dimInfo): for relax_model in [5] + ([6] if ndims <= 2 else []): for num_fluids in ([2] if ndims == 1 or relax_model == 5 else []) + [3]: for model_eqns in [3, 2]: - stack.push(f"Phase Change model {relax_model} -> {num_fluids} Fluid(s) -> model equation -> {model_eqns}", { - "relax": 'T', - "relax_model": relax_model, - 'model_eqns': model_eqns, - 'palpha_eps': 1E-02, - 'ptgalpha_eps': 1E-02, - "num_fluids": num_fluids, - 'riemann_solver': 2, - 'fluid_pp(1)%gamma': 0.7409, 'fluid_pp(1)%pi_inf': 1.7409E+09, - 'fluid_pp(1)%cv': 1816, 'fluid_pp(1)%qv': -1167000, - 'fluid_pp(1)%qvp': 0.0, - 'fluid_pp(2)%gamma': 2.3266, 'fluid_pp(2)%pi_inf': 0.0E+00, - 'fluid_pp(2)%cv': 1040, 'fluid_pp(2)%qv': 2030000, - 'fluid_pp(2)%qvp': -23400, - 'patch_icpp(1)%pres': 4.3755E+05, - 'patch_icpp(1)%alpha(1)': 8.7149E-06, 'patch_icpp(1)%alpha_rho(1)': 9.6457E+02 * 8.7149E-06, - 'patch_icpp(1)%alpha(2)': 1-8.7149E-06, 'patch_icpp(1)%alpha_rho(2)': 2.3132 * (1 - 8.7149E-06), - 'patch_icpp(2)%pres': 9.6602E+04, - 'patch_icpp(2)%alpha(1)': 3.6749E-05, 'patch_icpp(2)%alpha_rho(1)': 1.0957E+03 * 3.6749E-05, - 'patch_icpp(2)%alpha(2)': 1-3.6749E-05, 'patch_icpp(2)%alpha_rho(2)': 0.5803 * (1 - 3.6749E-05), - 'patch_icpp(3)%pres': 9.6602E+04, - 'patch_icpp(3)%alpha(1)': 3.6749E-05, 'patch_icpp(3)%alpha_rho(1)': 1.0957E+03 * 3.6749E-05, - 'patch_icpp(3)%alpha(2)': 1-3.6749E-05, 'patch_icpp(3)%alpha_rho(2)': 0.5803 * (1 - 3.6749E-05) - }) + stack.push( + f"Phase Change model {relax_model} -> {num_fluids} Fluid(s) -> model equation -> {model_eqns}", + { + "relax": "T", + "relax_model": relax_model, + "model_eqns": model_eqns, + "palpha_eps": 1e-02, + "ptgalpha_eps": 1e-02, + "num_fluids": num_fluids, + "riemann_solver": 2, + "fluid_pp(1)%gamma": 0.7409, + "fluid_pp(1)%pi_inf": 1.7409e09, + "fluid_pp(1)%cv": 1816, + "fluid_pp(1)%qv": -1167000, + "fluid_pp(1)%qvp": 0.0, + "fluid_pp(2)%gamma": 2.3266, + "fluid_pp(2)%pi_inf": 0.0e00, + "fluid_pp(2)%cv": 1040, + "fluid_pp(2)%qv": 2030000, + "fluid_pp(2)%qvp": -23400, + "patch_icpp(1)%pres": 4.3755e05, + "patch_icpp(1)%alpha(1)": 8.7149e-06, + "patch_icpp(1)%alpha_rho(1)": 9.6457e02 * 8.7149e-06, + "patch_icpp(1)%alpha(2)": 1 - 8.7149e-06, + "patch_icpp(1)%alpha_rho(2)": 2.3132 * (1 - 8.7149e-06), + "patch_icpp(2)%pres": 9.6602e04, + "patch_icpp(2)%alpha(1)": 3.6749e-05, + "patch_icpp(2)%alpha_rho(1)": 1.0957e03 * 3.6749e-05, + "patch_icpp(2)%alpha(2)": 1 - 3.6749e-05, + "patch_icpp(2)%alpha_rho(2)": 0.5803 * (1 - 3.6749e-05), + "patch_icpp(3)%pres": 9.6602e04, + "patch_icpp(3)%alpha(1)": 3.6749e-05, + "patch_icpp(3)%alpha_rho(1)": 1.0957e03 * 3.6749e-05, + "patch_icpp(3)%alpha(2)": 1 - 3.6749e-05, + "patch_icpp(3)%alpha_rho(2)": 0.5803 * (1 - 3.6749e-05), + }, + ) if num_fluids == 3: - stack.push("", { - 'fluid_pp(3)%gamma': 2.4870, 'fluid_pp(3)%pi_inf': 0.0E+00, - 'fluid_pp(3)%cv': 717.5, 'fluid_pp(3)%qv': 0.0E+00, - 'fluid_pp(3)%qvp': 0.0, - 'patch_icpp(1)%alpha(2)': 2.5893E-02, 'patch_icpp(1)%alpha_rho(2)': 2.3132 * 2.5893E-02, - 'patch_icpp(2)%alpha(2)': 2.8728E-02, 'patch_icpp(2)%alpha_rho(2)': 0.5803 * 2.8728E-02, - 'patch_icpp(3)%alpha(2)': 2.8728E-02, 'patch_icpp(3)%alpha_rho(2)': 0.5803 * 2.8728E-02, - 'patch_icpp(1)%alpha(3)': 1-8.7149E-06-2.5893E-02, 'patch_icpp(1)%alpha_rho(3)': 3.5840 * (1-8.7149E-06-2.5893E-02), - 'patch_icpp(2)%alpha(3)': 1-3.6749E-05-2.8728E-02, 'patch_icpp(2)%alpha_rho(3)': 0.8991 * (1-3.6749E-05-2.8728E-02), - 'patch_icpp(3)%alpha(3)': 1-3.6749E-05-2.8728E-02, 'patch_icpp(3)%alpha_rho(3)': 0.8991 * (1-3.6749E-05-2.8728E-02) - }) + stack.push( + "", + { + "fluid_pp(3)%gamma": 2.4870, + "fluid_pp(3)%pi_inf": 0.0e00, + "fluid_pp(3)%cv": 717.5, + "fluid_pp(3)%qv": 0.0e00, + "fluid_pp(3)%qvp": 0.0, + "patch_icpp(1)%alpha(2)": 2.5893e-02, + "patch_icpp(1)%alpha_rho(2)": 2.3132 * 2.5893e-02, + "patch_icpp(2)%alpha(2)": 2.8728e-02, + "patch_icpp(2)%alpha_rho(2)": 0.5803 * 2.8728e-02, + "patch_icpp(3)%alpha(2)": 2.8728e-02, + "patch_icpp(3)%alpha_rho(2)": 0.5803 * 2.8728e-02, + "patch_icpp(1)%alpha(3)": 1 - 8.7149e-06 - 2.5893e-02, + "patch_icpp(1)%alpha_rho(3)": 3.5840 * (1 - 8.7149e-06 - 2.5893e-02), + "patch_icpp(2)%alpha(3)": 1 - 3.6749e-05 - 2.8728e-02, + "patch_icpp(2)%alpha_rho(3)": 0.8991 * (1 - 3.6749e-05 - 2.8728e-02), + "patch_icpp(3)%alpha(3)": 1 - 3.6749e-05 - 2.8728e-02, + "patch_icpp(3)%alpha_rho(3)": 0.8991 * (1 - 3.6749e-05 - 2.8728e-02), + }, + ) if ndims == 1: - stack.push("", { - 'patch_icpp(1)%vel(1)': 606.15, 'patch_icpp(2)%vel(1)': 10.0, 'patch_icpp(3)%vel(1)': 10.0 - }) + stack.push("", {"patch_icpp(1)%vel(1)": 606.15, "patch_icpp(2)%vel(1)": 10.0, "patch_icpp(3)%vel(1)": 10.0}) elif ndims == 2: - stack.push("", { - 'patch_icpp(1)%vel(1)': 0.0, 'patch_icpp(2)%vel(1)': 0.0, 'patch_icpp(3)%vel(1)': 0.0, - 'patch_icpp(1)%vel(2)': 606.15, 'patch_icpp(2)%vel(2)': 10.0, 'patch_icpp(3)%vel(2)': 10.0 - }) + stack.push( + "", + { + "patch_icpp(1)%vel(1)": 0.0, + "patch_icpp(2)%vel(1)": 0.0, + "patch_icpp(3)%vel(1)": 0.0, + "patch_icpp(1)%vel(2)": 606.15, + "patch_icpp(2)%vel(2)": 10.0, + "patch_icpp(3)%vel(2)": 10.0, + }, + ) elif ndims == 3: - stack.push("", { - 'patch_icpp(1)%vel(1)': 0.0, 'patch_icpp(2)%vel(1)': 0.0, 'patch_icpp(3)%vel(1)': 0.0, - 'patch_icpp(1)%vel(2)': 0.0, 'patch_icpp(2)%vel(2)': 0.0, 'patch_icpp(3)%vel(2)': 0.0, - 'patch_icpp(1)%vel(3)': 606.15, 'patch_icpp(2)%vel(3)': 10.0, 'patch_icpp(3)%vel(3)': 10.0 - }) - - cases.append(define_case_d(stack, '', {})) + stack.push( + "", + { + "patch_icpp(1)%vel(1)": 0.0, + "patch_icpp(2)%vel(1)": 0.0, + "patch_icpp(3)%vel(1)": 0.0, + "patch_icpp(1)%vel(2)": 0.0, + "patch_icpp(2)%vel(2)": 0.0, + "patch_icpp(3)%vel(2)": 0.0, + "patch_icpp(1)%vel(3)": 606.15, + "patch_icpp(2)%vel(3)": 10.0, + "patch_icpp(3)%vel(3)": 10.0, + }, + ) + + cases.append(define_case_d(stack, "", {})) stack.pop() stack.pop() @@ -829,52 +1193,73 @@ def alter_phasechange(dimInfo): def alter_viscosity(dimInfo): # Viscosity & bubbles checks if len(dimInfo[0]) > 0: - stack.push("Viscosity -> Bubbles", - {"fluid_pp(1)%Re(1)": 50, "bubbles_euler": 'T', "viscous": 'T'}) - - stack.push('', { - 'nb': 1, 'fluid_pp(1)%gamma': 0.16, 'fluid_pp(1)%pi_inf': 3515.0, - 'bub_pp%R0ref': 1.0, 'bub_pp%p0ref': 1.0, 'bub_pp%rho0ref': 1.0, 'bub_pp%T0ref': 1.0, - 'bub_pp%ss': 0.07179866765358993, 'bub_pp%pv': 0.02308216136195411, 'bub_pp%vd': 0.2404125083932959, - 'bub_pp%mu_l': 0.009954269975623244, 'bub_pp%mu_v': 8.758168074360729e-05, - 'bub_pp%mu_g': 0.00017881922111898042, 'bub_pp%gam_v': 1.33, 'bub_pp%gam_g': 1.4, - 'bub_pp%M_v': 18.02, 'bub_pp%M_g': 28.97, 'bub_pp%k_v': 0.5583395141263873, - 'bub_pp%k_g': 0.7346421281308791, 'bub_pp%R_v': 1334.8378710170155, 'bub_pp%R_g': 830.2995663005393, - 'patch_icpp(1)%alpha_rho(1)': 0.96, 'patch_icpp(1)%alpha(1)': 4e-02, - 'patch_icpp(2)%alpha_rho(1)': 0.96, 'patch_icpp(2)%alpha(1)': 4e-02, 'patch_icpp(3)%alpha_rho(1)': 0.96, - 'patch_icpp(3)%alpha(1)': 4e-02, 'patch_icpp(1)%pres': 1.0, 'patch_icpp(2)%pres': 1.0, - 'patch_icpp(3)%pres': 1.0 - }) - - for polytropic in ['T', 'F']: - stack.push("Polytropic" if polytropic == 'T' else '', {'polytropic': polytropic}) + stack.push("Viscosity -> Bubbles", {"fluid_pp(1)%Re(1)": 50, "bubbles_euler": "T", "viscous": "T"}) + + stack.push( + "", + { + "nb": 1, + "fluid_pp(1)%gamma": 0.16, + "fluid_pp(1)%pi_inf": 3515.0, + "bub_pp%R0ref": 1.0, + "bub_pp%p0ref": 1.0, + "bub_pp%rho0ref": 1.0, + "bub_pp%T0ref": 1.0, + "bub_pp%ss": 0.07179866765358993, + "bub_pp%pv": 0.02308216136195411, + "bub_pp%vd": 0.2404125083932959, + "bub_pp%mu_l": 0.009954269975623244, + "bub_pp%mu_v": 8.758168074360729e-05, + "bub_pp%mu_g": 0.00017881922111898042, + "bub_pp%gam_v": 1.33, + "bub_pp%gam_g": 1.4, + "bub_pp%M_v": 18.02, + "bub_pp%M_g": 28.97, + "bub_pp%k_v": 0.5583395141263873, + "bub_pp%k_g": 0.7346421281308791, + "bub_pp%R_v": 1334.8378710170155, + "bub_pp%R_g": 830.2995663005393, + "patch_icpp(1)%alpha_rho(1)": 0.96, + "patch_icpp(1)%alpha(1)": 4e-02, + "patch_icpp(2)%alpha_rho(1)": 0.96, + "patch_icpp(2)%alpha(1)": 4e-02, + "patch_icpp(3)%alpha_rho(1)": 0.96, + "patch_icpp(3)%alpha(1)": 4e-02, + "patch_icpp(1)%pres": 1.0, + "patch_icpp(2)%pres": 1.0, + "patch_icpp(3)%pres": 1.0, + }, + ) + + for polytropic in ["T", "F"]: + stack.push("Polytropic" if polytropic == "T" else "", {"polytropic": polytropic}) for bubble_model in [3, 2]: - stack.push(f"bubble_model={bubble_model}", {'bubble_model': bubble_model}) + stack.push(f"bubble_model={bubble_model}", {"bubble_model": bubble_model}) - if not (polytropic == 'F' and bubble_model == 3): - cases.append(define_case_d(stack, '', {})) + if not (polytropic == "F" and bubble_model == 3): + cases.append(define_case_d(stack, "", {})) stack.pop() stack.pop() - stack.push('', {'polytropic': 'T', 'bubble_model': 2}) - cases.append(define_case_d(stack, 'nb=1', {'nb': 1})) + stack.push("", {"polytropic": "T", "bubble_model": 2}) + cases.append(define_case_d(stack, "nb=1", {"nb": 1})) - stack.push("QBMM", {'qbmm': 'T'}) - cases.append(define_case_d(stack, '', {})) + stack.push("QBMM", {"qbmm": "T"}) + cases.append(define_case_d(stack, "", {})) - stack.push('bubble_model=3', {'bubble_model': 3}) - cases.append(define_case_d(stack, '', {})) + stack.push("bubble_model=3", {"bubble_model": 3}) + cases.append(define_case_d(stack, "", {})) - stack.push('cfl_adap_dt=T', {'cfl_adap_dt': 'T', 'cfl_target': 0.8, 't_save': 0.01, 'n_start': 0, 't_stop': 0.01, 'm': 24}) - cases.append(define_case_d(stack, '', {})) + stack.push("cfl_adap_dt=T", {"cfl_adap_dt": "T", "cfl_target": 0.8, "t_save": 0.01, "n_start": 0, "t_stop": 0.01, "m": 24}) + cases.append(define_case_d(stack, "", {})) stack.pop() - stack.push('cfl_const_dt=T', {'cfl_const_dt': 'T', 'cfl_target': 0.8, 't_save': 0.01, 'n_start': 0, 't_stop': 0.01, 'm': 24}) - cases.append(define_case_d(stack, '', {})) + stack.push("cfl_const_dt=T", {"cfl_const_dt": "T", "cfl_target": 0.8, "t_save": 0.01, "n_start": 0, "t_stop": 0.01, "m": 24}) + cases.append(define_case_d(stack, "", {})) for _ in range(6): stack.pop() @@ -882,50 +1267,102 @@ def alter_viscosity(dimInfo): def alter_lag_bubbles(dimInfo): # Lagrangian bubbles if len(dimInfo[0]) > 1: - for adap_dt in ['F', 'T']: + for adap_dt in ["F", "T"]: for couplingMethod in [1, 2]: - stack.push("Lagrange Bubbles", {"bubbles_lagrange": 'T', - 'dt': 1e-06, 'lag_params%pressure_corrector': 'T', 'bubble_model': 2, - 'num_fluids': 2, 'lag_params%heatTransfer_model': 'T', 'lag_params%massTransfer_model': 'T', - 'fluid_pp(1)%gamma': 0.16, 'fluid_pp(1)%pi_inf': 3515.0, 'fluid_pp(2)%gamma': 2.5, - 'fluid_pp(2)%pi_inf': 0.0, - 'patch_icpp(1)%alpha_rho(1)': 0.96, - 'patch_icpp(1)%alpha(1)': 4e-02, 'patch_icpp(1)%alpha_rho(2)': 0., 'patch_icpp(1)%alpha(2)': 0., - 'patch_icpp(2)%alpha_rho(1)': 0.96, 'patch_icpp(2)%alpha(1)': 4e-02, 'patch_icpp(2)%alpha_rho(2)': 0., - 'patch_icpp(2)%alpha(2)': 0., 'patch_icpp(3)%alpha_rho(1)': 0.96, 'patch_icpp(3)%alpha(1)': 4e-02, - 'patch_icpp(3)%alpha_rho(2)': 0., 'patch_icpp(3)%alpha(2)': 0., 'patch_icpp(1)%pres': 1.0, - 'patch_icpp(2)%pres': 1.0, 'patch_icpp(3)%pres': 1.0, 'acoustic_source': 'T', 'acoustic(1)%loc(2)': 0.5, - 'acoustic(1)%wavelength': 0.25, 'acoustic(1)%mag': 2e+04, 't_step_start': 0, 't_step_stop': 50, - 't_step_save': 50, 'lag_txt_wrt': "T", 'lag_header': "T", 'lag_db_wrt': "T", 'lag_id_wrt': "T", - 'lag_pos_wrt': "T", 'lag_pos_prev_wrt': "T", 'lag_vel_wrt': "T", 'lag_rad_wrt': "T", - 'lag_rvel_wrt': "T", 'lag_r0_wrt': "T", 'lag_rmax_wrt': "T", 'lag_rmin_wrt': "T", - 'lag_dphidt_wrt': "T", 'lag_pres_wrt': "T", 'lag_mv_wrt': "T", 'lag_mg_wrt': "T", - 'lag_betaT_wrt': "T", 'lag_betaC_wrt': "T", 'lag_params%write_bubbles': "T", - 'lag_params%write_bubbles_stats': "T", "polytropic": "F", - 'bub_pp%R0ref': 1.0, 'bub_pp%p0ref': 1.0, 'bub_pp%rho0ref': 1.0, 'bub_pp%T0ref': 1.0, - 'bub_pp%ss': 7.131653759435349e-07, 'bub_pp%pv': 0.02292716400352907, 'bub_pp%vd': 2.4752475247524753e-06, - 'bub_pp%mu_l': 9.920792079207921e-08, 'bub_pp%gam_v': 1.33, 'bub_pp%gam_g': 1.4, - 'bub_pp%M_v': 18.02, 'bub_pp%M_g': 28.97, 'bub_pp%k_v': 5.618695895665441e-06, - 'bub_pp%k_g': 7.392868685947116e-06, 'bub_pp%R_v': 1347.810235139403, 'bub_pp%R_g': 838.3686723235085, - 'bub_pp%cp_g': 2921.2822272326243, 'bub_pp%cp_v': 6134.692677188511 - }) + stack.push( + "Lagrange Bubbles", + { + "bubbles_lagrange": "T", + "dt": 1e-06, + "lag_params%pressure_corrector": "T", + "bubble_model": 2, + "num_fluids": 2, + "lag_params%heatTransfer_model": "T", + "lag_params%massTransfer_model": "T", + "fluid_pp(1)%gamma": 0.16, + "fluid_pp(1)%pi_inf": 3515.0, + "fluid_pp(2)%gamma": 2.5, + "fluid_pp(2)%pi_inf": 0.0, + "patch_icpp(1)%alpha_rho(1)": 0.96, + "patch_icpp(1)%alpha(1)": 4e-02, + "patch_icpp(1)%alpha_rho(2)": 0.0, + "patch_icpp(1)%alpha(2)": 0.0, + "patch_icpp(2)%alpha_rho(1)": 0.96, + "patch_icpp(2)%alpha(1)": 4e-02, + "patch_icpp(2)%alpha_rho(2)": 0.0, + "patch_icpp(2)%alpha(2)": 0.0, + "patch_icpp(3)%alpha_rho(1)": 0.96, + "patch_icpp(3)%alpha(1)": 4e-02, + "patch_icpp(3)%alpha_rho(2)": 0.0, + "patch_icpp(3)%alpha(2)": 0.0, + "patch_icpp(1)%pres": 1.0, + "patch_icpp(2)%pres": 1.0, + "patch_icpp(3)%pres": 1.0, + "acoustic_source": "T", + "acoustic(1)%loc(2)": 0.5, + "acoustic(1)%wavelength": 0.25, + "acoustic(1)%mag": 2e04, + "t_step_start": 0, + "t_step_stop": 50, + "t_step_save": 50, + "lag_txt_wrt": "T", + "lag_header": "T", + "lag_db_wrt": "T", + "lag_id_wrt": "T", + "lag_pos_wrt": "T", + "lag_pos_prev_wrt": "T", + "lag_vel_wrt": "T", + "lag_rad_wrt": "T", + "lag_rvel_wrt": "T", + "lag_r0_wrt": "T", + "lag_rmax_wrt": "T", + "lag_rmin_wrt": "T", + "lag_dphidt_wrt": "T", + "lag_pres_wrt": "T", + "lag_mv_wrt": "T", + "lag_mg_wrt": "T", + "lag_betaT_wrt": "T", + "lag_betaC_wrt": "T", + "lag_params%write_bubbles": "T", + "lag_params%write_bubbles_stats": "T", + "polytropic": "F", + "bub_pp%R0ref": 1.0, + "bub_pp%p0ref": 1.0, + "bub_pp%rho0ref": 1.0, + "bub_pp%T0ref": 1.0, + "bub_pp%ss": 7.131653759435349e-07, + "bub_pp%pv": 0.02292716400352907, + "bub_pp%vd": 2.4752475247524753e-06, + "bub_pp%mu_l": 9.920792079207921e-08, + "bub_pp%gam_v": 1.33, + "bub_pp%gam_g": 1.4, + "bub_pp%M_v": 18.02, + "bub_pp%M_g": 28.97, + "bub_pp%k_v": 5.618695895665441e-06, + "bub_pp%k_g": 7.392868685947116e-06, + "bub_pp%R_v": 1347.810235139403, + "bub_pp%R_g": 838.3686723235085, + "bub_pp%cp_g": 2921.2822272326243, + "bub_pp%cp_v": 6134.692677188511, + }, + ) if len(dimInfo[0]) == 2: - stack.push("", {'acoustic(1)%support': 2}) + stack.push("", {"acoustic(1)%support": 2}) else: - stack.push("", {'acoustic(1)%support': 3, 'acoustic(1)%height': 1e10}) + stack.push("", {"acoustic(1)%support": 3, "acoustic(1)%height": 1e10}) if couplingMethod == 1: - stack.push('One-way Coupling', {'lag_params%solver_approach': 1}) + stack.push("One-way Coupling", {"lag_params%solver_approach": 1}) else: - stack.push('Two-way Coupling', {'lag_params%solver_approach': 2}) + stack.push("Two-way Coupling", {"lag_params%solver_approach": 2}) - if adap_dt == 'F': - stack.push('', {}) + if adap_dt == "F": + stack.push("", {}) else: - stack.push('adap_dt=T', {'adap_dt': 'T'}) + stack.push("adap_dt=T", {"adap_dt": "T"}) - cases.append(define_case_d(stack, '', {})) + cases.append(define_case_d(stack, "", {})) stack.pop() @@ -938,36 +1375,58 @@ def alter_lag_bubbles(dimInfo): def alter_elliptic_smoothing(): # Elliptic Smoothing - stack.push("Smoothing", { - 'elliptic_smoothing': 'T', 'elliptic_smoothing_iters': 10 - }) + stack.push("Smoothing", {"elliptic_smoothing": "T", "elliptic_smoothing_iters": 10}) - cases.append(define_case_d(stack, '', {})) + cases.append(define_case_d(stack, "", {})) stack.pop() def alter_bc_patches(dimInfo): - # BC_Patches + # BC_Patches - stack.push('BC Patches', { - 'num_bc_patches': 1 - }) + stack.push("BC Patches", {"num_bc_patches": 1}) if len(dimInfo[0]) > 2: for direc in [1, 2, 3]: - stack.push('Circle', { - 'patch_bc(1)%geometry': 2, 'patch_bc(1)%dir': direc, - 'patch_bc(1)%type': -17, 'patch_bc(1)%loc': -1, - }) + stack.push( + "Circle", + { + "patch_bc(1)%geometry": 2, + "patch_bc(1)%dir": direc, + "patch_bc(1)%type": -17, + "patch_bc(1)%loc": -1, + }, + ) if direc == 1: - stack.push('X', {'patch_bc(1)%centroid(2)': 0, 'patch_bc(1)%centroid(3)': 0, "patch_bc(1)%radius": 0.000125, }) + stack.push( + "X", + { + "patch_bc(1)%centroid(2)": 0, + "patch_bc(1)%centroid(3)": 0, + "patch_bc(1)%radius": 0.000125, + }, + ) elif direc == 2: - stack.push('Y', {'patch_bc(1)%centroid(1)': 0, 'patch_bc(1)%centroid(3)': 0, "patch_bc(1)%radius": 0.000125, }) + stack.push( + "Y", + { + "patch_bc(1)%centroid(1)": 0, + "patch_bc(1)%centroid(3)": 0, + "patch_bc(1)%radius": 0.000125, + }, + ) else: - stack.push('Z', {'patch_bc(1)%centroid(1)': 0, 'patch_bc(1)%centroid(2)': 0, "patch_bc(1)%radius": 0.000125, }) + stack.push( + "Z", + { + "patch_bc(1)%centroid(1)": 0, + "patch_bc(1)%centroid(2)": 0, + "patch_bc(1)%radius": 0.000125, + }, + ) - cases.append(define_case_d(stack, '', {})) + cases.append(define_case_d(stack, "", {})) stack.pop() @@ -975,17 +1434,14 @@ def alter_bc_patches(dimInfo): elif len(dimInfo[0]) > 1: for direc in [1, 2]: - stack.push('Line Segment', { - 'patch_bc(1)%geometry': 1, 'patch_bc(1)%dir': direc, - 'patch_bc(1)%type': -17, 'patch_bc(1)%loc': -1 - }) + stack.push("Line Segment", {"patch_bc(1)%geometry": 1, "patch_bc(1)%dir": direc, "patch_bc(1)%type": -17, "patch_bc(1)%loc": -1}) if direc == 1: - stack.push('X', {'patch_bc(1)%centroid(2)': 0.0, 'patch_bc(1)%length(2)': 0.0025}) + stack.push("X", {"patch_bc(1)%centroid(2)": 0.0, "patch_bc(1)%length(2)": 0.0025}) else: - stack.push('Y', {'patch_bc(1)%centroid(1)': 0.0, 'patch_bc(1)%length(1)': 0.0025}) + stack.push("Y", {"patch_bc(1)%centroid(1)": 0.0, "patch_bc(1)%length(1)": 0.0025}) - cases.append(define_case_d(stack, '', {})) + cases.append(define_case_d(stack, "", {})) stack.pop() @@ -995,21 +1451,21 @@ def alter_bc_patches(dimInfo): def mhd_cases(): params = { - '1D': {"m": 200, "dt": 0.001, "t_step_stop": 200, "t_step_save": 200}, - '2D': {"m": 50, "n": 50, "dt": 0.002, "t_step_stop": 500, "t_step_save": 500}, - '3D': {"m": 25, "n": 25, "p": 25, "dt": 0.005, "t_step_stop": 200, "t_step_save": 200}, + "1D": {"m": 200, "dt": 0.001, "t_step_stop": 200, "t_step_save": 200}, + "2D": {"m": 50, "n": 50, "dt": 0.002, "t_step_stop": 500, "t_step_save": 500}, + "3D": {"m": 25, "n": 25, "p": 25, "dt": 0.005, "t_step_stop": 200, "t_step_save": 200}, } case_specs = [ - ("1D -> MHD -> HLL", "examples/1D_brio_wu/case.py", params['1D']), - ("1D -> MHD -> HLLD", "examples/1D_brio_wu_hlld/case.py", params['1D']), - ("1D -> RMHD", "examples/1D_brio_wu_rmhd/case.py", params['1D']), - ("2D -> MHD -> HLL", "examples/2D_orszag_tang/case.py", params['2D']), - ("2D -> MHD -> HLLD", "examples/2D_orszag_tang/case.py", {**params['2D'], 'riemann_solver': 4}), - ("2D -> MHD -> hyper_cleaning", "examples/2D_orszag_tang_hyper_cleaning/case.py", params['2D']), - ("2D -> RMHD", "examples/2D_shock_cloud_rmhd/case.py", params['2D']), - ("3D -> MHD", "examples/3D_brio_wu/case.py", params['3D']), - ("3D -> RMHD", "examples/3D_brio_wu/case.py", {**params['3D'], 'relativity': 'T'}), + ("1D -> MHD -> HLL", "examples/1D_brio_wu/case.py", params["1D"]), + ("1D -> MHD -> HLLD", "examples/1D_brio_wu_hlld/case.py", params["1D"]), + ("1D -> RMHD", "examples/1D_brio_wu_rmhd/case.py", params["1D"]), + ("2D -> MHD -> HLL", "examples/2D_orszag_tang/case.py", params["2D"]), + ("2D -> MHD -> HLLD", "examples/2D_orszag_tang/case.py", {**params["2D"], "riemann_solver": 4}), + ("2D -> MHD -> hyper_cleaning", "examples/2D_orszag_tang_hyper_cleaning/case.py", params["2D"]), + ("2D -> RMHD", "examples/2D_shock_cloud_rmhd/case.py", params["2D"]), + ("3D -> MHD", "examples/3D_brio_wu/case.py", params["3D"]), + ("3D -> RMHD", "examples/3D_brio_wu/case.py", {**params["3D"], "relativity": "T"}), ] for name, path, param in case_specs: @@ -1029,7 +1485,7 @@ def foreach_dimension(): alter_3d() alter_lag_bubbles(dimInfo) alter_ppn(dimInfo) - stack.push('', {'dt': [1e-07, 1e-06, 1e-06][len(dimInfo[0])-1]}) + stack.push("", {"dt": [1e-07, 1e-06, 1e-06][len(dimInfo[0]) - 1]}) alter_acoustic_src(dimInfo) alter_bubbles(dimInfo) alter_hypoelasticity(dimInfo) @@ -1048,93 +1504,127 @@ def foreach_example(): continue # # List of all example cases that will be skipped during testing - casesToSkip = ["2D_ibm_cfl_dt", "1D_sodHypo", "2D_viscous", - "2D_laplace_pressure_jump", "2D_bubbly_steady_shock", - "2D_advection", "2D_hardcoded_ic", - "2D_ibm_multiphase", "2D_acoustic_broadband", - "1D_inert_shocktube", "1D_reactive_shocktube", - "2D_ibm_steady_shock", "3D_performance_test", - "3D_ibm_stl_ellipsoid", "3D_sphbubcollapse", - "2D_ibm_stl_wedge", "3D_ibm_stl_pyramid", - "3D_ibm_bowshock", "3D_turb_mixing", - "2D_mixing_artificial_Ma", - "2D_lagrange_bubblescreen", - "3D_lagrange_bubblescreen", "2D_triple_point", - "1D_shuosher_analytical", - "1D_titarevtorro_analytical", - "2D_acoustic_pulse_analytical", - "2D_isentropicvortex_analytical", - "2D_zero_circ_vortex_analytical", - "3D_TaylorGreenVortex_analytical", - "3D_IGR_TaylorGreenVortex_nvidia", - "2D_backward_facing_step", - "2D_forward_facing_step", - "1D_convergence", - "3D_IGR_33jet", "1D_multispecies_diffusion", - "2D_ibm_stl_MFCCharacter"] + casesToSkip = [ + "2D_ibm_cfl_dt", + "1D_sodHypo", + "2D_viscous", + "2D_laplace_pressure_jump", + "2D_bubbly_steady_shock", + "2D_advection", + "2D_hardcoded_ic", + "2D_ibm_multiphase", + "2D_acoustic_broadband", + "1D_inert_shocktube", + "1D_reactive_shocktube", + "2D_ibm_steady_shock", + "3D_performance_test", + "3D_ibm_stl_ellipsoid", + "3D_sphbubcollapse", + "2D_ibm_stl_wedge", + "3D_ibm_stl_pyramid", + "3D_ibm_bowshock", + "3D_turb_mixing", + "2D_mixing_artificial_Ma", + "2D_lagrange_bubblescreen", + "3D_lagrange_bubblescreen", + "2D_triple_point", + "1D_shuosher_analytical", + "1D_titarevtorro_analytical", + "2D_acoustic_pulse_analytical", + "2D_isentropicvortex_analytical", + "2D_zero_circ_vortex_analytical", + "3D_TaylorGreenVortex_analytical", + "3D_IGR_TaylorGreenVortex_nvidia", + "2D_backward_facing_step", + "2D_forward_facing_step", + "1D_convergence", + "3D_IGR_33jet", + "1D_multispecies_diffusion", + "2D_ibm_stl_MFCCharacter", + ] if path in casesToSkip: continue name = f"{path.split('_')[0]} -> Example -> {'_'.join(path.split('_')[1:])}" - path = os.path.join(common.MFC_EXAMPLE_DIRPATH, path, "case.py") - if not os.path.isfile(path): + case_path = os.path.join(common.MFC_EXAMPLE_DIRPATH, path, "case.py") + if not os.path.isfile(case_path): continue def modify_example_case(case: dict): - case['parallel_io'] = 'F' - if 't_step_stop' in case and case['t_step_stop'] >= 50: - case['t_step_start'] = 0 - case['t_step_stop'] = 50 - case['t_step_save'] = 50 + case["parallel_io"] = "F" + if "t_step_stop" in case and case["t_step_stop"] >= 50: + case["t_step_start"] = 0 + case["t_step_stop"] = 50 + case["t_step_save"] = 50 - caseSize = case['m'] * max(case['n'], 1) * max(case['p'], 1) + caseSize = case["m"] * max(case["n"], 1) * max(case["p"], 1) if caseSize > 25 * 25: - if case['n'] == 0 and case['p'] == 0: - case['m'] = 25 * 25 - elif case['p'] == 0: - case['m'] = 25 - case['n'] = 25 + if case["n"] == 0 and case["p"] == 0: + case["m"] = 25 * 25 + elif case["p"] == 0: + case["m"] = 25 + case["n"] = 25 elif caseSize > 25 * 25 * 25: - case['m'] = 25 - case['n'] = 25 - case['p'] = 25 + case["m"] = 25 + case["n"] = 25 + case["p"] = 25 - cases.append(define_case_f(name, path, [], {}, functor=modify_example_case)) + cases.append(define_case_f(name, case_path, [], {}, functor=modify_example_case)) def chemistry_cases(): - common_mods = { - 't_step_stop': Nt, 't_step_save': Nt - } + common_mods = {"t_step_stop": Nt, "t_step_save": Nt} for ndim in range(1, 4): - cases.append(define_case_f( - f'{ndim}D -> Chemistry -> Perfect Reactor', - 'examples/nD_perfect_reactor/case.py', - ['--ndim', str(ndim)], - mods=common_mods - )) + cases.append(define_case_f(f"{ndim}D -> Chemistry -> Perfect Reactor", "examples/nD_perfect_reactor/case.py", ["--ndim", str(ndim)], mods=common_mods)) for riemann_solver, gamma_method in itertools.product([1, 2], [1, 2]): - cases.append(define_case_f( - f'1D -> Chemistry -> Inert Shocktube -> Riemann Solver {riemann_solver} -> Gamma Method {gamma_method}', - 'examples/1D_inert_shocktube/case.py', - mods={ - **common_mods, - 'riemann_solver': riemann_solver, - 'chem_params%gamma_method': gamma_method, - 'weno_order': 3, "mapped_weno": 'F', 'mp_weno': 'F' - }, - override_tol=10**(-10) - )) - - stack.push(f'1D -> Chemistry -> MultiComponent Diffusion', {'m': 200, - 'dt': 0.1e-06, 'num_patches': 1, 'num_fluids': 1, 'x_domain%beg': 0.0, 'x_domain%end': 0.05, - 'bc_x%beg': -1, 'bc_x%end': -1, 'weno_order': 5, 'weno_eps': 1e-16, 'weno_avg': 'F', - 'mapped_weno': 'T', 'mp_weno': 'T', 'weno_Re_flux': 'F', 'riemann_solver': 2, 'wave_speeds': 1, - 'avg_state': 1, 'chemistry': 'T', 'chem_params%diffusion': 'T', 'chem_params%reactions': 'F', 'chem_wrt_T': 'T', - 'patch_icpp(1)%geometry': 1, 'patch_icpp(1)%hcid': 182, 'patch_icpp(1)%x_centroid': 0.05 / 2, - 'patch_icpp(1)%length_x': 0.05, 'patch_icpp(1)%vel(1)': '0', 'patch_icpp(1)%pres': 1.01325e5, 'patch_icpp(1)%alpha(1)': 1, - 'fluid_pp(1)%gamma': 1.0e00 / (1.9326e00 - 1.0e00), 'fluid_pp(1)%pi_inf': 0, 'cantera_file': 'h2o2.yaml', 't_step_start': 0, 't_step_stop': 50, 't_step_save': 50 - }) - cases.append(define_case_d(stack, '', {}, override_tol=10**(-9))) + cases.append( + define_case_f( + f"1D -> Chemistry -> Inert Shocktube -> Riemann Solver {riemann_solver} -> Gamma Method {gamma_method}", + "examples/1D_inert_shocktube/case.py", + mods={**common_mods, "riemann_solver": riemann_solver, "chem_params%gamma_method": gamma_method, "weno_order": 3, "mapped_weno": "F", "mp_weno": "F"}, + override_tol=10 ** (-10), + ) + ) + + stack.push( + "1D -> Chemistry -> MultiComponent Diffusion", + { + "m": 200, + "dt": 0.1e-06, + "num_patches": 1, + "num_fluids": 1, + "x_domain%beg": 0.0, + "x_domain%end": 0.05, + "bc_x%beg": -1, + "bc_x%end": -1, + "weno_order": 5, + "weno_eps": 1e-16, + "weno_avg": "F", + "mapped_weno": "T", + "mp_weno": "T", + "weno_Re_flux": "F", + "riemann_solver": 2, + "wave_speeds": 1, + "avg_state": 1, + "chemistry": "T", + "chem_params%diffusion": "T", + "chem_params%reactions": "F", + "chem_wrt_T": "T", + "patch_icpp(1)%geometry": 1, + "patch_icpp(1)%hcid": 182, + "patch_icpp(1)%x_centroid": 0.05 / 2, + "patch_icpp(1)%length_x": 0.05, + "patch_icpp(1)%vel(1)": "0", + "patch_icpp(1)%pres": 1.01325e5, + "patch_icpp(1)%alpha(1)": 1, + "fluid_pp(1)%gamma": 1.0e00 / (1.9326e00 - 1.0e00), + "fluid_pp(1)%pi_inf": 0, + "cantera_file": "h2o2.yaml", + "t_step_start": 0, + "t_step_stop": 50, + "t_step_save": 50, + }, + ) + cases.append(define_case_d(stack, "", {}, override_tol=10 ** (-9))) stack.pop() diff --git a/toolchain/mfc/test/test.py b/toolchain/mfc/test/test.py index 2193e677b4..6565aa303d 100644 --- a/toolchain/mfc/test/test.py +++ b/toolchain/mfc/test/test.py @@ -1,21 +1,25 @@ -import os, typing, shutil, time, itertools, threading +import itertools +import os +import shutil +import sys +import threading +import time +import typing from random import sample, seed -import rich, rich.table +import rich +import rich.table from rich.panel import Panel -from ..printer import cons -from .. import common -from ..state import ARG -from .case import TestCase -from .cases import list_cases -from .. import sched -from ..common import MFCException, does_command_exist, format_list_to_string, get_program_output -from ..build import build, HDF5, PRE_PROCESS, SIMULATION, POST_PROCESS - -from ..packer import tol as packtol +from .. import common, sched +from ..build import HDF5, POST_PROCESS, PRE_PROCESS, SIMULATION, build +from ..common import MFCException, does_command_exist, format_list_to_string, get_program_output from ..packer import packer - +from ..packer import tol as packtol +from ..printer import cons +from ..state import ARG +from .case import TestCase +from .cases import list_cases nFAIL = 0 nPASS = 0 @@ -39,9 +43,11 @@ # from worker threads which could leave the scheduler in an undefined state. abort_tests = threading.Event() + class TestTimeoutError(MFCException): pass + def _filter_only(cases, skipped_cases): """Filter cases by --only terms using AND for labels, OR for UUIDs. @@ -49,10 +55,11 @@ def _filter_only(cases, skipped_cases): UUIDs (8-char hex terms): case must match ANY UUID (OR logic). Mixed: keep case if all labels match OR any UUID matches. """ + def is_uuid(term): - return len(term) == 8 and all(c in '0123456789abcdefABCDEF' for c in term) + return len(term) == 8 and all(c in "0123456789abcdefABCDEF" for c in term) - uuids = [t for t in ARG("only") if is_uuid(t)] + uuids = [t for t in ARG("only") if is_uuid(t)] labels = [t for t in ARG("only") if not is_uuid(t)] for case in cases[:]: @@ -60,7 +67,7 @@ def is_uuid(term): check.add(case.get_uuid()) label_ok = all(label in check for label in labels) if labels else True - uuid_ok = any(u in check for u in uuids) if uuids else True + uuid_ok = any(u in check for u in uuids) if uuids else True if labels and uuids: keep = label_ok or uuid_ok @@ -76,22 +83,21 @@ def is_uuid(term): return cases, skipped_cases -# pylint: disable=too-many-branches, too-many-statements, trailing-whitespace def __filter(cases_) -> typing.List[TestCase]: cases = cases_[:] selected_cases = [] - skipped_cases = [] + skipped_cases = [] # Check "--from" and "--to" exist and are in the right order bFoundFrom, bFoundTo = (False, False) from_i = -1 for i, case in enumerate(cases): if case.get_uuid() == ARG("from"): - from_i = i + from_i = i bFoundFrom = True # Do not "continue" because "--to" might be the same as "--from" if bFoundFrom and case.get_uuid() == ARG("to"): - cases = cases[from_i:i+1] + cases = cases[from_i : i + 1] skipped_cases = [case for case in cases_ if case not in cases] bFoundTo = True break @@ -103,10 +109,7 @@ def __filter(cases_) -> typing.List[TestCase]: cases, skipped_cases = _filter_only(cases, skipped_cases) if not cases: - raise MFCException( - f"--only filter matched zero test cases. " - f"Specified: {ARG('only')}. Check that UUIDs/names are valid." - ) + raise MFCException(f"--only filter matched zero test cases. Specified: {ARG('only')}. Check that UUIDs/names are valid.") for case in cases[:]: if case.ppn > 1 and not ARG("mpi"): @@ -115,15 +118,14 @@ def __filter(cases_) -> typing.List[TestCase]: for case in cases[:]: if ARG("single"): - skip = ['low_Mach', 'Hypoelasticity', 'teno', 'Chemistry', 'Phase Change model 6' - ,'Axisymmetric', 'Transducer', 'Transducer Array', 'Cylindrical', 'HLLD', 'Example'] + skip = ["low_Mach", "Hypoelasticity", "teno", "Chemistry", "Phase Change model 6", "Axisymmetric", "Transducer", "Transducer Array", "Cylindrical", "HLLD", "Example"] if any(label in case.trace for label in skip): cases.remove(case) skipped_cases.append(case) for case in cases[:]: if ARG("gpu"): - skip = ['Gauss Seidel'] + skip = ["Gauss Seidel"] if any(label in case.trace for label in skip): cases.remove(case) @@ -141,25 +143,22 @@ def __filter(cases_) -> typing.List[TestCase]: cases = [c for i, c in enumerate(cases) if i % shard_count == shard_idx - 1] if not cases: - raise MFCException( - f"--shard {ARG('shard')} matched zero test cases. " - f"Total cases before sharding may be less than shard count." - ) + raise MFCException(f"--shard {ARG('shard')} matched zero test cases. Total cases before sharding may be less than shard count.") if ARG("percent") == 100: return cases, skipped_cases seed(time.time()) - selected_cases = sample(cases, k=int(len(cases)*ARG("percent")/100.0)) + selected_cases = sample(cases, k=int(len(cases) * ARG("percent") / 100.0)) skipped_cases += [item for item in cases if item not in selected_cases] return selected_cases, skipped_cases + def test(): - # pylint: disable=global-statement, global-variable-not-assigned, too-many-statements, too-many-locals - global nFAIL, nPASS, nSKIP, total_test_count - global errors, failed_tests, test_start_time + global nFAIL, nPASS, nSKIP, total_test_count # noqa: PLW0603 + global errors, failed_tests, test_start_time # noqa: PLW0603 test_start_time = time.time() # Start timing failed_uuids_path = os.path.join(common.MFC_TEST_DIR, "failed_uuids.txt") @@ -168,7 +167,7 @@ def test(): # Delete UUIDs that are not in the list of cases from tests/ if ARG("remove_old_tests"): dir_uuids = set(os.listdir(common.MFC_TEST_DIR)) - new_uuids = { case.get_uuid() for case in cases } + new_uuids = {case.get_uuid() for case in cases} for old_uuid in dir_uuids - new_uuids: cons.print(f"[bold red]Deleting:[/bold red] {old_uuid}") @@ -177,7 +176,7 @@ def test(): return cases, skipped_cases = __filter(cases) - cases = [ _.to_case() for _ in cases ] + cases = [_.to_case() for _ in cases] total_test_count = len(cases) if ARG("list"): @@ -196,7 +195,7 @@ def test(): # Some cases require a specific build of MFC for features like Chemistry, # Analytically defined patches, and --case-optimization. Here, we build all # the unique versions of MFC we need to run cases. - codes = [PRE_PROCESS, SIMULATION] + ([POST_PROCESS] if ARG('test_all') else []) + codes = [PRE_PROCESS, SIMULATION] + ([POST_PROCESS] if ARG("test_all") else []) unique_builds = set() for case, code in itertools.product(cases, codes): slug = code.get_slug(case.to_input_file()) @@ -211,7 +210,7 @@ def test(): if len(ARG("only")) > 0: range_str = "Only " + format_list_to_string(ARG("only"), "bold magenta", "Nothing to run") - cons.print(f"[bold]Test {format_list_to_string([ x.name for x in codes ], 'magenta')}[/bold] | {range_str} ({len(cases)} test{'s' if len(cases) != 1 else ''})") + cons.print(f"[bold]Test {format_list_to_string([x.name for x in codes], 'magenta')}[/bold] | {range_str} ({len(cases)} test{'s' if len(cases) != 1 else ''})") cons.indent() # Run cases with multiple threads (if available) @@ -224,9 +223,7 @@ def test(): # because running a test case may cause it to rebuild, and thus # interfere with the other test cases. It is a niche feature so we won't # engineer around this issue (for now). - sched.sched( - [ sched.Task(ppn=case.ppn, func=handle_case, args=[case], load=case.get_cell_count()) for case in cases ], - ARG("jobs"), ARG("gpus")) + sched.sched([sched.Task(ppn=case.ppn, func=handle_case, args=[case], load=case.get_cell_count()) for case in cases], ARG("jobs"), ARG("gpus")) # Check if we aborted due to high failure rate if abort_tests.is_set(): @@ -241,13 +238,8 @@ def test(): cons.print() cons.unindent() if total_completed > 0: - raise MFCException( - f"Excessive test failures: {nFAIL}/{total_completed} " - f"failed ({nFAIL/total_completed*100:.1f}%)" - ) - raise MFCException( - f"Excessive test failures: {nFAIL} failed, but no tests were completed." - ) + raise MFCException(f"Excessive test failures: {nFAIL}/{total_completed} failed ({nFAIL / total_completed * 100:.1f}%)") + raise MFCException(f"Excessive test failures: {nFAIL} failed, but no tests were completed.") nSKIP = len(skipped_cases) cons.print() @@ -265,16 +257,14 @@ def test(): if failed_tests: with open(failed_uuids_path, "w") as f: for test_info in failed_tests: - f.write(test_info['uuid'] + "\n") + f.write(test_info["uuid"] + "\n") elif os.path.exists(failed_uuids_path): os.remove(failed_uuids_path) - exit(nFAIL) + sys.exit(nFAIL) -def _print_test_summary(passed: int, failed: int, skipped: int, minutes: int, seconds: float, - failed_test_list: list, _skipped_cases: list): - # pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-locals +def _print_test_summary(passed: int, failed: int, skipped: int, minutes: int, seconds: float, failed_test_list: list, _skipped_cases: list): """Print a comprehensive test summary report.""" total = passed + failed + skipped @@ -312,9 +302,9 @@ def _print_test_summary(passed: int, failed: int, skipped: int, minutes: int, se summary_lines.append("") summary_lines.append(" [bold red]Failed Tests:[/bold red]") for test_info in failed_test_list[:10]: # Limit to first 10 - trace = test_info.get('trace', 'Unknown') - uuid = test_info.get('uuid', 'Unknown') - error_type = test_info.get('error_type', '') + trace = test_info.get("trace", "Unknown") + uuid = test_info.get("uuid", "Unknown") + error_type = test_info.get("error_type", "") if len(trace) > 40: trace = trace[:37] + "..." summary_lines.append(f" [red]•[/red] {trace}") @@ -333,16 +323,10 @@ def _print_test_summary(passed: int, failed: int, skipped: int, minutes: int, se summary_lines.append(" • Run specific test: [cyan]./mfc.sh test --only [/cyan]") cons.print() - cons.raw.print(Panel( - "\n".join(summary_lines), - title="[bold]Test Summary[/bold]", - border_style=border_style, - padding=(1, 2) - )) + cons.raw.print(Panel("\n".join(summary_lines), title="[bold]Test Summary[/bold]", border_style=border_style, padding=(1, 2))) cons.print() -# pylint: disable=too-many-locals, too-many-branches, too-many-statements, trailing-whitespace def _process_silo_file(silo_filepath: str, case: TestCase, out_filepath: str): """Process a single silo file with h5dump and check for NaNs/Infinities.""" h5dump = f"{HDF5.get_install_dirpath(case.to_input_file())}/bin/h5dump" @@ -355,27 +339,17 @@ def _process_silo_file(silo_filepath: str, case: TestCase, out_filepath: str): output, err = get_program_output([h5dump, silo_filepath]) if err != 0: - raise MFCException( - f"Test {case}: Failed to run h5dump. You can find the run's output in {out_filepath}, " - f"and the case dictionary in {case.get_filepath()}." - ) + raise MFCException(f"Test {case}: Failed to run h5dump. You can find the run's output in {out_filepath}, and the case dictionary in {case.get_filepath()}.") if "nan," in output: - raise MFCException( - f"Test {case}: Post Process has detected a NaN. You can find the run's output in {out_filepath}, " - f"and the case dictionary in {case.get_filepath()}." - ) + raise MFCException(f"Test {case}: Post Process has detected a NaN. You can find the run's output in {out_filepath}, and the case dictionary in {case.get_filepath()}.") if "inf," in output: - raise MFCException( - f"Test {case}: Post Process has detected an Infinity. You can find the run's output in {out_filepath}, " - f"and the case dictionary in {case.get_filepath()}." - ) + raise MFCException(f"Test {case}: Post Process has detected an Infinity. You can find the run's output in {out_filepath}, and the case dictionary in {case.get_filepath()}.") def _handle_case(case: TestCase, devices: typing.Set[int]): - # pylint: disable=global-statement, global-variable-not-assigned - global current_test_number + global current_test_number # noqa: PLW0603 start_time = time.time() # Set timeout using threading.Timer (works in worker threads) @@ -456,7 +430,7 @@ def _handle_case(case: TestCase, devices: typing.Set[int]): out_filepath = os.path.join(case.get_dirpath(), "out_post.txt") common.file_write(out_filepath, cmd.stdout) - silo_dir = os.path.join(case.get_dirpath(), 'silo_hdf5', 'p0') + silo_dir = os.path.join(case.get_dirpath(), "silo_hdf5", "p0") if os.path.isdir(silo_dir): for silo_filename in os.listdir(silo_dir): silo_filepath = os.path.join(silo_dir, silo_filename) @@ -474,36 +448,29 @@ def _handle_case(case: TestCase, devices: typing.Set[int]): 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') + log_path = os.path.join(case.get_dirpath(), "out_pre_sim.txt") if os.path.exists(log_path): log_msg = f"Check the log at: {log_path}" else: - log_msg = ( - f"Log file ({log_path}) may not exist if the timeout occurred early." - ) - raise MFCException( - f"Test {case} exceeded 1 hour timeout. " - f"This may indicate a hung simulation or misconfigured case. " - f"{log_msg}" - ) from exc + log_msg = f"Log file ({log_path}) may not exist if the timeout occurred early." + raise MFCException(f"Test {case} exceeded 1 hour timeout. This may indicate a hung simulation or misconfigured case. {log_msg}") from exc finally: timeout_timer.cancel() # Cancel timeout timer def handle_case(case: TestCase, devices: typing.Set[int]): - # pylint: disable=global-statement, global-variable-not-assigned - global nFAIL, nPASS, nSKIP - global errors, failed_tests + global nFAIL, nPASS, nSKIP # noqa: PLW0603 + global errors, failed_tests # noqa: PLW0603 # Check if we should abort before processing this case if abort_tests.is_set(): return # Exit gracefully if abort was requested nAttempts = 0 - if ARG('single'): - max_attempts = max(ARG('max_attempts'), 3) + if ARG("single"): + max_attempts = max(ARG("max_attempts"), 3) else: - max_attempts = ARG('max_attempts') + max_attempts = ARG("max_attempts") while True: nAttempts += 1 @@ -535,13 +502,13 @@ def handle_case(case: TestCase, devices: typing.Set[int]): # 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]") + cons.print(" [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]") + cons.print(" [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]") + cons.print(" [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(" [dim]Hint: Check build logs and case parameters[/dim]") cons.print() # Track failed test details for summary @@ -556,12 +523,7 @@ def handle_case(case: TestCase, devices: typing.Set[int]): elif "failed to execute" in exc_lower: error_type = "execution failed" - failed_tests.append({ - 'trace': case.trace, - 'uuid': case.get_uuid(), - 'error_type': error_type, - 'attempts': nAttempts - }) + failed_tests.append({"trace": case.trace, "uuid": case.get_uuid(), "error_type": error_type, "attempts": nAttempts}) # Still collect for final summary errors.append(f"[bold red]Failed test {case} after {nAttempts} attempt(s).[/bold red]") @@ -574,7 +536,7 @@ def handle_case(case: TestCase, devices: typing.Set[int]): if total_completed >= MIN_CASES_BEFORE_ABORT: failure_rate = nFAIL / total_completed if failure_rate >= FAILURE_RATE_THRESHOLD: - cons.print(f"\n[bold red]CRITICAL: {failure_rate*100:.1f}% failure rate detected after {total_completed} tests.[/bold red]") + cons.print(f"\n[bold red]CRITICAL: {failure_rate * 100:.1f}% failure rate detected after {total_completed} tests.[/bold red]") cons.print("[bold red]This suggests a systemic issue (bad build, broken environment, etc.)[/bold red]") cons.print("[bold red]Aborting remaining tests to fail fast.[/bold red]\n") # Set abort flag instead of raising exception from worker thread diff --git a/toolchain/mfc/user_guide.py b/toolchain/mfc/user_guide.py index 1beb7cc4d2..fa40cc0025 100644 --- a/toolchain/mfc/user_guide.py +++ b/toolchain/mfc/user_guide.py @@ -10,21 +10,19 @@ """ import os -import subprocess import re +import subprocess +from rich import box +from rich.markdown import Markdown from rich.panel import Panel -from rich.table import Table from rich.prompt import Prompt -from rich.markdown import Markdown -from rich import box - -from .printer import cons -from .common import MFC_ROOT_DIR +from rich.table import Table # Import command definitions from CLI schema (SINGLE SOURCE OF TRUTH) from .cli.commands import COMMANDS - +from .common import MFC_ROOT_DIR +from .printer import cons # ============================================================================= # DYNAMIC CLUSTER HELP GENERATION @@ -79,8 +77,8 @@ def _parse_modules_file(): try: with open(modules_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() # Skip comments and empty lines if not line or line.startswith("#"): continue @@ -89,7 +87,7 @@ def _parse_modules_file(): continue # Parse cluster definition lines: "slug System Name" - match = re.match(r'^(\S+)\s+(.+)$', line) + match = re.match(r"^(\S+)\s+(.+)$", line) if match: slug = match.group(1) full_name = match.group(2).strip() @@ -120,7 +118,7 @@ def _get_cluster_short_name(slug, full_name): # Strip org prefix if present for prefix in CLUSTER_ORGS: if full_name.startswith(prefix + " "): - return full_name[len(prefix) + 1:] + return full_name[len(prefix) + 1 :] return full_name @@ -145,17 +143,14 @@ def _generate_clusters_content(): if not org_clusters.get(org): continue # Format: " [yellow]ORG:[/yellow] [cyan]slug[/cyan]=Name [cyan]slug2[/cyan]=Name2" - entries = [ - f"[cyan]{slug}[/cyan]={_get_cluster_short_name(slug, name)}" - for slug, name in org_clusters[org] - ] - color = ORG_COLORS.get(org, 'yellow') + entries = [f"[cyan]{slug}[/cyan]={_get_cluster_short_name(slug, name)}" for slug, name in org_clusters[org]] + color = ORG_COLORS.get(org, "yellow") cluster_lines.append(f" [{color}]{org}:[/{color}] " + " ".join(entries)) # Handle "Other" if any if org_clusters.get("Other"): entries = [f"[cyan]{slug}[/cyan]={name}" for slug, name in org_clusters["Other"]] - cluster_lines.append(f" [yellow]Other:[/yellow] " + " ".join(entries)) + cluster_lines.append(" [yellow]Other:[/yellow] " + " ".join(entries)) cluster_list = "\n".join(cluster_lines) if cluster_lines else " [dim]No clusters found in modules file[/dim]" @@ -209,7 +204,7 @@ def _extract_markdown_section(content: str, section_heading: str) -> str: """ # Find the section heading (## or ###) # Note: In f-strings, literal braces must be doubled: {{1,3}} -> {1,3} - pattern = rf'^(#{{1,3}})\s+{re.escape(section_heading)}\s*$' + pattern = rf"^(#{{1,3}})\s+{re.escape(section_heading)}\s*$" match = re.search(pattern, content, re.MULTILINE) if not match: return None @@ -219,11 +214,11 @@ def _extract_markdown_section(content: str, section_heading: str) -> str: # Find the end: horizontal rule (---) which separates major sections # Note: We use --- instead of heading detection because shell comments # inside code blocks (# comment) look like markdown headings to regex - end_pattern = r'^---' + end_pattern = r"^---" end_match = re.search(end_pattern, content[start_pos:], re.MULTILINE) if end_match: - section = content[start_pos:start_pos + end_match.start()] + section = content[start_pos : start_pos + end_match.start()] else: section = content[start_pos:] @@ -256,20 +251,22 @@ def _load_markdown_help(topic: str) -> str: # Strip Doxygen-specific syntax # Remove @page directives - content = re.sub(r'^@page\s+\S+\s+.*$', '', content, flags=re.MULTILINE) + content = re.sub(r"^@page\s+\S+\s+.*$", "", content, flags=re.MULTILINE) # Remove @ref, @see directives (but keep the text after them readable) - content = re.sub(r'@(ref|see)\s+"([^"]+)"', r'\2', content) # @ref "Text" -> Text - content = re.sub(r'@(ref|see)\s+(\S+)', '', content) # @ref name -> (remove) + content = re.sub(r'@(ref|see)\s+"([^"]+)"', r"\2", content) # @ref "Text" -> Text + content = re.sub(r"@(ref|see)\s+(\S+)", "", content) # @ref name -> (remove) # Clean up any resulting empty lines at the start - content = content.lstrip('\n') + content = content.lstrip("\n") return content def _generate_markdown_help(topic: str): """Generate a function that loads markdown help for a topic.""" + def loader(): return _load_markdown_help(topic) + return loader @@ -342,23 +339,14 @@ def print_topic_help(topic: str): cons.raw.print(Markdown(content)) else: # Render as Rich markup in a panel - cons.raw.print(Panel( - content, - title=f"[bold]{topic_info['title']}[/bold]", - box=box.ROUNDED, - padding=(1, 2) - )) + cons.raw.print(Panel(content, title=f"[bold]{topic_info['title']}[/bold]", box=box.ROUNDED, padding=(1, 2))) cons.print() def print_help_topics(): """Print list of available help topics.""" cons.print() - cons.raw.print(Panel( - "[bold cyan]MFC Help System[/bold cyan]", - box=box.ROUNDED, - padding=(0, 2) - )) + cons.raw.print(Panel("[bold cyan]MFC Help System[/bold cyan]", box=box.ROUNDED, padding=(0, 2))) cons.print() table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2)) @@ -379,11 +367,12 @@ def print_help_topics(): # ENHANCED HELP OUTPUT # ============================================================================= + def _truncate_desc(desc: str, max_len: int = 50) -> str: """Truncate description to fit compact display.""" if len(desc) <= max_len: return desc - return desc[:max_len-3] + "..." + return desc[: max_len - 3] + "..." def print_help(): @@ -439,12 +428,7 @@ def print_command_help(command: str, show_argparse: bool = True): # Header panel cons.print() - cons.raw.print(Panel( - f"[bold cyan]{command}[/bold cyan]{alias_str}\n" - f"[dim]{cmd['description']}[/dim]", - box=box.ROUNDED, - padding=(0, 2) - )) + cons.raw.print(Panel(f"[bold cyan]{command}[/bold cyan]{alias_str}\n[dim]{cmd['description']}[/dim]", box=box.ROUNDED, padding=(0, 2))) cons.print() # Examples @@ -475,6 +459,7 @@ def print_command_help(command: str, show_argparse: bool = True): # CONTEXTUAL TIPS # ============================================================================= + class Tips: """Contextual tips shown after various events.""" @@ -482,16 +467,18 @@ class Tips: def after_build_failure(): """Show tips after a build failure.""" cons.print() - cons.raw.print(Panel( - "[bold yellow]Troubleshooting Tips[/bold yellow]\n\n" - " [cyan]1.[/cyan] Rebuild with [green]--debug[/green] for debug compiler flags and verbose output\n" - " [cyan]2.[/cyan] Check [green]docs/documentation/troubleshooting.md[/green]\n" - " [cyan]3.[/cyan] Ensure required modules are loaded: [green]source ./mfc.sh load -c -m [/green]\n" - " [cyan]4.[/cyan] Try [green]./mfc.sh clean[/green] and rebuild", - box=box.ROUNDED, - border_style="yellow", - padding=(0, 2) - )) + cons.raw.print( + Panel( + "[bold yellow]Troubleshooting Tips[/bold yellow]\n\n" + " [cyan]1.[/cyan] Rebuild with [green]--debug[/green] for debug compiler flags and verbose output\n" + " [cyan]2.[/cyan] Check [green]docs/documentation/troubleshooting.md[/green]\n" + " [cyan]3.[/cyan] Ensure required modules are loaded: [green]source ./mfc.sh load -c -m [/green]\n" + " [cyan]4.[/cyan] Try [green]./mfc.sh clean[/green] and rebuild", + box=box.ROUNDED, + border_style="yellow", + padding=(0, 2), + ) + ) @staticmethod def after_case_error(case_path: str = None): @@ -528,16 +515,18 @@ def after_test_failure(failed_uuids: list = None): def after_run_failure(): """Show tips after a run failure.""" cons.print() - cons.raw.print(Panel( - "[bold yellow]Troubleshooting Tips[/bold yellow]\n\n" - " [cyan]1.[/cyan] Validate your case: [green]./mfc.sh validate case.py[/green]\n" - " [cyan]2.[/cyan] Check the output in [green]/[/green]\n" - " [cyan]3.[/cyan] Rebuild with [green]--debug[/green] for debug compiler flags\n" - " [cyan]4.[/cyan] Check MFC documentation: [green]docs/[/green]", - box=box.ROUNDED, - border_style="yellow", - padding=(0, 2) - )) + cons.raw.print( + Panel( + "[bold yellow]Troubleshooting Tips[/bold yellow]\n\n" + " [cyan]1.[/cyan] Validate your case: [green]./mfc.sh validate case.py[/green]\n" + " [cyan]2.[/cyan] Check the output in [green]/[/green]\n" + " [cyan]3.[/cyan] Rebuild with [green]--debug[/green] for debug compiler flags\n" + " [cyan]4.[/cyan] Check MFC documentation: [green]docs/[/green]", + box=box.ROUNDED, + border_style="yellow", + padding=(0, 2), + ) + ) @staticmethod def suggest_validate(): @@ -550,6 +539,7 @@ def suggest_validate(): # ONBOARDING FOR NEW USERS # ============================================================================= + def is_first_time_user() -> bool: """Check if this is a first-time user (no build directory).""" build_dir = os.path.join(MFC_ROOT_DIR, "build") @@ -559,27 +549,29 @@ def is_first_time_user() -> bool: def print_welcome(): """Print welcome message for new users.""" cons.print() - cons.raw.print(Panel( - "[bold cyan]Welcome to MFC![/bold cyan]\n\n" - "It looks like this is your first time using MFC. Here's how to get started:\n\n" - " [green]1.[/green] [bold]Load environment[/bold] (HPC clusters):\n" - " [cyan]source ./mfc.sh load -c -m [/cyan]\n" - " Example: [dim]source ./mfc.sh load -c p -m g[/dim] (Phoenix, GPU)\n\n" - " [green]2.[/green] [bold]Create a new case[/bold]:\n" - " [cyan]./mfc.sh new my_first_case[/cyan]\n\n" - " [green]3.[/green] [bold]Build MFC[/bold]:\n" - " [cyan]./mfc.sh build -j $(nproc)[/cyan]\n\n" - " [green]4.[/green] [bold]Run your simulation[/bold]:\n" - " [cyan]./mfc.sh run my_first_case/case.py[/cyan]\n\n" - "[bold yellow]Optional:[/bold yellow] Enable tab completion for your shell:\n" - " [cyan]./mfc.sh completion install[/cyan]\n\n" - "[dim]Run [cyan]./mfc.sh --help[/cyan] for all available commands[/dim]\n" - "[dim]Run [cyan]./mfc.sh interactive[/cyan] for a guided menu[/dim]", - title="[bold]Getting Started[/bold]", - box=box.DOUBLE, - border_style="cyan", - padding=(1, 2) - )) + cons.raw.print( + Panel( + "[bold cyan]Welcome to MFC![/bold cyan]\n\n" + "It looks like this is your first time using MFC. Here's how to get started:\n\n" + " [green]1.[/green] [bold]Load environment[/bold] (HPC clusters):\n" + " [cyan]source ./mfc.sh load -c -m [/cyan]\n" + " Example: [dim]source ./mfc.sh load -c p -m g[/dim] (Phoenix, GPU)\n\n" + " [green]2.[/green] [bold]Create a new case[/bold]:\n" + " [cyan]./mfc.sh new my_first_case[/cyan]\n\n" + " [green]3.[/green] [bold]Build MFC[/bold]:\n" + " [cyan]./mfc.sh build -j $(nproc)[/cyan]\n\n" + " [green]4.[/green] [bold]Run your simulation[/bold]:\n" + " [cyan]./mfc.sh run my_first_case/case.py[/cyan]\n\n" + "[bold yellow]Optional:[/bold yellow] Enable tab completion for your shell:\n" + " [cyan]./mfc.sh completion install[/cyan]\n\n" + "[dim]Run [cyan]./mfc.sh --help[/cyan] for all available commands[/dim]\n" + "[dim]Run [cyan]./mfc.sh interactive[/cyan] for a guided menu[/dim]", + title="[bold]Getting Started[/bold]", + box=box.DOUBLE, + border_style="cyan", + padding=(1, 2), + ) + ) cons.print() @@ -587,16 +579,13 @@ def print_welcome(): # INTERACTIVE MODE # ============================================================================= + def interactive_mode(): """Run interactive menu-driven interface.""" while True: cons.print() - cons.raw.print(Panel( - "[bold cyan]MFC Interactive Mode[/bold cyan]", - box=box.ROUNDED, - padding=(0, 2) - )) + cons.raw.print(Panel("[bold cyan]MFC Interactive Mode[/bold cyan]", box=box.ROUNDED, padding=(0, 2))) cons.print() # Menu options diff --git a/toolchain/mfc/validate.py b/toolchain/mfc/validate.py index 517144a71d..c07ffec8b3 100644 --- a/toolchain/mfc/validate.py +++ b/toolchain/mfc/validate.py @@ -3,12 +3,13 @@ """ import os +import sys -from .state import ARG +from .case_validator import CaseConstraintError, CaseValidator +from .common import MFCException from .printer import cons from .run import input as run_input -from .case_validator import CaseValidator, CaseConstraintError -from .common import MFCException +from .state import ARG def validate(): @@ -17,7 +18,7 @@ def validate(): if not os.path.isfile(input_file): cons.print(f"[bold red]Error:[/bold red] File not found: {input_file}") - exit(1) + sys.exit(1) cons.print(f"Validating [bold magenta]{input_file}[/bold magenta]...\n") @@ -28,7 +29,7 @@ def validate(): cons.print(f" [dim]Loaded {len(case.params)} parameters[/dim]") # Step 2: Run constraint validation for each stage - stages = ['pre_process', 'simulation', 'post_process'] + stages = ["pre_process", "simulation", "post_process"] all_passed = True for stage in stages: @@ -45,7 +46,7 @@ def validate(): 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'): + for line in str(e).split("\n"): if line.strip(): cons.print(f" [dim]{line}[/dim]") @@ -58,6 +59,6 @@ def validate(): 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("\n[bold red]✗ Validation failed:[/bold red]") cons.print(f"{e}") - exit(1) + sys.exit(1) diff --git a/toolchain/mfc/viz/_step_cache.py b/toolchain/mfc/viz/_step_cache.py index 20593d6973..4d23443528 100644 --- a/toolchain/mfc/viz/_step_cache.py +++ b/toolchain/mfc/viz/_step_cache.py @@ -40,7 +40,7 @@ def _get_prefetch_pool() -> ThreadPoolExecutor: """Return the prefetch pool, creating it lazily on first use.""" - global _prefetch_pool # pylint: disable=global-statement + global _prefetch_pool # noqa: PLW0603 with _prefetch_pool_lock: if _prefetch_pool is None: _prefetch_pool = ThreadPoolExecutor( @@ -117,7 +117,7 @@ def _bg_load(key: object, read_func: Callable) -> None: _cache.pop(evict, None) _cache[key] = data _cache_order.append(key) - except Exception: # pylint: disable=broad-except + except Exception: logger.debug("Prefetch failed for key %s", key, exc_info=True) finally: with _lock: diff --git a/toolchain/mfc/viz/interactive.py b/toolchain/mfc/viz/interactive.py index c27d530cfa..71c64f28f7 100644 --- a/toolchain/mfc/viz/interactive.py +++ b/toolchain/mfc/viz/interactive.py @@ -6,7 +6,6 @@ controls for slice position, isosurface thresholds, volume opacity, colormap, log scale, vmin/vmax, and timestep playback. """ -# pylint: disable=use-dict-literal,too-many-lines import atexit import base64 @@ -15,14 +14,15 @@ import math import threading import time -from typing import List, Callable, Optional +from typing import Callable, List, Optional import numpy as np import plotly.graph_objects as go -from dash import Dash, Patch, dcc, html, Input, Output, State, callback_context, no_update -from skimage.measure import marching_cubes as _marching_cubes # type: ignore[import] # pylint: disable=no-name-in-module +from dash import Dash, Input, Output, Patch, State, callback_context, dcc, html, no_update +from skimage.measure import marching_cubes as _marching_cubes # type: ignore[import] from mfc.printer import cons + from . import _step_cache from ._step_cache import prefetch_one as _prefetch_one @@ -63,7 +63,7 @@ def _get_jpeg_pool() -> concurrent.futures.ThreadPoolExecutor: """Return the JPEG prefetch pool, creating it lazily on first use.""" - global _jpeg_pool # pylint: disable=global-statement + global _jpeg_pool # noqa: PLW0603 with _jpeg_pool_lock: if _jpeg_pool is None: _jpeg_pool = concurrent.futures.ThreadPoolExecutor( @@ -76,10 +76,10 @@ def _get_lut(cmap_name: str) -> np.ndarray: """Return a (256, 3) uint8 LUT for the named matplotlib colormap.""" if cmap_name not in _lut_cache: try: - import matplotlib as mpl # pylint: disable=import-outside-toplevel + import matplotlib as mpl cm = mpl.colormaps.get_cmap(cmap_name) except (ImportError, KeyError): - import matplotlib.cm as mcm # pylint: disable=import-outside-toplevel + import matplotlib.cm as mcm cm = mcm.get_cmap(cmap_name) t = np.linspace(0.0, 1.0, 256) _lut_cache[cmap_name] = (cm(t)[:, :3] * 255 + 0.5).astype(np.uint8) @@ -112,8 +112,9 @@ def _encode_jpeg(rgb: np.ndarray) -> bytes: """ if _tj is not None: return _tj.encode(rgb, quality=90) - from PIL import Image as _PIL # pylint: disable=import-outside-toplevel - import io as _io # pylint: disable=import-outside-toplevel + import io as _io + + from PIL import Image as _PIL buf = _io.BytesIO() _PIL.fromarray(rgb, 'RGB').save(buf, format='jpeg', quality=90, optimize=False) return buf.getvalue() @@ -140,7 +141,7 @@ def _make_png_source(arr_yx: np.ndarray, cmap_name: str, return f"data:image/jpeg;base64,{b64}" -def _compute_isomesh(raw_ds: np.ndarray, x_ds: np.ndarray, y_ds: np.ndarray, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals +def _compute_isomesh(raw_ds: np.ndarray, x_ds: np.ndarray, y_ds: np.ndarray, z_ds: np.ndarray, log_fn, ilo: float, ihi: float, iso_n: int): """Server-side marching cubes for *iso_n* levels between *ilo* and *ihi*. @@ -182,7 +183,9 @@ def _compute_isomesh(raw_ds: np.ndarray, x_ds: np.ndarray, y_ds: np.ndarray, # verts[:, 0] += float(x_ds[0]) verts[:, 1] += float(y_ds[0]) verts[:, 2] += float(z_ds[0]) - xs.append(verts[:, 0]); ys.append(verts[:, 1]); zs.append(verts[:, 2]) + xs.append(verts[:, 0]) + ys.append(verts[:, 1]) + zs.append(verts[:, 2]) ii.append(faces[:, 0] + offset) jj.append(faces[:, 1] + offset) kk.append(faces[:, 2] + offset) @@ -222,7 +225,7 @@ def _downsample_3d(raw: np.ndarray, x_cc: np.ndarray, y_cc: np.ndarray, return raw[::s, ::s, ::s], x_cc[::s], y_cc[::s], z_cc[::s] -def _get_ds3(step, var, raw, x_cc, y_cc, z_cc, max_total): # pylint: disable=too-many-arguments,too-many-positional-arguments +def _get_ds3(step, var, raw, x_cc, y_cc, z_cc, max_total): """Downsampled 3D array with bounded LRU caching. Avoids re-striding the same large array on every iso threshold / volume @@ -245,7 +248,7 @@ def _get_ds3(step, var, raw, x_cc, y_cc, z_cc, max_total): # pylint: disable=to return result -def _prefetch_jpeg(step, var, get_ad_fn, cmap, vmin_in, vmax_in, log_bool, # pylint: disable=too-many-arguments,too-many-positional-arguments +def _prefetch_jpeg(step, var, get_ad_fn, cmap, vmin_in, vmax_in, log_bool, max_nx=1200, max_ny=600): """Pre-encode JPEG for *step/var* in a background thread. @@ -259,7 +262,7 @@ def _prefetch_jpeg(step, var, get_ad_fn, cmap, vmin_in, vmax_in, log_bool, # py if key in _jpeg_cache: return - def _bg(): # pylint: disable=too-many-locals + def _bg(): try: ad = get_ad_fn(step) if var not in ad.variables: @@ -298,7 +301,7 @@ def _bg(): # pylint: disable=too-many-locals if len(_jpeg_cache) >= _JPEG_CACHE_MAX: _jpeg_cache.pop(next(iter(_jpeg_cache))) _jpeg_cache[key] = src - except Exception: # pylint: disable=broad-except + except Exception: logger.debug("JPEG prefetch failed for step %s var %s", step, var, exc_info=True) _get_jpeg_pool().submit(_bg) @@ -366,7 +369,7 @@ def _lbl(text): }) -def _slider(sid, lo, hi, step, val, marks=None): # pylint: disable=too-many-arguments,too-many-positional-arguments +def _slider(sid, lo, hi, step, val, marks=None): return dcc.Slider( id=sid, min=lo, max=hi, step=step, value=val, marks=marks or {}, updatemode='mouseup', @@ -420,7 +423,7 @@ def _make_cbar(title_text: str, cmin: float, cmax: float, n: int = 6) -> dict: ) -def _slice_3d(raw, log_fn, x_cc, y_cc, z_cc, slice_axis, slice_pos, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals +def _slice_3d(raw, log_fn, x_cc, y_cc, z_cc, slice_axis, slice_pos, max_pts=(600, 300)): """Extract and downsample a 2-D slice from a 3-D array. @@ -449,7 +452,7 @@ def _slice_3d(raw, log_fn, x_cc, y_cc, z_cc, slice_axis, slice_pos, # pylint: d return sliced[::s1, ::s2], c1[::s1], c2[::s2], actual -def _build_3d(ad, raw, varname, step, mode, cmap, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements,unused-argument +def _build_3d(ad, raw, varname, step, mode, cmap, log_fn, cmin, cmax, cbar_title, slice_axis, slice_pos, iso_min_frac, iso_max_frac, iso_n, _iso_caps, @@ -557,7 +560,7 @@ def _build_3d(ad, raw, varname, step, mode, cmap, # pylint: disable=too-many-ar # Main entry point # --------------------------------------------------------------------------- -def run_interactive( # pylint: disable=too-many-locals,too-many-statements,too-many-arguments,too-many-positional-arguments +def run_interactive( varname: str, steps: List[int], read_func: Callable, @@ -874,7 +877,7 @@ def run_interactive( # pylint: disable=too-many-locals,too-many-statements,too- State('playing-st', 'data'), prevent_initial_call=True, ) - def _toggle_play(_, __, fps, is_playing): # pylint: disable=unused-argument + def _toggle_play(_, __, fps, is_playing): iv = max(int(1000 / max(float(fps or 2), 0.1)), 50) trig = (callback_context.triggered or [{}])[0].get('prop_id', '') if 'stop-btn' in trig: @@ -945,7 +948,7 @@ def _reset_range(_reset): Input('vmin-inp', 'value'), Input('vmax-inp', 'value'), ) - def _update(var_sel, step, mode, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches,too-many-statements + def _update(var_sel, step, mode, slice_axis, slice_pos, iso_min_frac, iso_max_frac, iso_n, iso_caps, vol_opacity, vol_nsurf, vol_min_frac, vol_max_frac, diff --git a/toolchain/mfc/viz/reader.py b/toolchain/mfc/viz/reader.py index a7f865d636..cda76766e2 100644 --- a/toolchain/mfc/viz/reader.py +++ b/toolchain/mfc/viz/reader.py @@ -25,7 +25,6 @@ import numpy as np - NAME_LEN = 50 # Fortran character length for variable names _READ_POOL: Optional[ThreadPoolExecutor] = None @@ -34,7 +33,7 @@ def _get_pool() -> ThreadPoolExecutor: """Return a persistent module-level thread pool, creating it on first use.""" - global _READ_POOL # pylint: disable=global-statement + global _READ_POOL # noqa: PLW0603 with _POOL_LOCK: if _READ_POOL is None: _READ_POOL = ThreadPoolExecutor( @@ -121,7 +120,7 @@ def _read_record_endian(f, endian: str) -> bytes: return payload -def read_binary_file(path: str, var_filter: Optional[str] = None) -> ProcessorData: # pylint: disable=too-many-locals,too-many-statements +def read_binary_file(path: str, var_filter: Optional[str] = None) -> ProcessorData: """ Read a single MFC binary post-process file. @@ -347,7 +346,7 @@ def _is_1d(case_dir: str) -> bool: and not os.path.isdir(p0)) -def assemble_from_proc_data( # pylint: disable=too-many-locals,too-many-statements +def assemble_from_proc_data( proc_data: List[Tuple[int, ProcessorData]], ) -> AssembledData: """ @@ -454,7 +453,7 @@ def _norm_round(arr, origin, extent): ) -def assemble(case_dir: str, step: int, fmt: str = 'binary', # pylint: disable=too-many-locals +def assemble(case_dir: str, step: int, fmt: str = 'binary', var: Optional[str] = None) -> AssembledData: """ Read and assemble multi-processor data for a given timestep. diff --git a/toolchain/mfc/viz/renderer.py b/toolchain/mfc/viz/renderer.py index a6f4d4edbd..2eec5635cb 100644 --- a/toolchain/mfc/viz/renderer.py +++ b/toolchain/mfc/viz/renderer.py @@ -11,17 +11,16 @@ import re import tempfile -import numpy as np - import imageio - import matplotlib +import numpy as np + try: matplotlib.use('Agg') except ValueError: pass -import matplotlib.pyplot as plt # pylint: disable=wrong-import-position -from matplotlib.colors import LogNorm # pylint: disable=wrong-import-position +import matplotlib.pyplot as plt # noqa: E402 +from matplotlib.colors import LogNorm # noqa: E402 matplotlib.rcParams.update({ 'mathtext.fontset': 'cm', @@ -83,8 +82,8 @@ def _overlay_bubbles(ax, bubbles, scale: float = 1.0) -> None: """ if bubbles is None or len(bubbles) == 0: return - from matplotlib.patches import Circle # pylint: disable=import-outside-toplevel - from matplotlib.collections import PatchCollection # pylint: disable=import-outside-toplevel + from matplotlib.collections import PatchCollection + from matplotlib.patches import Circle circles = [Circle((b[0], b[1]), b[3] * scale) for b in bubbles] pc = PatchCollection(circles, facecolors='none', edgecolors='white', linewidths=0.5, alpha=0.8) @@ -102,7 +101,7 @@ def pretty_label(varname): return varname -def render_1d(x_cc, data, varname, step, output, **opts): # pylint: disable=too-many-arguments,too-many-positional-arguments +def render_1d(x_cc, data, varname, step, output, **opts): """Render a 1D line plot and save as PNG.""" fig, ax = plt.subplots(figsize=opts.get('figsize', (10, 6))) label = pretty_label(varname) @@ -126,7 +125,7 @@ def render_1d(x_cc, data, varname, step, output, **opts): # pylint: disable=too plt.close(fig) -def render_1d_tiled(x_cc, variables, step, output, **opts): # pylint: disable=too-many-locals +def render_1d_tiled(x_cc, variables, step, output, **opts): """Render all 1D variables in a tiled subplot grid and save as PNG.""" varnames = sorted(variables.keys()) n = len(varnames) @@ -184,7 +183,7 @@ def _figsize_for_domain(x_cc, y_cc, base=10): return (fig_w, fig_h) -def render_2d(x_cc, y_cc, data, varname, step, output, **opts): # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals +def render_2d(x_cc, y_cc, data, varname, step, output, **opts): """Render a 2D colormap via pcolormesh and save as PNG.""" default_size = _figsize_for_domain(x_cc, y_cc) fig, ax = plt.subplots(figsize=opts.get('figsize', default_size)) @@ -223,7 +222,7 @@ def render_2d(x_cc, y_cc, data, varname, step, output, **opts): # pylint: disab plt.close(fig) -def render_2d_tiled(assembled, step, output, **opts): # pylint: disable=too-many-locals +def render_2d_tiled(assembled, step, output, **opts): """Render all 2D variables in a tiled subplot grid and save as PNG.""" varnames = sorted(assembled.variables.keys()) n = len(varnames) @@ -277,7 +276,7 @@ def render_2d_tiled(assembled, step, output, **opts): # pylint: disable=too-man plt.close(fig) -def render_3d_slice(assembled, varname, step, output, slice_axis='z', # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-statements,too-many-branches +def render_3d_slice(assembled, varname, step, output, slice_axis='z', slice_index=None, slice_value=None, **opts): """Extract a 2D slice from 3D data and render as a colormap.""" data_3d = assembled.variables[varname] @@ -362,7 +361,7 @@ def render_3d_slice(assembled, varname, step, output, slice_axis='z', # pylint: plt.close(fig) -def render_mp4(varname, steps, output, fps=10, # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-statements,too-many-branches +def render_mp4(varname, steps, output, fps=10, read_func=None, tiled=False, bubble_func=None, **opts): """ Generate an MP4 video by iterating over timesteps. @@ -440,7 +439,7 @@ def _cleanup(): pass try: - from tqdm import tqdm # pylint: disable=import-outside-toplevel + from tqdm import tqdm step_iter = tqdm(steps, desc='Rendering frames') except ImportError: step_iter = steps @@ -456,7 +455,7 @@ def _cleanup(): try: frame_opts = dict(opts, bubbles=bubble_func(step)) except (OSError, ValueError) as exc: - import warnings # pylint: disable=import-outside-toplevel + import warnings warnings.warn(f"Skipping bubble overlay for step {step}: {exc}", stacklevel=2) if tiled and assembled.ndim == 1: @@ -547,8 +546,8 @@ def _uniform_frame(arr): imageio.imread(os.path.join(viz_dir, fname)) )) success = True - except Exception as exc: # pylint: disable=broad-except - import warnings # pylint: disable=import-outside-toplevel + except Exception as exc: + import warnings warnings.warn(f"MP4 encoding error: {exc}", stacklevel=2) finally: _cleanup() diff --git a/toolchain/mfc/viz/silo_reader.py b/toolchain/mfc/viz/silo_reader.py index 287a14c197..8e6114f94d 100644 --- a/toolchain/mfc/viz/silo_reader.py +++ b/toolchain/mfc/viz/silo_reader.py @@ -135,7 +135,7 @@ def _resolve_path(h5file, path_bytes): return np.array(h5file[path]) -def read_silo_file( # pylint: disable=too-many-locals +def read_silo_file( path: str, var_filter: Optional[str] = None, rank_dir: Optional[str] = None, @@ -199,7 +199,7 @@ def read_silo_file( # pylint: disable=too-many-locals def _get_pool() -> ThreadPoolExecutor: """Return a module-level thread pool, creating it on first use.""" - global _READ_POOL # pylint: disable=global-statement + global _READ_POOL # noqa: PLW0603 with _POOL_LOCK: if _READ_POOL is None: _READ_POOL = ThreadPoolExecutor( diff --git a/toolchain/mfc/viz/test_viz.py b/toolchain/mfc/viz/test_viz.py index 7fe72096a4..cb55251cd1 100644 --- a/toolchain/mfc/viz/test_viz.py +++ b/toolchain/mfc/viz/test_viz.py @@ -5,13 +5,11 @@ data assembly (binary + silo, 1D/2D/3D), and 1D rendering. Uses checked-in fixture data generated from minimal MFC runs. """ -# pylint: disable=import-outside-toplevel,protected-access import os import tempfile import unittest - FIXTURES = os.path.join(os.path.dirname(__file__), 'fixtures') # Fixture paths for each dimension + format @@ -257,6 +255,7 @@ def test_cell_count_after_dedup(self): def test_grid_is_sorted_and_unique(self): """Assembled global grid is strictly increasing with no duplicates.""" import numpy as np + from .reader import assemble data = assemble(FIX_1D_BIN_2RANK, 0, 'binary') diffs = np.diff(data.x_cc) @@ -265,6 +264,7 @@ def test_grid_is_sorted_and_unique(self): def test_variable_values_match_position(self): """pres values (== x_cc position) are placed at the correct global cells.""" import numpy as np + from .reader import assemble data = assemble(FIX_1D_BIN_2RANK, 0, 'binary') np.testing.assert_allclose(data.variables['pres'], data.x_cc, atol=1e-10) @@ -383,9 +383,10 @@ class TestBinarySiloConsistency(unittest.TestCase): def test_1d_same_grid(self): """Binary and silo 1D fixtures have the same grid.""" + import numpy as np + from .reader import assemble from .silo_reader import assemble_silo - import numpy as np bin_data = assemble(FIX_1D_BIN, 0, 'binary') silo_data = assemble_silo(FIX_1D_SILO, 0) np.testing.assert_allclose(bin_data.x_cc, silo_data.x_cc, atol=1e-10) @@ -402,6 +403,7 @@ def test_1d_same_vars(self): def test_1d_same_values(self): """Binary and silo 1D fixtures have the same variable values.""" import numpy as np + from .reader import assemble from .silo_reader import assemble_silo bin_data = assemble(FIX_1D_BIN, 0, 'binary') @@ -651,6 +653,7 @@ class TestMultiRankAssembly(unittest.TestCase): def _make_proc(self, x_cb, pres): """Build a minimal 1D ProcessorData from boundary coordinates.""" import numpy as np + from .reader import ProcessorData return ProcessorData( m=len(x_cb) - 1, @@ -665,6 +668,7 @@ def _make_proc(self, x_cb, pres): def test_two_rank_1d_dedup(self): """Two processors with one overlapping ghost cell assemble correctly.""" import numpy as np + from .reader import assemble_from_proc_data # Domain: 4 cells with centers at 0.125, 0.375, 0.625, 0.875 # Proc 0 sees cells 0-2 (center 0.625 is ghost from proc 1) @@ -684,6 +688,7 @@ def test_two_rank_1d_dedup(self): def test_large_extent_dedup(self): """Deduplication works correctly for large-extent domains (>1e6).""" import numpy as np + from .reader import assemble_from_proc_data # Scale up by 1e7: extent=1e7, decimals = ceil(-log10(1e7)) + 12 = 5 scale = 1e7 @@ -710,6 +715,7 @@ def test_very_large_extent_dedup_negative_decimals(self): are >> 10, so distinct cell-centers must not be collapsed. """ import numpy as np + from .reader import assemble_from_proc_data scale = 1e13 p0 = self._make_proc( diff --git a/toolchain/mfc/viz/tui.py b/toolchain/mfc/viz/tui.py index 3efd34b053..dd99b3adab 100644 --- a/toolchain/mfc/viz/tui.py +++ b/toolchain/mfc/viz/tui.py @@ -14,12 +14,10 @@ from typing import Callable, List, Optional, Tuple import numpy as np - from rich.color import Color as RichColor from rich.console import Group as RichGroup from rich.style import Style from rich.text import Text as RichText - from textual import on, work from textual.app import App, ComposeResult from textual.binding import Binding @@ -27,14 +25,20 @@ from textual.message import Message from textual.reactive import reactive from textual.widgets import ( - Digits, Footer, Header, Label, ListItem, ListView, Static, + Digits, + Footer, + Header, + Label, + ListItem, + ListView, + Static, ) from textual.worker import get_current_worker - from textual_plotext import PlotextPlot from mfc.common import MFCException from mfc.printer import cons + from . import _step_cache # Colormaps available via [c] cycling @@ -64,7 +68,7 @@ # Plot widget # --------------------------------------------------------------------------- -class MFCPlot(PlotextPlot): # pylint: disable=too-many-instance-attributes,too-few-public-methods +class MFCPlot(PlotextPlot): """Plotext plot widget. Caller sets ._x_cc / ._y_cc / ._data / ._ndim / ._varname / ._step before calling .refresh().""" @@ -120,7 +124,7 @@ def reset_zoom(self) -> None: self._zoom = (0.0, 1.0, 0.0, 1.0) self.refresh() - def _zoom_around( # pylint: disable=too-many-locals + def _zoom_around( self, cx_frac: float, cy_frac: float, factor: float ) -> None: """Zoom by *factor* centred at *(cx_frac, cy_frac)* in [0,1]² of current view.""" @@ -169,7 +173,7 @@ def on_mouse_scroll_up(self, event) -> None: # type: ignore[override] def on_mouse_scroll_down(self, event) -> None: # type: ignore[override] self._scroll_zoom(event, factor=1.0 / 0.75) - def on_mouse_up(self, event) -> None: # pylint: disable=too-many-locals + def on_mouse_up(self, event) -> None: """Feature 5 — post Clicked message with the data value at the heatmap cell.""" if event.button != 1: return @@ -188,17 +192,17 @@ def on_mouse_up(self, event) -> None: # pylint: disable=too-many-locals ix_pos = int(np.round(col * (n_ix - 1) / max(self._last_w_map - 1, 1))) # Display is y-flipped: row 0 = top = y_max. iy_pos = n_iy - 1 - int(np.round(row * (n_iy - 1) / max(self._last_h_plot - 1, 1))) - xi = int(self._last_ix[np.clip(ix_pos, 0, n_ix - 1)]) # pylint: disable=unsubscriptable-object - yi = int(self._last_iy[np.clip(iy_pos, 0, n_iy - 1)]) # pylint: disable=unsubscriptable-object + xi = int(self._last_ix[np.clip(ix_pos, 0, n_ix - 1)]) + yi = int(self._last_iy[np.clip(iy_pos, 0, n_iy - 1)]) x_cc = self._x_cc y_cc = self._y_cc if self._y_cc is not None else np.array([0.0, 1.0]) data = self._data - x_val = float(x_cc[xi]) # type: ignore[index] # pylint: disable=unsubscriptable-object + x_val = float(x_cc[xi]) # type: ignore[index] y_val = float(y_cc[yi]) - val = float(data[xi, yi]) # type: ignore[index] # pylint: disable=unsubscriptable-object + val = float(data[xi, yi]) # type: ignore[index] self.post_message(MFCPlot.Clicked(x_val, y_val, val)) - def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many-statements + def render(self): data = self._data x_cc = self._x_cc self.plt.clear_figure() @@ -232,8 +236,8 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- return super().render() # 2D: pure-Rich heatmap with vertical colorbar. - import matplotlib # pylint: disable=import-outside-toplevel - import matplotlib.colors as mcolors # pylint: disable=import-outside-toplevel + import matplotlib + import matplotlib.colors as mcolors # Content area = widget size minus 1-char border on each side. # Reserve 1 row each for header and footer → h_plot rows for the image. @@ -247,7 +251,7 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- # Preserve the physical x/y aspect ratio. y_cc_2d = self._y_cc if self._y_cc is not None else np.array([0.0, 1.0]) - x_extent = max(abs(float(x_cc[-1]) - float(x_cc[0])), 1e-30) # pylint: disable=unsubscriptable-object + x_extent = max(abs(float(x_cc[-1]) - float(x_cc[0])), 1e-30) y_extent = max(abs(float(y_cc_2d[-1]) - float(y_cc_2d[0])), 1e-30) domain_ratio = float(np.clip(x_extent / y_extent, _ASPECT_MIN, _ASPECT_MAX)) char_ratio = domain_ratio * _CELL_RATIO @@ -275,19 +279,19 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- self._last_ix = ix self._last_iy = iy - ds = data[np.ix_(ix, iy)] # pylint: disable=unsubscriptable-object + ds = data[np.ix_(ix, iy)] # Compute which screen cells to stamp with an open-circle glyph. bubble_cells: set = set() bubbles = self._bubbles if bubbles is not None and len(bubbles) > 0: - x_phys = x_cc[ix] # type: ignore[index] # pylint: disable=unsubscriptable-object + x_phys = x_cc[ix] # type: ignore[index] y_phys = y_cc_2d[iy] x_min, x_max = float(x_phys[0]), float(x_phys[-1]) y_min, y_max = float(y_phys[0]), float(y_phys[-1]) x_range = max(abs(x_max - x_min), 1e-30) y_range = max(abs(y_max - y_min), 1e-30) - for b in bubbles: # pylint: disable=not-an-iterable + for b in bubbles: bx, by, br = float(b[0]), float(b[1]), float(b[3]) if bx < x_min - br or bx > x_max + br: continue @@ -374,8 +378,8 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- style="bold" ) # Show the visible coordinate range (reflects zoom when active). - x_lo = float(x_cc[ix[0]]) # type: ignore[index] # pylint: disable=unsubscriptable-object - x_hi = float(x_cc[ix[-1]]) # type: ignore[index] # pylint: disable=unsubscriptable-object + x_lo = float(x_cc[ix[0]]) # type: ignore[index] + x_hi = float(x_cc[ix[-1]]) # type: ignore[index] y_vis = y_cc_2d[iy] footer = RichText( f" x: [{x_lo:.3f} \u2026 {x_hi:.3f}]" @@ -389,7 +393,7 @@ def render(self): # pylint: disable=too-many-branches,too-many-locals,too-many- # Main TUI app # --------------------------------------------------------------------------- -class MFCTuiApp(App): # pylint: disable=too-many-instance-attributes +class MFCTuiApp(App): """Textual TUI for MFC post-processed data.""" CSS = """ @@ -451,7 +455,7 @@ class MFCTuiApp(App): # pylint: disable=too-many-instance-attributes log_scale: reactive[bool] = reactive(False, always_update=True) playing: reactive[bool] = reactive(False, always_update=True) - def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + def __init__( self, steps: List[int], varnames: List[str], @@ -515,10 +519,9 @@ def watch_log_scale(self, _old: bool, _new: bool) -> None: def watch_playing(self, _old: bool, new: bool) -> None: if new: self._play_timer = self.set_interval(0.5, self._auto_advance) - else: - if self._play_timer is not None: - self._play_timer.stop() - self._play_timer = None + elif self._play_timer is not None: + self._play_timer.stop() + self._play_timer = None # ------------------------------------------------------------------ # MFCPlot.Clicked handler — update status bar (Feature 5) @@ -573,7 +576,7 @@ def _push_data(self) -> None: self._apply_data, assembled, data, step, var, cmap, log, frozen, bubbles, ) - def _apply_data( # pylint: disable=too-many-arguments,too-many-positional-arguments + def _apply_data( self, assembled, data: Optional[np.ndarray], @@ -586,20 +589,20 @@ def _apply_data( # pylint: disable=too-many-arguments,too-many-positional-argum ) -> None: """Apply loaded data to the plot widget. Runs on the main thread.""" plot = self.query_one("#plot", MFCPlot) - plot._x_cc = assembled.x_cc # pylint: disable=protected-access - plot._y_cc = assembled.y_cc # pylint: disable=protected-access - plot._data = data # pylint: disable=protected-access - plot._ndim = self._ndim # pylint: disable=protected-access - plot._varname = var # pylint: disable=protected-access - plot._step = step # pylint: disable=protected-access - plot._cmap_name = cmap # pylint: disable=protected-access - plot._log_scale = log # pylint: disable=protected-access - plot._bubbles = bubbles # pylint: disable=protected-access + plot._x_cc = assembled.x_cc + plot._y_cc = assembled.y_cc + plot._data = data + plot._ndim = self._ndim + plot._varname = var + plot._step = step + plot._cmap_name = cmap + plot._log_scale = log + plot._bubbles = bubbles if frozen is not None: - plot._vmin, plot._vmax = frozen # pylint: disable=protected-access + plot._vmin, plot._vmax = frozen else: - plot._vmin = None # pylint: disable=protected-access - plot._vmax = None # pylint: disable=protected-access + plot._vmin = None + plot._vmax = None plot.refresh() # Update step counter (Feature 2). @@ -660,7 +663,7 @@ def action_toggle_freeze(self) -> None: self._frozen_range = None else: plot = self.query_one("#plot", MFCPlot) - self._frozen_range = (plot._last_vmin, plot._last_vmax) # pylint: disable=protected-access + self._frozen_range = (plot._last_vmin, plot._last_vmax) self._push_data() def action_toggle_play(self) -> None: diff --git a/toolchain/mfc/viz/viz.py b/toolchain/mfc/viz/viz.py index 5b935c44a6..18a5dfcdcf 100644 --- a/toolchain/mfc/viz/viz.py +++ b/toolchain/mfc/viz/viz.py @@ -7,11 +7,9 @@ import os import warnings -from mfc.state import ARG from mfc.common import MFCException from mfc.printer import cons - - +from mfc.state import ARG _CMAP_POPULAR = ( 'viridis, plasma, inferno, magma, turbo, ' @@ -21,11 +19,11 @@ def _validate_cmap(name): """Raise a helpful MFCException if *name* is not a known matplotlib colormap.""" - import matplotlib # pylint: disable=import-outside-toplevel + import matplotlib if name in matplotlib.colormaps: return try: - from rapidfuzz import process # pylint: disable=import-outside-toplevel + from rapidfuzz import process matches = process.extract(name, list(matplotlib.colormaps), limit=3) suggestions = ', '.join(m[0] for m in matches) hint = f" Did you mean: {suggestions}?" @@ -166,11 +164,11 @@ def _parse_steps(step_arg, available_steps): return [single], 1 -def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branches +def viz(): """Main viz command dispatcher.""" - from .reader import discover_format, discover_timesteps, assemble, has_lag_bubble_evol, read_lag_bubbles_at_step # pylint: disable=import-outside-toplevel - from .renderer import render_1d, render_1d_tiled, render_2d, render_2d_tiled, render_3d_slice, render_mp4 # pylint: disable=import-outside-toplevel + from .reader import assemble, discover_format, discover_timesteps, has_lag_bubble_evol, read_lag_bubbles_at_step + from .renderer import render_1d, render_1d_tiled, render_2d, render_2d_tiled, render_3d_slice, render_mp4 case_dir = ARG('input') if case_dir is None: @@ -248,7 +246,7 @@ def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branc f"Available steps: {_steps_hint(steps)}") if fmt == 'silo': - from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel + from .silo_reader import assemble_silo assembled = assemble_silo(case_dir, step) else: assembled = assemble(case_dir, step, fmt) @@ -301,14 +299,14 @@ def viz(): # pylint: disable=too-many-locals,too-many-statements,too-many-branc def read_step(step): if fmt == 'silo': - from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel + from .silo_reader import assemble_silo return assemble_silo(case_dir, step, var=None if load_all else varname) return assemble(case_dir, step, fmt, var=None if load_all else varname) def read_step_one_var(step, var): """Read a single variable for a step — used by interactive mode.""" if fmt == 'silo': - from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel + from .silo_reader import assemble_silo return assemble_silo(case_dir, step, var=var) return assemble(case_dir, step, fmt, var=var) @@ -340,7 +338,7 @@ def read_step_one_var(step, var): # to build a useful "available variables" list for the error message. if not avail: if fmt == 'silo': - from .silo_reader import assemble_silo # pylint: disable=import-outside-toplevel + from .silo_reader import assemble_silo _full = assemble_silo(case_dir, requested_steps[0]) else: _full = assemble(case_dir, requested_steps[0], fmt) @@ -359,7 +357,7 @@ def read_step_one_var(step, var): "Terminal UI only supports 1D and 2D data. " "Use --interactive for 3D data." ) - from .tui import run_tui # pylint: disable=import-outside-toplevel + from .tui import run_tui init_var = varname if varname in avail else (avail[0] if avail else None) run_tui(init_var, requested_steps, read_step, ndim=test_assembled.ndim, bubble_func=bubble_func) @@ -374,7 +372,7 @@ def read_step_one_var(step, var): if ignored: cons.print(f"[yellow]Warning:[/yellow] {', '.join('--' + f.replace('_', '-') for f in ignored)} " "ignored in --interactive mode (use the UI controls instead).") - from .interactive import run_interactive # pylint: disable=import-outside-toplevel + from .interactive import run_interactive port = ARG('port') host = ARG('host') # Default to first available variable if --var was not specified @@ -445,7 +443,7 @@ def read_step_one_var(step, var): # Single or multiple PNG frames try: - from tqdm import tqdm # pylint: disable=import-outside-toplevel + from tqdm import tqdm step_iter = tqdm(requested_steps, desc='Rendering') except ImportError: step_iter = requested_steps diff --git a/toolchain/pyproject.toml b/toolchain/pyproject.toml index f0b3a85dd0..9581eea2b9 100644 --- a/toolchain/pyproject.toml +++ b/toolchain/pyproject.toml @@ -23,9 +23,8 @@ dependencies = [ # Code Health "typos", - "pylint", + "ruff", "fprettify", - "autopep8", # Python formatter (black has issues with Python 3.12.5) "ansi2txt", # Profiling