diff --git a/.claude/rules/common-pitfalls.md b/.claude/rules/common-pitfalls.md index 858d52a1f8..8f3a0edf43 100644 --- a/.claude/rules/common-pitfalls.md +++ b/.claude/rules/common-pitfalls.md @@ -59,7 +59,7 @@ Before submitting a PR: - [ ] `./mfc.sh precheck -j 8` (5 CI lint checks) - [ ] `./mfc.sh build -j 8` (compiles) - [ ] `./mfc.sh test --only -j 8` (tests pass) -- [ ] If adding parameters: all 4 locations updated +- [ ] If adding parameters: definitions.py (_r + _nv) updated; cmake reconfigured; case_validator.py if constraints - [ ] If modifying `src/common/`: all three targets tested - [ ] If changing output: golden files regenerated for affected tests - [ ] One logical change per commit diff --git a/.claude/rules/parameter-system.md b/.claude/rules/parameter-system.md index 28f020090a..d3f6ea99cf 100644 --- a/.claude/rules/parameter-system.md +++ b/.claude/rules/parameter-system.md @@ -23,20 +23,22 @@ MFC has ~3,400 simulation parameters defined in Python and read by Fortran via n - Reads `&user_inputs` namelist - Each parameter must be declared in the namelist statement -## Adding a New Parameter (4-location checklist) +## Adding a New Parameter (2-location checklist) -YOU MUST update the first 3 locations. Missing any causes silent failures or compile errors. -Location 4 is required only if the parameter has physics constraints. +Fortran declarations and namelist bindings are now auto-generated from definitions.py +at CMake configure time — no manual Fortran edits needed for simple scalar parameters. -1. **`toolchain/mfc/params/definitions.py`**: Add parameter with type, default, constraints -2. **`src/*/m_global_parameters.fpp`**: Declare the Fortran variable in the relevant - target(s). If the param is used by simulation only, add it there. If shared, add to - all three targets' m_global_parameters.fpp. -3. **`src/*/m_start_up.fpp`**: Add to the Fortran `namelist` declaration in the relevant - target(s). -4. **`toolchain/mfc/case_validator.py`**: Add validation rules if the parameter has +1. **`toolchain/mfc/params/definitions.py`**: Add parameter with `_r()` (type, default, + constraints) AND add it to `NAMELIST_VARS` via `_nv()` for the relevant target(s). + After editing, re-run cmake (or `./mfc.sh build`) to regenerate the Fortran includes. +2. **`toolchain/mfc/case_validator.py`**: Add validation rules if the parameter has physics constraints. Include `PHYSICS_DOCS` entry with title, category, explanation. +**Exceptions — still require manual Fortran edits:** +- Array variables (e.g. `logical, dimension(num_fluids_max)`) → declare in `src/*/m_global_parameters.fpp` +- Derived-type members (`fluid_pp%attr`, `patch_icpp(i)%attr`) → declare in the relevant derived type +- Case-optimization parameters → add to `CASE_OPT_PARAMS` and the `#:else` block in `src/simulation/m_global_parameters.fpp` + ## Case Files - Case files are Python scripts (`.py`) that define a dict of parameters - Validated with `./mfc.sh validate case.py` diff --git a/CLAUDE.md b/CLAUDE.md index 928ca9ce27..497e0815e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -139,11 +139,12 @@ NEVER use `stop` or `error stop`. Use `call s_mpi_abort()` or `@:PROHIBIT()`/`@: NEVER use `goto`, `COMMON` blocks, or global `save` variables. Every `@:ALLOCATE(...)` MUST have a matching `@:DEALLOCATE(...)`. -Every new parameter MUST be added in at least 3 places (4 if it has constraints): - 1. `toolchain/mfc/params/definitions.py` (parameter definition) - 2. Fortran variable declaration in `src/*/m_global_parameters.fpp` - 3. Fortran namelist in `src/*/m_start_up.fpp` (namelist binding) - 4. `toolchain/mfc/case_validator.py` (only if parameter has physics constraints) +Every new parameter MUST be added in at least 2 places (3 if it has constraints): + 1. `toolchain/mfc/params/definitions.py` (parameter definition + NAMELIST_VARS target set) + 2. `toolchain/mfc/case_validator.py` (only if parameter has physics constraints) + Note: Fortran declarations and namelist bindings are auto-generated from definitions.py + at CMake configure time. Simple scalars need no manual Fortran edits. Array/derived-type + variables still require a manual declaration in `src/*/m_global_parameters.fpp`. Changes to `src/common/` affect ALL three executables. Test comprehensively. diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a44aca223..83bbb8fe0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -461,6 +461,39 @@ macro(HANDLE_SOURCES target useCommon) endmacro() +# Generate Fortran parameter namelist/decl includes into the per-target build +# include directories before HANDLE_SOURCES globs them for Fypp. +find_package(Python3 REQUIRED COMPONENTS Interpreter) +set(_mfc_gen_stamp "${CMAKE_BINARY_DIR}/mfc_params_gen.stamp") +file(GLOB_RECURSE _mfc_gen_inputs + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/*.py" +) +set(_mfc_needs_regen FALSE) +if(NOT EXISTS "${_mfc_gen_stamp}") + set(_mfc_needs_regen TRUE) +else() + foreach(_input IN LISTS _mfc_gen_inputs) + if("${_input}" IS_NEWER_THAN "${_mfc_gen_stamp}") + set(_mfc_needs_regen TRUE) + break() + endif() + endforeach() +endif() +if(_mfc_needs_regen) + execute_process( + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/toolchain/mfc/params/generators/cmake_gen.py" + "${CMAKE_BINARY_DIR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE _mfc_gen_result + ERROR_VARIABLE _mfc_gen_error + ) + if(NOT _mfc_gen_result EQUAL 0) + message(FATAL_ERROR "Fortran param generation failed:\n${_mfc_gen_error}") + endif() + file(TOUCH "${_mfc_gen_stamp}") +endif() + HANDLE_SOURCES(pre_process ON) HANDLE_SOURCES(simulation ON) HANDLE_SOURCES(post_process ON) diff --git a/src/post_process/m_global_parameters.fpp b/src/post_process/m_global_parameters.fpp index aed6e616c4..b9673f354c 100644 --- a/src/post_process/m_global_parameters.fpp +++ b/src/post_process/m_global_parameters.fpp @@ -17,10 +17,11 @@ module m_global_parameters implicit none + #:include 'generated_decls.fpp' + !> @name Logistics !> @{ - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location + integer :: num_procs !< Number of processors !> @} ! Computational Domain Parameters @@ -28,9 +29,7 @@ module m_global_parameters integer :: proc_rank !< Rank of the local processor !> @name Number of cells in the x-, y- and z-coordinate directions !> @{ - integer :: m, m_root - integer :: n - integer :: p + integer :: m_root !> @} !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- @@ -39,7 +38,6 @@ module m_global_parameters !> @name Cylindrical coordinates (either axisymmetric or full 3D) !> @{ - logical :: cyl_coord integer :: grid_geometry !> @} @@ -66,50 +64,24 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: dx, dy, dz !> @} - integer :: buff_size !< Number of ghost cells for boundary condition storage - integer :: t_step_start !< First time-step directory - integer :: t_step_stop !< Last time-step directory - integer :: t_step_save !< Interval between consecutive time-step directory + integer :: buff_size !< Number of ghost cells for boundary condition storage !> @name IO options for adaptive time-stepping !> @{ - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - real(wp) :: t_save - real(wp) :: t_stop - real(wp) :: cfl_target - integer :: n_save - integer :: n_start + logical :: cfl_dt + integer :: n_save !> @} ! NOTE: m_root, x_root_cb, x_root_cc = defragmented grid (1D only; equals m, x_cb, x_cc in serial) !> @name Simulation Algorithm Parameters !> @{ - integer :: model_eqns !< Multicomponent flow model - integer :: num_fluids !< Number of different fluids present in the flow - logical :: relax !< phase change - integer :: relax_model !< Phase change relaxation model - logical :: mpp_lim !< Maximum volume fraction limiter integer :: sys_size !< Number of unknowns in the system of equations - integer :: recon_type !< Which type of reconstruction to use - integer :: weno_order !< Order of accuracy for the WENO reconstruction - integer :: muscl_order !< Order of accuracy for the MUSCL reconstruction - logical :: mixture_err !< Mixture error limiter - logical :: alt_soundspeed !< Alternate sound speed - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity for RMHD - logical :: hypoelasticity !< Turn hypoelasticity on - logical :: hyperelasticity !< Turn hyperelasticity on logical :: elasticity !< elasticity modeling, true for hyper or hypo integer :: b_size !< Number of components in the b tensor integer :: tensor_size !< Number of components in the nonsymmetric tensor - logical :: cont_damage !< Continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD - logical :: igr !< enable IGR - integer :: igr_order !< IGR reconstruction order logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling !> @} - integer :: avg_state !< Average state evaluation method !> @name Annotations of the structure, i.e. the organization, of the state vectors !> @{ type(eqn_idx_info) :: eqn_idx !< All conserved-variable equation index ranges and scalars. @@ -122,7 +94,6 @@ module m_global_parameters ! Cell indices (InDices With BUFFer): includes buffer in simulation only type(int_bounds_info) :: idwbuff(1:3) - integer :: num_bc_patches logical :: bc_io !> @name Boundary conditions in the x-, y- and z-coordinate directions !> @{ @@ -133,12 +104,8 @@ module m_global_parameters integer, dimension(3) :: shear_indices !< Indices of the stress components that represent shear stress integer :: shear_BC_flip_num !< Number of shear stress components to reflect for boundary conditions integer, dimension(3, 2) :: shear_BC_flip_indices !< Shear stress BC reflection indices (1:3, 1:shear_BC_flip_num) - logical :: parallel_io !< Format of the data files - logical :: sim_data - logical :: file_per_process !< output format integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid - integer :: num_ibs !< Number of immersed boundaries #ifdef MFC_MPI type(mpi_io_var), public :: MPI_IO_DATA type(mpi_io_ib_var), public :: MPI_IO_IB_DATA @@ -159,10 +126,6 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: adv !< Advection variables ! Formatted Database File(s) Structure Parameters - integer :: format !< Format of the database file(s) - integer :: precision !< Floating point precision of the database file(s) - logical :: down_sample !< down sampling of the database file(s) - logical :: output_partial_domain !< Specify portion of domain to output for post-processing type(bounds_info) :: x_output, y_output, z_output !< Portion of domain to output for post-processing type(int_bounds_info) :: x_output_idx, y_output_idx, z_output_idx !< Indices of domain to output for post-processing !> @name Size of the ghost zone layer in the x-, y- and z-coordinate directions. The definition of the ghost zone layers is only @@ -172,101 +135,23 @@ module m_global_parameters type(int_bounds_info) :: offset_x, offset_y, offset_z !> @} - !> @name The list of all possible flow variables that may be written to a database file. It includes partial densities, density, - !! momentum, velocity, energy, pressure, volume fraction(s), specific heat ratio function, specific heat ratio, liquid stiffness - !! function, liquid stiffness, primitive variables, conservative variables, speed of sound, the vorticity, and the numerical - !! Schlieren function. - !> @{ - logical, dimension(num_fluids_max) :: alpha_rho_wrt - logical :: rho_wrt - logical, dimension(3) :: mom_wrt - logical, dimension(3) :: vel_wrt - integer :: flux_lim - logical, dimension(3) :: flux_wrt - logical :: E_wrt - logical, dimension(num_fluids_max) :: alpha_rho_e_wrt - logical :: fft_wrt - logical :: pres_wrt - logical, dimension(num_fluids_max) :: alpha_wrt - logical :: gamma_wrt - logical :: heat_ratio_wrt - logical :: pi_inf_wrt - logical :: pres_inf_wrt - logical :: prim_vars_wrt - logical :: cons_vars_wrt - logical :: c_wrt - logical, dimension(3) :: omega_wrt - logical :: qm_wrt - logical :: liutex_wrt - logical :: schlieren_wrt - logical :: cf_wrt - logical :: ib - logical :: ib_state_wrt - logical :: chem_wrt_Y(1:num_species) - logical :: chem_wrt_T - logical :: lag_header - logical :: lag_txt_wrt - logical :: lag_db_wrt - logical :: lag_id_wrt - logical :: lag_pos_wrt - logical :: lag_pos_prev_wrt - logical :: lag_vel_wrt - logical :: lag_rad_wrt - logical :: lag_rvel_wrt - logical :: lag_r0_wrt - logical :: lag_rmax_wrt - logical :: lag_rmin_wrt - logical :: lag_dphidt_wrt - logical :: lag_pres_wrt - logical :: lag_mv_wrt - logical :: lag_mg_wrt - logical :: lag_betaT_wrt - logical :: lag_betaC_wrt - !> @} - - real(wp), dimension(num_fluids_max) :: schlieren_alpha !< Per-fluid Schlieren intensity amplitude coefficients - integer :: fd_order !< Finite-difference order for vorticity and Schlieren derivatives - integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) - !> @name Reference parameters for Tait EOS - !> @{ - real(wp) :: rhoref, pref - !> @} - + ! alpha_rho_wrt, mom_wrt, vel_wrt, flux_wrt, alpha_rho_e_wrt, alpha_wrt, + ! omega_wrt, chem_wrt_Y, schlieren_alpha: auto-generated in generated_decls.fpp + integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) type(chemistry_parameters) :: chem_params !> @name Bubble modeling variables and parameters !> @{ - integer :: nb - real(wp) :: Eu, Ca, Web, Re_inv + real(wp) :: Eu real(wp), dimension(:), allocatable :: weight, R0 - logical :: bubbles_euler - logical :: qbmm - logical :: polytropic - logical :: polydisperse - logical :: adv_n - integer :: thermal !< 1 = adiabatic, 2 = isotherm, 3 = transfer real(wp) :: phi_vg, phi_gv, Pe_c, Tw, k_vl, k_gl real(wp) :: gam_m real(wp), dimension(:), allocatable :: pb0, mass_g0, mass_v0, Pe_T, k_v, k_g real(wp), dimension(:), allocatable :: Re_trans_T, Re_trans_c, Im_trans_T, Im_trans_c, omegaN - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g real(wp) :: G - real(wp) :: poly_sigma - real(wp) :: sigR integer :: nmom !> @} - !> @name surface tension coefficient - !> @{ - real(wp) :: sigma - logical :: surface_tension - !> @} - - !> @name Lagrangian bubbles - !> @{ - logical :: bubbles_lagrange - !> @} - - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) real(wp) :: wall_time, wall_time_avg !< Wall time measurements contains diff --git a/src/post_process/m_start_up.fpp b/src/post_process/m_start_up.fpp index ce51702f93..708234797b 100644 --- a/src/post_process/m_start_up.fpp +++ b/src/post_process/m_start_up.fpp @@ -59,18 +59,7 @@ contains integer :: iostatus character(len=1000) :: line - namelist /user_inputs/ case_dir, m, n, p, t_step_start, t_step_stop, t_step_save, model_eqns, num_fluids, mpp_lim, & - & weno_order, bc_x, bc_y, bc_z, fluid_pp, bub_pp, format, precision, output_partial_domain, x_output, y_output, & - & z_output, hypoelasticity, G, mhd, chem_wrt_Y, chem_wrt_T, avg_state, alpha_rho_wrt, rho_wrt, mom_wrt, vel_wrt, & - & E_wrt, fft_wrt, pres_wrt, alpha_wrt, gamma_wrt, heat_ratio_wrt, pi_inf_wrt, pres_inf_wrt, cons_vars_wrt, & - & prim_vars_wrt, c_wrt, omega_wrt, qm_wrt, liutex_wrt, schlieren_wrt, schlieren_alpha, fd_order, mixture_err, & - & alt_soundspeed, flux_lim, flux_wrt, cyl_coord, parallel_io, rhoref, pref, bubbles_euler, qbmm, sigR, R0ref, nb, & - & polytropic, thermal, Ca, Web, Re_inv, polydisperse, poly_sigma, file_per_process, relax, relax_model, cf_wrt, & - & sigma, adv_n, ib, num_ibs, cfl_adap_dt, cfl_const_dt, t_save, t_stop, n_start, cfl_target, surface_tension, & - & bubbles_lagrange, sim_data, hyperelasticity, Bx0, relativity, cont_damage, hyper_cleaning, num_bc_patches, igr, & - & igr_order, down_sample, recon_type, muscl_order, lag_header, lag_txt_wrt, lag_db_wrt, lag_id_wrt, lag_pos_wrt, & - & lag_pos_prev_wrt, lag_vel_wrt, lag_rad_wrt, lag_rvel_wrt, lag_r0_wrt, lag_rmax_wrt, lag_rmin_wrt, lag_dphidt_wrt, & - & lag_pres_wrt, lag_mv_wrt, lag_mg_wrt, lag_betaT_wrt, lag_betaC_wrt, alpha_rho_e_wrt, ib_state_wrt + #:include 'generated_namelist.fpp' file_loc = 'post_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/pre_process/m_global_parameters.fpp b/src/pre_process/m_global_parameters.fpp index ae850a9134..8f45ee9733 100644 --- a/src/pre_process/m_global_parameters.fpp +++ b/src/pre_process/m_global_parameters.fpp @@ -17,21 +17,16 @@ module m_global_parameters implicit none + #:include 'generated_decls.fpp' + ! Logistics - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location - logical :: old_grid !< Use existing grid data - logical :: old_ic, non_axis_sym !< Use existing IC data - integer :: t_step_old, t_step_start !< Existing IC/grid folder - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - integer :: n_start, n_start_old + integer :: num_procs !< Number of processors + logical :: non_axis_sym !< Use existing IC data + logical :: cfl_dt ! Computational Domain Parameters integer :: proc_rank !< Rank of the local processor Number of cells in the x-, y- and z-coordinate directions - integer :: m - integer :: n - integer :: p !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- type(cell_num_bounds) :: cells_bounds @@ -39,7 +34,6 @@ module m_global_parameters integer :: m_glb, n_glb, p_glb !< Global number of cells in each direction integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) - logical :: cyl_coord integer :: grid_geometry !< Cylindrical coordinates (either axisymmetric or full 3D) !> Locations of cell-centers (cc) in x-, y- and z-directions, respectively real(wp), allocatable, dimension(:) :: x_cc, y_cc, z_cc @@ -47,39 +41,14 @@ module m_global_parameters real(wp), allocatable, dimension(:) :: x_cb, y_cb, z_cb real(wp) :: dx, dy, dz !< Minimum cell-widths in the x-, y- and z-coordinate directions type(bounds_info) :: x_domain, y_domain, z_domain !< Locations of the domain bounds in the x-, y- and z-coordinate directions - logical :: stretch_x, stretch_y, stretch_z !< Grid stretching flags for the x-, y- and z-coordinate directions - ! Grid stretching: a_x/a_y/a_z = rate, x_a/y_a/z_a = location - real(wp) :: a_x, a_y, a_z - integer :: loops_x, loops_y, loops_z - real(wp) :: x_a, y_a, z_a - real(wp) :: x_b, y_b, z_b ! Simulation Algorithm Parameters - integer :: model_eqns !< Multicomponent flow model - logical :: relax !< activate phase change - integer :: relax_model !< Relax Model - real(wp) :: palpha_eps !< trigger parameter for the p relaxation procedure, phase change model - real(wp) :: ptgalpha_eps !< trigger parameter for the pTg relaxation procedure, phase change model - integer :: num_fluids !< Number of different fluids present in the flow - logical :: mpp_lim !< Alpha limiter integer :: sys_size !< Number of unknowns in the system of equations - integer :: recon_type !< Reconstruction Type integer :: weno_polyn !< Degree of the WENO polynomials (polyn) integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn) - integer :: weno_order !< Order of accuracy for the WENO reconstruction - integer :: muscl_order !< Order of accuracy for the MUSCL reconstruction - logical :: hypoelasticity !< activate hypoelasticity - logical :: hyperelasticity !< activate hyperelasticity logical :: elasticity !< elasticity modeling, true for hyper or hypo - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity for RMHD integer :: b_size !< Number of components in the b tensor integer :: tensor_size !< Number of components in the nonsymmetric tensor - logical :: pre_stress !< activate pre_stressed domain - logical :: cont_damage !< continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD - logical :: igr !< Use information geometric regularization - integer :: igr_order !< IGR reconstruction order logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling ! Annotations of the structure, i.e. the organization, of the state vectors type(eqn_idx_info) :: eqn_idx !< All conserved-variable equation index ranges and scalars. @@ -94,32 +63,11 @@ module m_global_parameters integer, dimension(3) :: shear_indices !< Indices of the stress components that represent shear stress integer :: shear_BC_flip_num !< Number of shear stress components to reflect for boundary conditions integer, dimension(3, 2) :: shear_BC_flip_indices !< Shear stress BC reflection indices (1:3, 1:shear_BC_flip_num) - logical :: parallel_io !< Format of the data files - logical :: file_per_process !< type of data output - integer :: precision !< Precision of output files - logical :: down_sample !< Down-sample the output data - logical :: mixlayer_vel_profile !< Set hyperbolic tangent streamwise velocity profile - real(wp) :: mixlayer_vel_coef !< Coefficient for the hyperbolic tangent streamwise velocity profile - logical :: mixlayer_perturb !< Superimpose instability waves to surrounding fluid flow - integer :: mixlayer_perturb_nk !< Number of Fourier modes for perturbation with mixlayer_perturb flag - real(wp) :: mixlayer_perturb_k0 !< Peak wavenumber for mixlayer perturbation (default: most unstable mode) - logical :: simplex_perturb type(simplex_noise_params) :: simplex_params - real(wp) :: pi_fac !< Factor for artificial pi_inf - logical :: viscous - logical :: bubbles_lagrange - - ! Perturb density of surrounding air so as to break symmetry of grid - logical :: perturb_flow - integer :: perturb_flow_fluid !< Fluid to be perturbed with perturb_flow flag - real(wp) :: perturb_flow_mag !< Magnitude of perturbation with perturb_flow flag - logical :: perturb_sph - integer :: perturb_sph_fluid !< Fluid to be perturbed with perturb_sph flag - real(wp), dimension(num_fluids_max) :: fluid_rho - logical :: elliptic_smoothing - integer :: elliptic_smoothing_iters - integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM - integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid + + ! Perturb density of surrounding air so as to break symmetry of grid fluid_rho: auto-generated in generated_decls.fpp + integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM + integer, allocatable, dimension(:) :: start_idx !< Starting cell-center index of local processor in global grid #ifdef MFC_MPI type(mpi_io_var), public :: MPI_IO_DATA character(LEN=name_len) :: mpiiofs @@ -127,34 +75,24 @@ module m_global_parameters #endif ! Initial Condition Parameters - integer :: num_patches !< Number of patches composing initial condition - type(ic_patch_parameters), dimension(num_patches_max) :: patch_icpp !< IC patch parameters (max: num_patches_max) - integer :: num_bc_patches !< Number of boundary condition patches - logical :: bc_io !< whether or not to save BC data - type(bc_patch_parameters), dimension(num_bc_patches_max) :: patch_bc !< Boundary condition patch parameters + type(ic_patch_parameters), dimension(num_patches_max) :: patch_icpp !< IC patch parameters (max: num_patches_max) + type(bc_patch_parameters), dimension(num_bc_patches_max) :: patch_bc !< Boundary condition patch parameters + logical :: bc_io !< whether or not to save BC data ! Fluids Physical Parameters type(physical_parameters), dimension(num_fluids_max) :: fluid_pp !< Stiffened gas EOS parameters and Reynolds numbers per fluid ! Subgrid Bubble Parameters type(subgrid_bubble_physical_parameters) :: bub_pp - real(wp) :: rhoref, pref !< Reference parameters for Tait EOS type(chemistry_parameters) :: chem_params !> @name Bubble modeling !> @{ - integer :: nb - real(wp) :: Ca, Web, Re_inv, Eu + real(wp) :: Eu real(wp), dimension(:), allocatable :: weight, R0 - logical :: bubbles_euler - logical :: qbmm !< Quadrature moment method integer :: nmom !< Number of carried moments - real(wp) :: sigR, sigV, rhoRV !< standard deviations in R/V - logical :: adv_n !< Solve the number density equation and compute alpha from number density !> @} !> @name Immersed Boundaries !> @{ - logical :: ib !< Turn immersed boundaries on - integer :: num_ibs !< Number of immersed boundaries integer :: Np type(ib_patch_parameters), dimension(num_ib_patches_max) :: patch_ib !< Immersed boundary patch parameters type(vec3_dt), allocatable, dimension(:) :: airfoil_grid_u, airfoil_grid_l @@ -162,30 +100,17 @@ module m_global_parameters !> @name Non-polytropic bubble gas compression !> @{ - logical :: polytropic - logical :: polydisperse - real(wp) :: poly_sigma - integer :: dist_type !< 1 = binormal, 2 = lognormal-normal - integer :: thermal !< 1 = adiabatic, 2 = isotherm, 3 = transfer real(wp) :: phi_vg, phi_gv, Pe_c, Tw, k_vl, k_gl real(wp) :: gam_m real(wp), dimension(:), allocatable :: pb0, mass_g0, mass_v0, Pe_T, k_v, k_g real(wp), dimension(:), allocatable :: Re_trans_T, Re_trans_c, Im_trans_T, Im_trans_c, omegaN - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g - !> @} - - !> @name Surface Tension Modeling - !> @{ - real(wp) :: sigma - logical :: surface_tension + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g !> @} integer, allocatable, dimension(:,:,:) :: logic_grid type(pres_field) :: pb type(pres_field) :: mv - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) integer :: buff_size !< Number of ghost cells for boundary condition storage - logical :: fft_wrt contains diff --git a/src/pre_process/m_start_up.fpp b/src/pre_process/m_start_up.fpp index 25597baad7..7fabdc796a 100644 --- a/src/pre_process/m_start_up.fpp +++ b/src/pre_process/m_start_up.fpp @@ -74,17 +74,7 @@ contains integer :: iostatus character(len=1000) :: line - namelist /user_inputs/ case_dir, old_grid, old_ic, t_step_old, t_step_start, m, n, p, x_domain, y_domain, z_domain, & - & stretch_x, stretch_y, stretch_z, a_x, a_y, a_z, x_a, y_a, z_a, x_b, y_b, z_b, model_eqns, num_fluids, mpp_lim, & - & weno_order, bc_x, bc_y, bc_z, num_patches, hypoelasticity, mhd, patch_icpp, fluid_pp, bub_pp, precision, & - & parallel_io, mixlayer_vel_profile, mixlayer_vel_coef, mixlayer_perturb, mixlayer_perturb_nk, mixlayer_perturb_k0, & - & pi_fac, perturb_flow, perturb_flow_fluid, perturb_flow_mag, perturb_sph, perturb_sph_fluid, fluid_rho, cyl_coord, & - & loops_x, loops_y, loops_z, rhoref, pref, bubbles_euler, R0ref, nb, polytropic, thermal, Ca, Web, Re_inv, & - & polydisperse, poly_sigma, qbmm, sigR, sigV, dist_type, rhoRV, file_per_process, relax, relax_model, palpha_eps, & - & ptgalpha_eps, ib, num_ibs, patch_ib, sigma, adv_n, cfl_adap_dt, cfl_const_dt, n_start, n_start_old, & - & surface_tension, hyperelasticity, pre_stress, elliptic_smoothing, elliptic_smoothing_iters, viscous, & - & bubbles_lagrange, num_bc_patches, patch_bc, Bx0, relativity, cont_damage, igr, igr_order, down_sample, recon_type, & - & muscl_order, hyper_cleaning, simplex_perturb, simplex_params, fft_wrt + #:include 'generated_namelist.fpp' file_loc = 'pre_process.inp' inquire (FILE=trim(file_loc), EXIST=file_check) diff --git a/src/simulation/m_global_parameters.fpp b/src/simulation/m_global_parameters.fpp index b91dd99e47..e807dece0a 100644 --- a/src/simulation/m_global_parameters.fpp +++ b/src/simulation/m_global_parameters.fpp @@ -18,20 +18,15 @@ module m_global_parameters implicit none + #:include 'generated_decls.fpp' + real(wp) :: wall_time = 0 real(wp) :: wall_time_avg = 0 ! Logistics - integer :: num_procs !< Number of processors - character(LEN=path_len) :: case_dir !< Case folder location - logical :: run_time_info !< Run-time output flag - integer :: t_step_old !< Existing IC/grid folder + integer :: num_procs !< Number of processors ! Computational Domain Parameters integer :: proc_rank !< Rank of the local processor - !> @name Number of cells in the x-, y- and z-directions, respectively - !> @{ - integer :: m, n, p - !> @} !> @name Max and min number of cells in a direction of each combination of x-,y-, and z- type(cell_num_bounds) :: cells_bounds @@ -43,7 +38,6 @@ module m_global_parameters !> @name Cylindrical coordinates (either axisymmetric or full 3D) !> @{ - logical :: cyl_coord integer :: grid_geometry !> @} $:GPU_DECLARE(create='[cyl_coord, grid_geometry]') @@ -63,26 +57,12 @@ module m_global_parameters real(wp), target, allocatable, dimension(:) :: dx, dy, dz !> @} - real(wp) :: dt !< Size of the time-step $:GPU_DECLARE(create='[x_cb, y_cb, z_cb, x_cc, y_cc, z_cc, dx, dy, dz, dt, m, n, p]') - !> @name Starting time-step iteration, stopping time-step iteration and the number of time-step iterations between successive - !! solution backups, respectively - !> @{ - integer :: t_step_start, t_step_stop, t_step_save - !> @} - - !> @name Starting time, stopping time, and time between backups, simulation time, and prescribed cfl respectively - !> @{ - real(wp) :: t_stop, t_save, cfl_target - integer :: n_start - !> @} $:GPU_DECLARE(create='[cfl_target]') - logical :: cfl_adap_dt, cfl_const_dt, cfl_dt - integer :: t_step_print !< Number of time-steps between printouts + logical :: cfl_dt ! Simulation Algorithm Parameters - integer :: model_eqns !< Multicomponent flow model #:if MFC_CASE_OPTIMIZATION integer, parameter :: num_dims = ${num_dims}$ !< Number of spatial dimensions integer, parameter :: num_vels = ${num_vels}$ !< Number of velocity components (different from num_dims for mhd) @@ -90,10 +70,6 @@ module m_global_parameters integer :: num_dims !< Number of spatial dimensions integer :: num_vels !< Number of velocity components (different from num_dims for mhd) #:endif - logical :: mpp_lim !< Mixture physical parameters (MPP) limits - integer :: time_stepper !< Time-stepper algorithm - logical :: prim_vars_wrt - #:if MFC_CASE_OPTIMIZATION integer, parameter :: recon_type = ${recon_type}$ !< Reconstruction type integer, parameter :: weno_polyn = ${weno_polyn}$ !< Degree of the WENO polynomials (polyn) @@ -117,74 +93,35 @@ module m_global_parameters logical, parameter :: igr_pres_lim = (${igr_pres_lim}$ /= 0) !< Limit to positive pressures for IGR logical, parameter :: viscous = (${viscous}$ /= 0) !< Viscous effects #:else - integer :: recon_type !< Reconstruction Type - integer :: weno_polyn !< Degree of the WENO polynomials (polyn) - integer :: muscl_polyn !< Degree of the MUSCL polynomials (polyn)i - integer :: weno_order !< Order of the WENO reconstruction - integer :: muscl_order !< Order of the MUSCL reconstruction - integer :: weno_num_stencils !< Number of stencils for WENO reconstruction (only different from weno_polyn for TENO(>5)) - integer :: muscl_lim !< MUSCL Limiter - integer :: num_fluids !< number of fluids in the simulation - logical :: wenojs !< WENO-JS (default) - logical :: mapped_weno !< WENO-M (WENO with mapping of nonlinear weights) - logical :: wenoz !< WENO-Z - logical :: teno !< TENO (Targeted ENO) - real(wp) :: wenoz_q !< Power constant for WENO-Z - logical :: mhd !< Magnetohydrodynamics - logical :: relativity !< Relativity (only for MHD) - integer :: igr_iter_solver !< IGR elliptic solver - integer :: igr_order !< Reconstruction order for IGR - logical :: igr !< Use information geometric regularization - logical :: igr_pres_lim !< Limit to positive pressures for IGR - logical :: viscous !< Viscous effects + integer :: recon_type + integer :: weno_polyn + integer :: muscl_polyn + integer :: weno_order + integer :: muscl_order + integer :: weno_num_stencils + integer :: muscl_lim + integer :: num_fluids + logical :: wenojs + logical :: mapped_weno + logical :: wenoz + logical :: teno + real(wp) :: wenoz_q + logical :: mhd + logical :: relativity + integer :: igr_iter_solver + integer :: igr_order + logical :: igr + logical :: igr_pres_lim + logical :: viscous #:endif - !> @name Variables for our of core IGR computation on NVIDIA - !> @{ - logical :: nv_uvm_out_of_core !< Enable out-of-core storage of q_cons_ts(2) in timestepping (default FALSE) - integer :: nv_uvm_igr_temps_on_gpu !< 0 => jac, jac_rhs, and jac_old on CPU - ! 1 => jac on GPU, jac_rhs and jac_old on CPU 2 => jac and jac_rhs on GPU, jac_old on CPU 3 => jac, jac_rhs, and jac_old on GPU - ! (default) - logical :: nv_uvm_pref_gpu !< Enable explicit gpu memory hints (default FALSE) - !> @} - - real(wp) :: muscl_eps !< MUSCL limiter slope-product threshold - real(wp) :: weno_eps !< Binding for the WENO nonlinear weights - real(wp) :: teno_CT !< Smoothness threshold for TENO - logical :: mp_weno !< Monotonicity preserving (MP) WENO - logical :: weno_avg !< Average left/right cell-boundary states - logical :: weno_Re_flux !< WENO reconstruct velocity gradients for viscous stress tensor - integer :: riemann_solver !< Riemann solver algorithm - integer :: low_Mach !< Low Mach number fix to HLLC Riemann solver - integer :: wave_speeds !< Wave speeds estimation method - integer :: avg_state !< Average state evaluation method - logical :: alt_soundspeed !< Alternate mixture sound speed - logical :: null_weights !< Null undesired WENO weights - logical :: mixture_err !< Mixture properties correction - logical :: hypoelasticity !< hypoelasticity modeling - logical :: hyperelasticity !< hyperelasticity modeling - integer :: int_comp !< Interface compression: 0=off, 1=THINC, 2=MTHINC - real(wp) :: ic_eps !< THINC Epsilon to compress on surface cells - real(wp) :: ic_beta !< THINC Sharpness Parameter $:GPU_DECLARE(create='[int_comp, ic_eps, ic_beta]') - integer :: hyper_model !< hyperelasticity solver algorithm - logical :: elasticity !< elasticity modeling, true for hyper or hypo - logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling - logical :: shear_stress !< Shear stresses - logical :: bulk_stress !< Bulk stresses - logical :: cont_damage !< Continuum damage modeling - logical :: hyper_cleaning !< Hyperbolic cleaning for MHD for divB=0 - integer :: num_igr_iters !< number of iterations for elliptic solve - integer :: num_igr_warm_start_iters !< number of warm start iterations for elliptic solve - real(wp) :: alf_factor !< alpha factor for IGR - logical :: bodyForces - logical :: bf_x, bf_y, bf_z !< body force toggle in three directions - !> amplitude, frequency, and phase shift sinusoid in each direction - #:for dir in {'x', 'y', 'z'} - #:for param in {'k','w','p','g'} - real(wp) :: ${param}$_${dir}$ - #:endfor - #:endfor + integer :: hyper_model !< hyperelasticity solver algorithm + logical :: elasticity !< elasticity modeling, true for hyper or hypo + logical, parameter :: chemistry = .${chemistry}$. !< Chemistry modeling + logical :: shear_stress !< Shear stresses + logical :: bulk_stress !< Bulk stresses + logical :: bodyForces real(wp), dimension(3) :: accel_bf $:GPU_DECLARE(create='[accel_bf]') ! $:GPU_DECLARE(create='[k_x,w_x,p_x,g_x,k_y,w_y,p_y,g_y,k_z,w_z,p_z,g_z]') @@ -205,13 +142,8 @@ module m_global_parameters $:GPU_DECLARE(create='[hyperelasticity, hyper_model, elasticity, low_Mach]') $:GPU_DECLARE(create='[shear_stress, bulk_stress, cont_damage, hyper_cleaning]') - logical :: relax !< activate phase change - integer :: relax_model !< Relaxation model - real(wp) :: palpha_eps !< trigger parameter for the p relaxation procedure, phase change model - real(wp) :: ptgalpha_eps !< trigger parameter for the pTg relaxation procedure, phase change model $:GPU_DECLARE(create='[relax, relax_model, palpha_eps, ptgalpha_eps]') - integer :: num_bc_patches logical :: bc_io !> @name Boundary conditions (BC) in the x-, y- and z-directions, respectively !> @{ @@ -233,12 +165,6 @@ module m_global_parameters #endif type(bounds_info) :: x_domain, y_domain, z_domain $:GPU_DECLARE(create='[x_domain, y_domain, z_domain]') - real(wp) :: x_a, y_a, z_a - real(wp) :: x_b, y_b, z_b - logical :: parallel_io !< Format of the data files - logical :: file_per_process !< shared file or not when using parallel io - integer :: precision !< Precision of output files - logical :: down_sample !< down sample the output files $:GPU_DECLARE(create='[down_sample]') integer, allocatable, dimension(:) :: proc_coords !< Processor coordinates in MPI_CART_COMM @@ -320,32 +246,18 @@ module m_global_parameters type(physical_parameters), dimension(num_fluids_max) :: fluid_pp !< Stiffened gas EOS parameters and Reynolds numbers per fluid ! Subgrid Bubble Parameters type(subgrid_bubble_physical_parameters) :: bub_pp - integer :: fd_order !< Finite-difference order for CoM and flow probe derivatives integer :: fd_number !< Finite-difference half-stencil size: MAX(1, fd_order/2) $:GPU_DECLARE(create='[fd_order, fd_number]') - logical :: probe_wrt - logical :: integral_wrt - integer :: num_probes - integer :: num_integrals type(vec3_dt), dimension(num_probes_max) :: probe type(integral_parameters), dimension(num_probes_max) :: integral !> @name Reference density and pressure for Tait EOS !> @{ - real(wp) :: rhoref, pref - !> @} $:GPU_DECLARE(create='[rhoref, pref]') !> @name Immersed Boundaries !> @{ - logical :: ib - integer :: num_ibs - integer :: collision_model - real(wp) :: coefficient_of_restitution - real(wp) :: collision_time - real(wp) :: ib_coefficient_of_friction - logical :: ib_state_wrt type(ib_patch_parameters), dimension(num_ib_patches_max) :: patch_ib !< Immersed boundary patch parameters type(vec3_dt), allocatable, dimension(:) :: airfoil_grid_u, airfoil_grid_l integer :: Np @@ -359,43 +271,28 @@ module m_global_parameters #:if MFC_CASE_OPTIMIZATION integer, parameter :: nb = ${nb}$ !< Number of eq. bubble sizes #:else - integer :: nb !< Number of eq. bubble sizes + integer :: nb #:endif - real(wp) :: Eu !< Euler number - real(wp) :: Ca !< Cavitation number - real(wp) :: Web !< Weber number - real(wp) :: Re_inv !< Inverse Reynolds number + real(wp) :: Eu !< Euler number $:GPU_DECLARE(create='[Eu, Ca, Web, Re_inv]') real(wp), dimension(:), allocatable :: weight !< Simpson quadrature weights real(wp), dimension(:), allocatable :: R0 !< Bubble sizes $:GPU_DECLARE(create='[weight, R0]') - logical :: bubbles_euler !< Bubbles euler on/off - logical :: polytropic !< Polytropic switch - logical :: polydisperse !< Polydisperse bubbles $:GPU_DECLARE(create='[bubbles_euler, polytropic, polydisperse]') - logical :: adv_n !< Solve the number density equation and compute alpha from number density - logical :: adap_dt !< Adaptive step size control - real(wp) :: adap_dt_tol !< Tolerance to control adaptive step size - integer :: adap_dt_max_iters !< Maximum number of iterations $:GPU_DECLARE(create='[adv_n, adap_dt, adap_dt_tol, adap_dt_max_iters]') - integer :: bubble_model !< Gilmore or Keller--Miksis bubble model - integer :: thermal !< Thermal behavior. 1 = adiabatic, 2 = isotherm, 3 = transfer $:GPU_DECLARE(create='[bubble_model, thermal]') - real(wp), allocatable, dimension(:,:,:) :: ptil !< Pressure modification - real(wp) :: poly_sigma !< log normal sigma for polydisperse PDF + real(wp), allocatable, dimension(:,:,:) :: ptil !< Pressure modification $:GPU_DECLARE(create='[ptil, poly_sigma]') - logical :: qbmm !< Quadrature moment method integer, parameter :: nmom = 6 !< Number of carried moments per R0 location integer :: nmomsp !< Number of moments required by ensemble-averaging integer :: nmomtot !< Total number of carried moments moments/transport equations - real(wp) :: pi_fac !< Factor for artificial pi_inf $:GPU_DECLARE(create='[qbmm, nmomsp, nmomtot, pi_fac]') #:if not MFC_CASE_OPTIMIZATION @@ -423,22 +320,18 @@ module m_global_parameters real(wp) :: gam, gam_m $:GPU_DECLARE(create='[gam, gam_m]') - real(wp) :: R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g + real(wp) :: p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g $:GPU_DECLARE(create='[R0ref, p0ref, rho0ref, T0ref, ss, pv, vd, mu_l, mu_v, mu_g, gam_v, gam_g, M_v, M_g, cp_v, cp_g, R_v, R_g]') !> @} !> @name Acoustic acoustic_source parameters !> @{ - logical :: acoustic_source !< Acoustic source switch - type(acoustic_parameters), dimension(num_probes_max) :: acoustic !< Acoustic source parameters - integer :: num_source !< Number of acoustic sources + type(acoustic_parameters), dimension(num_probes_max) :: acoustic !< Acoustic source parameters !> @} $:GPU_DECLARE(create='[acoustic_source, acoustic, num_source]') !> @name Surface tension parameters !> @{ - real(wp) :: sigma - logical :: surface_tension $:GPU_DECLARE(create='[sigma, surface_tension]') !> @} @@ -447,7 +340,6 @@ module m_global_parameters real(wp) :: mytime !< Current simulation time real(wp) :: finaltime !< Final simulation time - logical :: rdma_mpi type(pres_field), allocatable, dimension(:) :: pb_ts type(pres_field), allocatable, dimension(:) :: mv_ts @@ -455,27 +347,19 @@ module m_global_parameters !> @name lagrangian subgrid bubble parameters !> @{! - logical :: bubbles_lagrange !< Lagrangian subgrid bubble model switch - type(bubbles_lagrange_parameters) :: lag_params !< Lagrange bubbles' parameters + type(bubbles_lagrange_parameters) :: lag_params !< Lagrange bubbles' parameters $:GPU_DECLARE(create='[bubbles_lagrange, lag_params]') !> @} - real(wp) :: Bx0 !< Constant magnetic field in the x-direction (1D) $:GPU_DECLARE(create='[Bx0]') - logical :: fft_wrt !> @name Continuum damage model parameters !> @{! - real(wp) :: tau_star !< Stress threshold for continuum damage modeling - real(wp) :: cont_damage_s !< Exponent s for continuum damage modeling - real(wp) :: alpha_bar !< Damage rate factor for continuum damage modeling $:GPU_DECLARE(create='[tau_star, cont_damage_s, alpha_bar]') !> @} !> @name MHD Hyperbolic cleaning parameters !> @{! - real(wp) :: hyper_cleaning_speed !< Hyperbolic cleaning wave speed (c_h) - real(wp) :: hyper_cleaning_tau !< Hyperbolic cleaning tau $:GPU_DECLARE(create='[hyper_cleaning_speed, hyper_cleaning_tau]') !> @} diff --git a/src/simulation/m_start_up.fpp b/src/simulation/m_start_up.fpp index e946ebd614..6fdc451ba7 100644 --- a/src/simulation/m_start_up.fpp +++ b/src/simulation/m_start_up.fpp @@ -82,37 +82,7 @@ contains character(len=1000) :: line - namelist /user_inputs/ case_dir, run_time_info, m, n, p, dt, & - t_step_start, t_step_stop, t_step_save, t_step_print, & - model_eqns, mpp_lim, time_stepper, weno_eps, muscl_eps, & - rdma_mpi, teno_CT, mp_weno, weno_avg, & - riemann_solver, low_Mach, wave_speeds, avg_state, & - bc_x, bc_y, bc_z, & - x_a, y_a, z_a, x_b, y_b, z_b, & - x_domain, y_domain, z_domain, & - hypoelasticity, & - ib, num_ibs, patch_ib, & - collision_model, coefficient_of_restitution, collision_time, & - ib_coefficient_of_friction, ib_state_wrt, & - fluid_pp, bub_pp, probe_wrt, prim_vars_wrt, & - fd_order, probe, num_probes, t_step_old, & - alt_soundspeed, mixture_err, weno_Re_flux, & - null_weights, precision, parallel_io, cyl_coord, & - rhoref, pref, bubbles_euler, bubble_model, & - R0ref, chem_params, & - #:if not MFC_CASE_OPTIMIZATION - nb, mapped_weno, wenoz, teno, wenoz_q, weno_order, & - num_fluids, mhd, relativity, igr_order, viscous, & - igr_iter_solver, igr, igr_pres_lim, & - recon_type, muscl_order, muscl_lim, & - #:endif - Ca, Web, Re_inv, acoustic_source, acoustic, num_source, polytropic, thermal, integral, integral_wrt, num_integrals, & - & polydisperse, poly_sigma, qbmm, relax, relax_model, palpha_eps, ptgalpha_eps, file_per_process, sigma, pi_fac, & - & adv_n, adap_dt, adap_dt_tol, adap_dt_max_iters, bf_x, bf_y, bf_z, k_x, k_y, k_z, w_x, w_y, w_z, p_x, p_y, p_z, g_x, & - & g_y, g_z, n_start, t_save, t_stop, cfl_adap_dt, cfl_const_dt, cfl_target, surface_tension, bubbles_lagrange, & - & lag_params, hyperelasticity, R0ref, num_bc_patches, Bx0, cont_damage, tau_star, cont_damage_s, alpha_bar, & - & hyper_cleaning, hyper_cleaning_speed, hyper_cleaning_tau, alf_factor, num_igr_iters, num_igr_warm_start_iters, & - & int_comp, ic_eps, ic_beta, nv_uvm_out_of_core, nv_uvm_igr_temps_on_gpu, nv_uvm_pref_gpu, down_sample, fft_wrt + #:include 'generated_namelist.fpp' inquire (FILE=trim(file_path), EXIST=file_exist) diff --git a/toolchain/bootstrap/lint.sh b/toolchain/bootstrap/lint.sh index c22b772f86..024f9dcd12 100644 --- a/toolchain/bootstrap/lint.sh +++ b/toolchain/bootstrap/lint.sh @@ -32,12 +32,8 @@ ruff check benchmarks/*/case.py 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 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 - python3 -m unittest mfc.run.test_archive -v + python3 -m pytest . -v cd - > /dev/null fi diff --git a/toolchain/bootstrap/precheck.sh b/toolchain/bootstrap/precheck.sh index fc0effae08..16da5b8dbd 100755 --- a/toolchain/bootstrap/precheck.sh +++ b/toolchain/bootstrap/precheck.sh @@ -15,17 +15,6 @@ show_help() { exit 0 } -# Cross-platform hash function (macOS uses md5, Linux uses md5sum) -compute_hash() { - if command -v md5sum > /dev/null 2>&1; then - md5sum | cut -d' ' -f1 - elif command -v md5 > /dev/null 2>&1; then - md5 -q - else - # Fallback: use cksum if neither available - cksum | cut -d' ' -f1 - fi -} JOBS=1 @@ -59,6 +48,8 @@ done # CI runs the full suite via ./mfc.sh lint without this variable. export MFC_SKIP_RENDER_TESTS=1 +NCHECK=7 + log "Running$MAGENTA precheck$COLOR_RESET (same checks as CI lint-gate)..." echo "" @@ -66,20 +57,17 @@ echo "" TMPDIR_PC=$(mktemp -d) trap "rm -rf $TMPDIR_PC" EXIT -# --- Phase 1: Format (modifies files, must run alone) --- -BEFORE_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) -if ! ./mfc.sh format -j "$JOBS" > /dev/null 2>&1; then - FORMAT_OK=1 -else - AFTER_HASH=$(git diff -- '*.f90' '*.fpp' '*.py' 2>/dev/null | compute_hash) - if [ "$BEFORE_HASH" != "$AFTER_HASH" ]; then - FORMAT_OK=2 - else - FORMAT_OK=0 - fi +# --- Phase 1: Format check (non-mutating; mirrors CI's format+diff check) --- +# Use --check mode so staged-but-unformatted code is caught even if the +# working tree was already reformatted by a prior ./mfc.sh format run. +FORMAT_OK=0 +if ! ruff format --check toolchain/ examples/ benchmarks/ > /dev/null 2>&1; then + FORMAT_OK=2 +elif ! ffmt --check -j "$JOBS" src/ > /dev/null 2>&1; then + FORMAT_OK=2 fi -# --- Phase 2: All remaining checks in parallel (read-only) --- +# --- Phase 2: All fast checks in parallel (read-only) --- # Spell check ( @@ -127,26 +115,33 @@ fi ) & PID_PARAM_DOCS=$! -# --- Collect results --- +# Example case validation +( + failed=0 + for case in examples/*/case.py; do + [ -f "$case" ] || continue + if ! ./mfc.sh validate "$case" > /dev/null 2>&1; then + failed=$((failed + 1)) + fi + done + echo "$failed" > "$TMPDIR_PC/examples_exit" +) & +PID_EXAMPLES=$! + +# --- Collect results (fast checks) --- FAILED=0 -log "[$CYAN 1/6$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." -if [ "$FORMAT_OK" = "1" ]; then - error "Formatting check failed to run." - FAILED=1 -elif [ "$FORMAT_OK" = "2" ]; then - error "Code was not formatted. Files have been auto-formatted; review and stage the changes." - echo "" - git diff --stat -- '*.f90' '*.fpp' '*.py' 2>/dev/null || true - echo "" +log "[$CYAN 1/$NCHECK$COLOR_RESET] Checking$MAGENTA formatting$COLOR_RESET..." +if [ "$FORMAT_OK" = "2" ]; then + error "Code is not formatted. Run$MAGENTA ./mfc.sh format$COLOR_RESET and re-stage the changes." FAILED=1 else ok "Formatting check passed." fi wait $PID_SPELL -log "[$CYAN 2/6$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." +log "[$CYAN 2/$NCHECK$COLOR_RESET] Running$MAGENTA spell check$COLOR_RESET..." SPELL_RC=$(cat "$TMPDIR_PC/spell_exit" 2>/dev/null || echo "1") if [ "$SPELL_RC" = "0" ]; then ok "Spell check passed." @@ -156,7 +151,7 @@ else fi wait $PID_LINT -log "[$CYAN 3/6$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." +log "[$CYAN 3/$NCHECK$COLOR_RESET] Running$MAGENTA toolchain lint$COLOR_RESET..." LINT_RC=$(cat "$TMPDIR_PC/lint_exit" 2>/dev/null || echo "1") if [ "$LINT_RC" = "0" ]; then ok "Toolchain lint passed." @@ -166,7 +161,7 @@ else fi wait $PID_SOURCE -log "[$CYAN 4/6$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." +log "[$CYAN 4/$NCHECK$COLOR_RESET] Running$MAGENTA source lint$COLOR_RESET..." SOURCE_RC=$(cat "$TMPDIR_PC/source_exit" 2>/dev/null || echo "1") if [ "$SOURCE_RC" = "0" ]; then ok "Source lint passed." @@ -175,7 +170,7 @@ else FAILED=1 fi -log "[$CYAN 5/6$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." +log "[$CYAN 5/$NCHECK$COLOR_RESET] Checking$MAGENTA doc references$COLOR_RESET..." if [ $DOC_FAILED -eq 0 ]; then ok "Doc references are valid." else @@ -184,7 +179,7 @@ else fi wait $PID_PARAM_DOCS -log "[$CYAN 6/6$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." +log "[$CYAN 6/$NCHECK$COLOR_RESET] Checking$MAGENTA parameter docs$COLOR_RESET..." PARAM_DOCS_RC=$(cat "$TMPDIR_PC/param_docs_exit" 2>/dev/null || echo "1") if [ "$PARAM_DOCS_RC" = "0" ]; then ok "Parameter documentation check passed." @@ -193,6 +188,16 @@ else FAILED=1 fi +wait $PID_EXAMPLES +log "[$CYAN 7/$NCHECK$COLOR_RESET] Validating$MAGENTA example cases$COLOR_RESET..." +EXAMPLES_FAILED=$(cat "$TMPDIR_PC/examples_exit" 2>/dev/null || echo "1") +if [ "$EXAMPLES_FAILED" = "0" ]; then + ok "All example cases are valid." +else + error "$EXAMPLES_FAILED example case(s) failed validation. Run$MAGENTA ./mfc.sh validate examples/\*/case.py$COLOR_RESET for details." + FAILED=1 +fi + echo "" if [ $FAILED -eq 0 ]; then diff --git a/toolchain/mfc/bench.py b/toolchain/mfc/bench.py index a6a04e6f8e..f697e3d6da 100644 --- a/toolchain/mfc/bench.py +++ b/toolchain/mfc/bench.py @@ -166,8 +166,6 @@ def bench(targets=None): cons.unindent() -# TODO: This function is too long and not nicely written at all. Someone should -# refactor it... def diff(): lhs, rhs = file_load_yaml(ARG("lhs")), file_load_yaml(ARG("rhs")) lhs_path = os.path.relpath(ARG("lhs")) diff --git a/toolchain/mfc/case_validator.py b/toolchain/mfc/case_validator.py index 05a3fee92c..cd2af49a64 100644 --- a/toolchain/mfc/case_validator.py +++ b/toolchain/mfc/case_validator.py @@ -250,6 +250,18 @@ def _validate_logical(self, key: str): 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_order_fits_grid(self, order: int, param_name: str) -> None: + """Prohibit reconstruction order that exceeds grid cell count in any active dimension.""" + m = self.get("m", 0) + n = self.get("n", 0) or 0 + p = self.get("p", 0) or 0 + self.prohibit(m + 1 < order, f"m must be at least {param_name} - 1 (= {order - 1})") + self.prohibit(n > 0 and n + 1 < order, f"For 2D: n must be at least {param_name} - 1 (= {order - 1})") + self.prohibit(p > 0 and p + 1 < order, f"For 3D: p must be at least {param_name} - 1 (= {order - 1})") + + def _get_recon_type(self) -> int: + return self.get("recon_type", 1) + def check_parameter_types(self): """Validate parameter types before other checks. @@ -265,6 +277,8 @@ def check_parameter_types(self): if param in self.params: # Only validate params that are set self._validate_logical(param) + self.prohibit(self.get("recon_type", 1) not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") + # Required domain parameters when m > 0 m = self.get("m") if m is not None and m > 0: @@ -316,72 +330,44 @@ def check_igr(self): 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") 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._check_order_fits_grid(igr_order, "igr_order") def check_weno(self): """Checks constraints regarding WENO order""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # WENO_TYPE = 1 - if recon_type != 1: + if self._get_recon_type() != 1: return for param in ["muscl_order", "muscl_lim"]: self.prohibit(self.is_set(param), f"recon_type = 1 (WENO) is not compatible with {param}") 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._check_order_fits_grid(weno_order, "weno_order") def check_muscl(self): """Check constraints regarding MUSCL order""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # MUSCL_TYPE = 2 - if recon_type != 2: + if self._get_recon_type() != 2: return - weno_log_params = ["mapped_weno", "wenoz", "teno", "mp_weno", "weno_avg", "null_weights", "weno_Re_flux"] - for param in weno_log_params: + for param in ["mapped_weno", "wenoz", "teno", "mp_weno", "weno_avg", "null_weights", "weno_Re_flux"]: self.prohibit(self.get(param) == "T", f"recon_type = 2 (MUSCL) is not compatible with {param} = T") - - weno_numeric_params = ["wenoz_q", "teno_CT", "weno_eps"] - for param in weno_numeric_params: + for param in ["wenoz_q", "teno_CT", "weno_eps"]: self.prohibit(self.is_set(param), f"recon_type = 2 (MUSCL) is not compatible with {param}") weno_order = self.get("weno_order") self.prohibit(weno_order is not None and weno_order != 0, f"recon_type = 2 (MUSCL) requires weno_order unset or 0, but got {weno_order}") 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._check_order_fits_grid(muscl_order, "muscl_order") def check_interface_compression(self): """Check constraints regarding interface compression""" @@ -752,11 +738,7 @@ def check_finite_difference(self): def check_weno_simulation(self): """Checks WENO-specific constraints for simulation""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # WENO_TYPE = 1 - if recon_type != 1: + if self._get_recon_type() != 1: return weno_order = self.get("weno_order") @@ -793,11 +775,7 @@ def check_weno_simulation(self): def check_muscl_simulation(self): """Checks MUSCL-specific constraints for simulation""" - recon_type = self.get("recon_type", 1) - self.prohibit(recon_type not in [1, 2], "recon_type must be 1 (WENO) or 2 (MUSCL)") - - # MUSCL_TYPE = 2 - if recon_type != 2: + if self._get_recon_type() != 2: return muscl_order = self.get("muscl_order") diff --git a/toolchain/mfc/cli/completion_gen.py b/toolchain/mfc/cli/completion_gen.py index de1ac0cd98..4f74e48e3e 100644 --- a/toolchain/mfc/cli/completion_gen.py +++ b/toolchain/mfc/cli/completion_gen.py @@ -5,10 +5,42 @@ in sync with the CLI schema definitions. """ +import dataclasses from typing import List, Set from .schema import CLISchema, Command, CompletionType + +def _mfc_config_bash_flags() -> List[str]: + """Return bash completion flag strings derived from MFCConfig fields.""" + from ..state import MFCConfig + + flags = [] + for f in dataclasses.fields(MFCConfig): + cli = f.name.replace("_", "-") + flags.append(f"--{cli}") + flags.append(f"--no-{cli}") + return flags + + +def _mfc_config_zsh_flags() -> List[str]: + """Return zsh completion spec strings derived from MFCConfig fields.""" + from ..state import MFCConfig, gpuConfigOptions + + specs = [] + modes = " ".join(e.value for e in gpuConfigOptions if e.value != gpuConfigOptions.NONE.value) + for f in dataclasses.fields(MFCConfig): + cli = f.name.replace("_", "-") + label = cli.replace("-", " ").title() + if f.name == "gpu": + specs.append(f"'--{cli}[Enable GPU]:mode:({modes})'") + specs.append(f"'--no-{cli}[Disable GPU]'") + else: + specs.append(f"'--{cli}[Enable {label}]'") + specs.append(f"'--no-{cli}[Disable {label}]'") + return specs + + # Mapping of completion types to bash completion expressions _BASH_COMPLETION_MAP = { CompletionType.FILES_PY: 'COMPREPLY=( $(compgen -f -X "!*.py" -- "${cur}") $(compgen -d -- "${cur}") )', @@ -31,26 +63,7 @@ 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(_mfc_config_bash_flags()) else: for arg in common_set.arguments: if arg.short: @@ -324,26 +337,7 @@ 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(_mfc_config_zsh_flags()) else: for arg in common_set.arguments: desc = arg.help.replace("'", "").replace("[", "").replace("]", "")[:120] diff --git a/toolchain/mfc/lint_docs.py b/toolchain/mfc/lint_docs.py index 15a1a6546b..32f765651a 100644 --- a/toolchain/mfc/lint_docs.py +++ b/toolchain/mfc/lint_docs.py @@ -407,6 +407,9 @@ def check_physics_docs_coverage(repo_root: Path) -> list[str]: # Methods without PHYSICS_DOCS entries. Add a PHYSICS_DOCS entry (with math, # references, and explanation) to case_validator.py to remove from this set. skip = { + # Private helpers — called from check_* methods, not check methods themselves + "_check_order_fits_grid", + "_get_recon_type", # Structural/mechanical checks (no physics meaning) "check_parameter_types", # type validation "check_output_format", # output format selection diff --git a/toolchain/mfc/lint_param_docs.py b/toolchain/mfc/lint_param_docs.py index 657ddae5af..2f62d34ac5 100644 --- a/toolchain/mfc/lint_param_docs.py +++ b/toolchain/mfc/lint_param_docs.py @@ -80,39 +80,6 @@ def _param_appears_in_case_md(param_base: str, tokens: set[str], text: str) -> b return False -def _parse_namelist_params(fpp_path: Path) -> set[str]: - """Parse parameter names from a namelist /user_inputs/ block in an fpp file.""" - text = fpp_path.read_text(encoding="utf-8") - params = set() - - in_namelist = False - accum = "" - for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith("!") or stripped.startswith("#:") or stripped.startswith("#"): - continue - lower = stripped.lower() - if "namelist /user_inputs/" in lower: - idx = lower.index("/user_inputs/") + len("/user_inputs/") - accum += " " + stripped[idx:] - in_namelist = accum.rstrip().endswith("&") - continue - if in_namelist: - if stripped.startswith("&"): - stripped = stripped[1:] - accum += " " + stripped - if not accum.rstrip().endswith("&"): - in_namelist = False - - accum = accum.replace("&", " ") - for raw_token in accum.split(","): - name = raw_token.strip() - if name and re.match(r"^[a-zA-Z_]\w*$", name): - params.add(name) - - return params - - def check_descriptions_in_case_md(repo_root: Path) -> list[str]: """Tier 1, Check 1+2: Params with DESCRIPTIONS entries should appear in case.md.""" REGISTRY, DESCRIPTIONS = _import_registry(repo_root) @@ -175,13 +142,14 @@ def _is_derived_type_parent_or_field(name: str, all_params: dict) -> bool: def check_namelist_registry_sync(repo_root: Path) -> list[str]: """Tier 2: Unknown Fortran namelist params must be in REGISTRY (blocking).""" REGISTRY, _ = _import_registry(repo_root) + from mfc.params.namelist_parser import parse_namelist_from_file errors = [] startup_files = sorted((repo_root / "src").rglob("m_start_up.fpp")) all_params = REGISTRY.all_params for fpp in startup_files: - nl_params = _parse_namelist_params(fpp) + nl_params = parse_namelist_from_file(fpp) rel = fpp.relative_to(repo_root) for p in sorted(nl_params): diff --git a/toolchain/mfc/lint_source.py b/toolchain/mfc/lint_source.py index d26c987465..59645ebd5a 100644 --- a/toolchain/mfc/lint_source.py +++ b/toolchain/mfc/lint_source.py @@ -185,10 +185,7 @@ def check_double_precision(repo_root: Path) -> list[str]: errors: list[str] = [] src_dir = repo_root / SRC_DIR precision_re = re.compile( - r"\b(?:double_precision|double\s+precision|dsqrt|dexp|dlog|dble|dabs|" - r"dprod|dmin|dmax|dfloat|dreal|dcos|dsin|dtan|dsign|dtanh|dsinh|dcosh)\b|" - r"\breal\s*\(\s*[48]\s*\)|" - r"[0-9]d0", + r"\b(?:double_precision|double\s+precision|dsqrt|dexp|dlog|dble|dabs|" r"dprod|dmin|dmax|dfloat|dreal|dcos|dsin|dtan|dsign|dtanh|dsinh|dcosh)\b|" r"\breal\s*\(\s*[48]\s*\)|" r"[0-9]d0", re.IGNORECASE, ) @@ -312,33 +309,6 @@ def check_junk_comments(repo_root: Path) -> list[str]: return errors -def check_pylint_directives(repo_root: Path) -> list[str]: - """Flag ``# pylint:`` directives in Python files. - - MFC uses ruff for linting; leftover pylint directives are dead code. - """ - errors: list[str] = [] - pylint_re = re.compile(r"#\s*pylint\s*:", re.IGNORECASE) - self_path = Path(__file__).resolve() - - for subdir in ["examples", "benchmarks", "toolchain"]: - d = repo_root / subdir - if not d.exists(): - continue - for py in sorted(d.rglob("*.py")): - if py.resolve() == self_path: - continue - lines = py.read_text(encoding="utf-8").splitlines() - rel = py.relative_to(repo_root) - - for i, line in enumerate(lines): - match = pylint_re.search(line) - if match: - errors.append(f" {rel}:{i + 1} pylint directive. Fix: remove (use ruff noqa comments if needed)") - - return errors - - def main(): repo_root = Path(__file__).resolve().parents[2] @@ -351,7 +321,6 @@ def main(): all_errors.extend(check_fypp_list_duplicates(repo_root)) all_errors.extend(check_duplicate_lines(repo_root)) all_errors.extend(check_hardcoded_byte_size(repo_root)) - all_errors.extend(check_pylint_directives(repo_root)) if all_errors: print("Source lint failed:") diff --git a/toolchain/mfc/params/definitions.py b/toolchain/mfc/params/definitions.py index c8db5547d0..ecdb728644 100644 --- a/toolchain/mfc/params/definitions.py +++ b/toolchain/mfc/params/definitions.py @@ -39,332 +39,6 @@ def _fc(name: str, default: int) -> int: NA = 4 # acoustic sources: enumerated individually -# Auto-generated Descriptions -# Descriptions are auto-generated from parameter names using naming conventions. -# Override with explicit desc= parameter when auto-generation is inadequate. - -# Prefix descriptions for indexed parameter families -_PREFIX_DESCS = { - "patch_icpp": "initial condition patch", - "patch_ib": "immersed boundary", - "patch_bc": "boundary condition patch", - "fluid_pp": "fluid", - "acoustic": "acoustic source", - "probe": "probe", - "integral": "integral region", -} - -# Attribute descriptions (suffix after %) -_ATTR_DESCS = { - # Geometry/position - "geometry": "Geometry type", - "x_centroid": "X-coordinate of centroid", - "y_centroid": "Y-coordinate of centroid", - "z_centroid": "Z-coordinate of centroid", - "length_x": "X-dimension length", - "length_y": "Y-dimension length", - "length_z": "Z-dimension length", - "radius": "Radius", - "radii": "Radii array", - "normal": "Normal direction", - "theta": "Theta angle", - "angles": "Orientation angles", - # Physics - "vel": "Velocity", - "pres": "Pressure", - "rho": "Density", - "alpha": "Volume fraction", - "alpha_rho": "Partial density", - "gamma": "Specific heat ratio", - "pi_inf": "Stiffness pressure", - "cv": "Specific heat (const. volume)", - "qv": "Heat of formation", - "qvp": "Heat of formation prime", - "G": "Shear modulus", - "Re": "Reynolds number", - "mul0": "Reference viscosity", - "ss": "Surface tension", - "pv": "Vapor pressure", - # MHD - "Bx": "Magnetic field (x-component)", - "By": "Magnetic field (y-component)", - "Bz": "Magnetic field (z-component)", - # Model/smoothing - "smoothen": "Enable smoothing", - "smooth_patch_id": "Patch ID to smooth against", - "smooth_coeff": "Smoothing coefficient", - "alter_patch": "Alter with another patch", - "model_filepath": "STL model file path", - "model_spc": "Model spacing", - "model_threshold": "Model threshold", - "model_translate": "Model translation", - "model_scale": "Model scale", - "model_rotate": "Model rotation", - # Bubbles - "r0": "Initial bubble radius", - "v0": "Initial bubble velocity", - "p0": "Initial bubble pressure", - "m0": "Initial bubble mass", - # IB specific - "slip": "Enable slip condition", - "moving_ibm": "Enable moving boundary", - "angular_vel": "Angular velocity", - "mass": "Mass", - # BC specific - "vel_in": "Inlet velocity", - "vel_out": "Outlet velocity", - "alpha_rho_in": "Inlet partial density", - "alpha_in": "Inlet volume fraction", - "pres_in": "Inlet pressure", - "pres_out": "Outlet pressure", - "grcbc_in": "Enable GRCBC inlet", - "grcbc_out": "Enable GRCBC outlet", - "grcbc_vel_out": "Enable GRCBC velocity outlet", - "isothermal_in": "Enable isothermal wall at the domain entrance (minimum coordinate)", - "isothermal_out": "Enable isothermal wall at the domain exit (maximum coordinate)", - # Acoustic - "loc": "Location", - "mag": "Magnitude", - "pulse": "Pulse type", - "support": "Support type", - "frequency": "Frequency", - "wavelength": "Wavelength", - "length": "Length", - "height": "Height", - "delay": "Delay time", - "dipole": "Enable dipole", - "dir": "Direction", - # Output - "x": "X-coordinate", - "y": "Y-coordinate", - "z": "Z-coordinate", - "xmin": "X minimum", - "xmax": "X maximum", - "ymin": "Y minimum", - "ymax": "Y maximum", - "zmin": "Z minimum", - "zmax": "Z maximum", - # Chemistry - "Y": "Species mass fraction", - # Shape coefficients - "a": "Shape coefficient", - # Elasticity - "tau_e": "Elastic stress component", - # Misc - "cf_val": "Color function value", - "hcid": "Hard-coded ID", - "epsilon": "Interface thickness", - "beta": "Shape parameter beta", - "non_axis_sym": "Non-axisymmetric parameter", -} - -# Simple parameter descriptions (non-indexed) -_SIMPLE_DESCS = { - # Grid - "m": "Grid cells in x-direction", - "n": "Grid cells in y-direction", - "p": "Grid cells in z-direction", - "cyl_coord": "Enable cylindrical coordinates", - "stretch_x": "Enable grid stretching in x", - "stretch_y": "Enable grid stretching in y", - "stretch_z": "Enable grid stretching in z", - "a_x": "Grid stretching rate in x", - "a_y": "Grid stretching rate in y", - "a_z": "Grid stretching rate in z", - "x_a": "Stretching start (negative x)", - "x_b": "Stretching start (positive x)", - "y_a": "Stretching start (negative y)", - "y_b": "Stretching start (positive y)", - "z_a": "Stretching start (negative z)", - "z_b": "Stretching start (positive z)", - "loops_x": "Stretching iterations in x", - "loops_y": "Stretching iterations in y", - "loops_z": "Stretching iterations in z", - # Time - "dt": "Time step size", - "t_step_start": "Starting time step", - "t_step_stop": "Ending time step", - "t_step_save": "Save interval (steps)", - "t_step_print": "Print interval (steps)", - "t_stop": "Stop time", - "t_save": "Save interval (time)", - "time_stepper": "Time integration scheme", - "cfl_target": "Target CFL number", - "cfl_adap_dt": "Enable adaptive CFL time stepping", - "cfl_const_dt": "Use constant CFL time stepping", - "cfl_dt": "Enable CFL-based time stepping", - "adap_dt": "Enable adaptive time stepping", - "adap_dt_tol": "Adaptive time stepping tolerance", - "adap_dt_max_iters": "Max iterations for adaptive dt", - # Model - "model_eqns": "Model equations", - "num_fluids": "Number of fluids", - "num_patches": "Number of IC patches", - "mpp_lim": "Mixture pressure positivity limiter", - # WENO - "weno_order": "WENO reconstruction order", - "weno_eps": "WENO epsilon parameter", - "mapped_weno": "Enable mapped WENO", - "wenoz": "Enable WENO-Z", - "teno": "Enable TENO", - "mp_weno": "Enable monotonicity-preserving WENO", - # Riemann - "riemann_solver": "Riemann solver", - "wave_speeds": "Wave speed estimate method", - "avg_state": "Average state", - # Physics toggles - "viscous": "Enable viscous effects", - "mhd": "Enable magnetohydrodynamics", - "hyper_cleaning": "Enable hyperbolic divergence cleaning", - "hyper_cleaning_speed": "Divergence cleaning wave speed", - "hyper_cleaning_tau": "Divergence cleaning damping time", - "bubbles_euler": "Enable Euler bubble model", - "bubbles_lagrange": "Enable Lagrangian bubbles", - "polytropic": "Enable polytropic gas", - "polydisperse": "Enable polydisperse bubbles", - "qbmm": "Enable QBMM", - "chemistry": "Enable chemistry", - "surface_tension": "Enable surface tension", - "hypoelasticity": "Enable hypoelastic model", - "hyperelasticity": "Enable hyperelastic model", - "relativity": "Enable special relativity", - "ib": "Enable immersed boundaries", - "collision_model": "Collision model for immersed boundaries (0=none, 1=soft sphere)", - "coefficient_of_restitution": "Coefficient of restitution for IB collisions", - "collision_time": "Characteristic collision time for IB collisions", - "ib_coefficient_of_friction": "Coefficient of friction for IB collisions", - "acoustic_source": "Enable acoustic sources", - # Output - "parallel_io": "Enable parallel I/O", - "probe_wrt": "Write probe data", - "prim_vars_wrt": "Write primitive variables", - "cons_vars_wrt": "Write conservative variables", - "run_time_info": "Print runtime info", - "ib_state_wrt": "Write IB state and load data", - # Misc - "case_dir": "Case directory path", - "cantera_file": "Cantera mechanism file", - "num_ibs": "Number of immersed boundaries", - "num_source": "Number of acoustic sources", - "num_probes": "Number of probes", - "num_integrals": "Number of integral regions", - "nb": "Number of bubble bins", - "R0ref": "Reference bubble radius", - "sigma": "Surface tension coefficient", - "Bx0": "Background magnetic field (x)", - "old_grid": "Load grid from previous simulation", - "old_ic": "Load initial conditions from previous", - "t_step_old": "Time step to restart from", - "fd_order": "Finite difference order", - "recon_type": "Reconstruction type", - "muscl_order": "MUSCL reconstruction order", - "muscl_lim": "MUSCL limiter type", - "muscl_eps": "MUSCL limiter slope-product threshold", - "low_Mach": "Low Mach number correction", - "bubble_model": "Bubble dynamics model", - "Ca": "Cavitation number", - "Web": "Weber number", - "Re_inv": "Inverse Reynolds number", - "format": "Output format", - "precision": "Output precision", - # Body forces - "bf_x": "Enable body force in x", - "bf_y": "Enable body force in y", - "bf_z": "Enable body force in z", - "k_x": "Body force wavenumber in x", - "k_y": "Body force wavenumber in y", - "k_z": "Body force wavenumber in z", - "w_x": "Body force frequency in x", - "w_y": "Body force frequency in y", - "w_z": "Body force frequency in z", - "p_x": "Body force phase in x", - "p_y": "Body force phase in y", - "p_z": "Body force phase in z", - "g_x": "Gravitational acceleration in x", - "g_y": "Gravitational acceleration in y", - "g_z": "Gravitational acceleration in z", - # More output - "E_wrt": "Write energy field", - "c_wrt": "Write sound speed field", - "rho_wrt": "Write density field", - "pres_wrt": "Write pressure field", - "schlieren_wrt": "Write schlieren images", - "cf_wrt": "Write color function", - "omega_wrt": "Write vorticity", - "qm_wrt": "Write Q-criterion", - "liutex_wrt": "Write Liutex vortex field", - "gamma_wrt": "Write gamma field", - "heat_ratio_wrt": "Write heat capacity ratio", - "pi_inf_wrt": "Write pi_inf field", - "pres_inf_wrt": "Write reference pressure", - "fft_wrt": "Write FFT output", - "chem_wrt_T": "Write temperature (chemistry)", - # Misc physics - "alt_soundspeed": "Alternative sound speed formulation", - "mixture_err": "Enable mixture error checking", - "cont_damage": "Enable continuum damage model", -} - - -def _auto_describe(name: str) -> str: - """Auto-generate description from parameter name.""" - # Check simple params first - if name in _SIMPLE_DESCS: - return _SIMPLE_DESCS[name] - - # Handle indexed params: prefix(N)%attr or prefix(N)%attr(M) - match = re.match(r"([a-z_]+)\((\d+)\)%(.+)", name) - if match: - prefix, idx, attr = match.group(1), match.group(2), match.group(3) - prefix_desc = _PREFIX_DESCS.get(prefix, prefix.replace("_", " ")) - - # Check for nested index: attr(M) or attr(M, K) - attr_match = re.match(r"([a-z_]+)\((\d+)(?:,\s*(\d+))?\)", attr) - if attr_match: - attr_base = attr_match.group(1) - idx2 = attr_match.group(2) - attr_desc = _ATTR_DESCS.get(attr_base, attr_base.replace("_", " ")) - return f"{attr_desc} {idx2} for {prefix_desc} {idx}" - - attr_desc = _ATTR_DESCS.get(attr, attr.replace("_", " ")) - return f"{attr_desc} for {prefix_desc} {idx}" - - # Handle bc_x%attr style (no index in prefix) - if "%" in name: - prefix, attr = name.split("%", 1) - # Check for indexed attr - attr_match = re.match(r"([a-z_]+)\((\d+)\)", attr) - if attr_match: - attr_base, idx = attr_match.group(1), attr_match.group(2) - attr_desc = _ATTR_DESCS.get(attr_base, attr_base.replace("_", " ")) - return f"{attr_desc} {idx} for {prefix.replace('_', ' ')}" - - attr_desc = _ATTR_DESCS.get(attr, "") - if attr_desc: - return f"{attr_desc} for {prefix.replace('_', ' ')}" - # Fallback: just clean up the name - return f"{attr.replace('_', ' ').title()} for {prefix.replace('_', ' ')}" - - # Handle suffix-indexed: name(N) or name(N, M) - match = re.match(r"([a-z_]+)\((\d+)(?:,\s*(\d+))?\)", name) - if match: - base, idx = match.group(1), match.group(2) - # Handle _wrt patterns - if base.endswith("_wrt"): - field = base[:-4].replace("_", " ") - return f"Write {field} for component {idx}" - return f"{base.replace('_', ' ').title()} {idx}" - - # Fallback patterns - if name.endswith("_wrt"): - return f"Write {name[:-4].replace('_', ' ')}" - if name.startswith("num_"): - return f"Number of {name[4:].replace('_', ' ')}" - - # Last resort: clean up the name - return name.replace("_", " ").replace("%", " ") - - # Parameters that can be hard-coded for GPU case optimization CASE_OPT_PARAMS = { "mapped_weno", @@ -815,27 +489,31 @@ def get_value_label(param_name: str, value: int) -> str: } -def _r(name, ptype, tags=None, desc=None, hint=None, math=None): +def _r(name, ptype, tags=None, desc=None, hint=None, math=None, str_len=None): """Register a parameter with optional feature tags and description.""" if hint is None: hint = _lookup_hint(name) - description = desc if desc else _auto_describe(name) + if desc is None: + from .descriptions import get_description + + desc = get_description(name) constraint = CONSTRAINTS.get(name) if constraint and "value_labels" in constraint: labels = constraint["value_labels"] suffix = ", ".join(f"{v}={labels[v]}" for v in sorted(labels)) - description = f"{description} ({suffix})" + desc = f"{desc} ({suffix})".strip() REGISTRY.register( ParamDef( name=name, param_type=ptype, - description=description, + description=desc, case_optimization=(name in CASE_OPT_PARAMS), constraints=constraint, dependencies=DEPENDENCIES.get(name), tags=tags if tags else set(), hint=hint, math_symbol=math or "", + str_len=str_len if str_len is not None else "name_len", ) ) @@ -897,7 +575,7 @@ def _load(): # Bubbles _r("R0ref", REAL, {"bubbles"}, math=r"\f$R_0\f$") - _r("nb", REAL, {"bubbles"}, math=r"\f$N_b\f$") + _r("nb", INT, {"bubbles"}, math=r"\f$N_b\f$") _r("Web", REAL, {"bubbles"}, math=r"\f$\mathrm{We}\f$") _r("Ca", REAL, {"bubbles"}, math=r"\f$\mathrm{Ca}\f$") _r("Re_inv", REAL, {"bubbles", "viscosity"}, math=r"\f$\mathrm{Re}^{-1}\f$") @@ -941,25 +619,19 @@ def _load(): # Output _r("precision", INT, {"output"}) _r("format", INT, {"output"}) - _r("schlieren_alpha", REAL, {"output"}) for n in ["parallel_io", "file_per_process", "run_time_info", "prim_vars_wrt", "cons_vars_wrt", "fft_wrt", "ib_state_wrt"]: _r(n, LOG, {"output"}) for n in [ "schlieren_wrt", - "alpha_rho_wrt", + "alpha_wrt", "rho_wrt", - "mom_wrt", - "vel_wrt", - "flux_wrt", "E_wrt", "pres_wrt", - "alpha_wrt", "gamma_wrt", "heat_ratio_wrt", "pi_inf_wrt", "pres_inf_wrt", "c_wrt", - "omega_wrt", "qm_wrt", "liutex_wrt", "cf_wrt", @@ -1035,7 +707,6 @@ def _load(): "mixlayer_vel_coef", "mixlayer_perturb_k0", "perturb_flow_mag", - "fluid_rho", "sigR", "sigV", "rhoRV", @@ -1072,7 +743,7 @@ def _load(): ]: _r(n, LOG) _r("int_comp", INT) - _r("case_dir", STR) + _r("case_dir", STR, str_len="path_len") # Body force for d in ["x", "y", "z"]: @@ -1346,3 +1017,289 @@ def _init_registry(): _init_registry() + +# Namelist target mapping for Fortran codegen. +# Maps each Fortran namelist root variable to the set of MFC executables whose +# namelist it appears in. Used by fortran_gen.py to generate per-target .fpp files. +# +# When adding a new parameter: +# 1. Add to definitions.py (type, constraints, etc.) — you are here +# 2. Add the namelist root variable to NAMELIST_VARS with its target set +# 3. Re-run cmake to regenerate the .fpp files (cmake reconfigure) + +NAMELIST_VARS: dict[str, set[str]] = {} + +# Maps indexed-family base names to their Fortran dimension expression. +# The generator emits `{type}, dimension({dim}) :: {name}` for each entry. +# Add here whenever a new array param needs no manual Fortran declaration. +FORTRAN_ARRAY_DIMS: dict[str, str] = { + "fluid_rho": "num_fluids_max", + "alpha_rho_wrt": "num_fluids_max", + "alpha_rho_e_wrt": "num_fluids_max", + "alpha_wrt": "num_fluids_max", + "schlieren_alpha": "num_fluids_max", + "chem_wrt_Y": "num_species", + "flux_wrt": "3", + "mom_wrt": "3", + "omega_wrt": "3", + "vel_wrt": "3", +} + + +def _nv(targets: set, *names: str) -> None: + for n in names: + NAMELIST_VARS[n] = set(targets) + + +_ALL, _PRE_SIM, _SIM_POST = {"pre", "sim", "post"}, {"pre", "sim"}, {"sim", "post"} +_PRE_POST = {"pre", "post"} +_SIM = {"sim"} +_PRE = {"pre"} +_POST = {"post"} + +_nv( + _ALL, + "m", + "n", + "p", + "cyl_coord", + "bc_x", + "bc_y", + "bc_z", + "num_bc_patches", + "case_dir", + "t_step_start", + "cfl_adap_dt", + "cfl_const_dt", + "n_start", + "model_eqns", + "mpp_lim", + "relax", + "relax_model", + "fluid_pp", + "bub_pp", + "rhoref", + "pref", + "bubbles_euler", + "bubbles_lagrange", + "R0ref", + "polytropic", + "thermal", + "Ca", + "Web", + "Re_inv", + "polydisperse", + "poly_sigma", + "qbmm", + "sigma", + "adv_n", + "hypoelasticity", + "hyperelasticity", + "surface_tension", + "relativity", + "ib", + "num_ibs", + "cont_damage", + "hyper_cleaning", + "Bx0", + "precision", + "parallel_io", + "file_per_process", + "fft_wrt", + "down_sample", +) +_nv( + _SIM_POST, + "t_step_stop", + "t_step_save", + "t_stop", + "t_save", + "cfl_target", + "avg_state", + "prim_vars_wrt", + "alt_soundspeed", + "mixture_err", + "fd_order", + "ib_state_wrt", +) +_nv( + _PRE_SIM, + "x_domain", + "y_domain", + "z_domain", + "x_a", + "y_a", + "z_a", + "x_b", + "y_b", + "z_b", + "palpha_eps", + "ptgalpha_eps", + "t_step_old", + "patch_ib", + "pi_fac", +) +_nv(_PRE_POST, "num_fluids", "weno_order", "recon_type", "muscl_order", "mhd", "nb", "sigR", "igr", "igr_order") +_nv( + _SIM, + "dt", + "t_step_print", + "time_stepper", + "adap_dt", + "adap_dt_tol", + "adap_dt_max_iters", + "weno_eps", + "teno_CT", + "wenoz_q", + "mp_weno", + "weno_avg", + "weno_Re_flux", + "null_weights", + "muscl_eps", + "int_comp", + "ic_eps", + "ic_beta", + "riemann_solver", + "wave_speeds", + "low_Mach", + "hyper_cleaning_speed", + "hyper_cleaning_tau", + "run_time_info", + "bubble_model", + "lag_params", + "probe_wrt", + "num_probes", + "probe", + "integral_wrt", + "num_integrals", + "integral", + "acoustic_source", + "num_source", + "acoustic", + "chem_params", + "bf_x", + "bf_y", + "bf_z", + "k_x", + "k_y", + "k_z", + "w_x", + "w_y", + "w_z", + "p_x", + "p_y", + "p_z", + "g_x", + "g_y", + "g_z", + "collision_model", + "coefficient_of_restitution", + "collision_time", + "ib_coefficient_of_friction", + "tau_star", + "cont_damage_s", + "alpha_bar", + "rdma_mpi", + "alf_factor", + "num_igr_iters", + "num_igr_warm_start_iters", + "igr_iter_solver", + "igr_pres_lim", + "nv_uvm_out_of_core", + "nv_uvm_igr_temps_on_gpu", + "nv_uvm_pref_gpu", +) +_nv( + _PRE, + "stretch_x", + "stretch_y", + "stretch_z", + "a_x", + "a_y", + "a_z", + "loops_x", + "loops_y", + "loops_z", + "n_start_old", + "num_patches", + "patch_icpp", + "patch_bc", + "sigV", + "dist_type", + "rhoRV", + "viscous", + "old_grid", + "old_ic", + "perturb_flow", + "perturb_flow_fluid", + "perturb_flow_mag", + "perturb_sph", + "perturb_sph_fluid", + "fluid_rho", + "mixlayer_vel_profile", + "mixlayer_vel_coef", + "mixlayer_perturb", + "mixlayer_perturb_nk", + "mixlayer_perturb_k0", + "pre_stress", + "elliptic_smoothing", + "elliptic_smoothing_iters", + "simplex_perturb", + "simplex_params", +) +_nv( + _POST, + "x_output", + "y_output", + "z_output", + "format", + "output_partial_domain", + "sim_data", + "G", + "flux_lim", + "cons_vars_wrt", + "rho_wrt", + "E_wrt", + "pres_wrt", + "c_wrt", + "gamma_wrt", + "heat_ratio_wrt", + "pi_inf_wrt", + "pres_inf_wrt", + "omega_wrt", + "qm_wrt", + "liutex_wrt", + "schlieren_wrt", + "schlieren_alpha", + "alpha_rho_wrt", + "mom_wrt", + "vel_wrt", + "flux_wrt", + "alpha_wrt", + "cf_wrt", + "chem_wrt_T", + "chem_wrt_Y", + "alpha_rho_e_wrt", + "lag_header", + "lag_txt_wrt", + "lag_db_wrt", + "lag_id_wrt", + "lag_pos_wrt", + "lag_pos_prev_wrt", + "lag_vel_wrt", + "lag_rad_wrt", + "lag_rvel_wrt", + "lag_r0_wrt", + "lag_rmax_wrt", + "lag_rmin_wrt", + "lag_dphidt_wrt", + "lag_pres_wrt", + "lag_mv_wrt", + "lag_mg_wrt", + "lag_betaT_wrt", + "lag_betaC_wrt", +) + +# Case-optimization params appear in the sim namelist under #:if not MFC_CASE_OPTIMIZATION. +for _v in CASE_OPT_PARAMS: + NAMELIST_VARS.setdefault(_v, set()).add("sim") diff --git a/toolchain/mfc/params/descriptions.py b/toolchain/mfc/params/descriptions.py index 8ceab7f96c..a0d7fd3bcb 100644 --- a/toolchain/mfc/params/descriptions.py +++ b/toolchain/mfc/params/descriptions.py @@ -514,28 +514,18 @@ def get_description(param_name: str) -> str: - """Get description for a parameter from hand-curated or auto-generated sources. + """Get the best available description for a parameter. - Priority: hand-curated DESCRIPTIONS > PATTERNS > auto-generated param.description. + Priority: hand-curated DESCRIPTIONS > PATTERNS > naming-convention inference. + (param.description in REGISTRY is now populated from this function at registration + time, so it can be read directly without calling this function again.) """ - # 1. Hand-curated descriptions (highest quality) if param_name in DESCRIPTIONS: return DESCRIPTIONS[param_name] - - # 2. Pattern matching for indexed params (hand-curated templates) for pattern, template in PATTERNS: match = re.fullmatch(pattern, param_name) if match: return template.format(*match.groups()) - - # 3. Auto-generated description from registry (set by _auto_describe at registration) - from . import REGISTRY - - param = REGISTRY.all_params.get(param_name) - if param and param.description: - return param.description - - # 4. Last resort: naming convention inference return _infer_from_naming(param_name) diff --git a/toolchain/mfc/params/generators/cmake_gen.py b/toolchain/mfc/params/generators/cmake_gen.py new file mode 100644 index 0000000000..c71a8e5c23 --- /dev/null +++ b/toolchain/mfc/params/generators/cmake_gen.py @@ -0,0 +1,20 @@ +"""Generate Fortran parameter .fpp files into the CMake build directory. + +Called by CMakeLists.txt at configure time: + python3 cmake_gen.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from mfc.params.generators.fortran_gen import get_generated_files # noqa: E402 + +if len(sys.argv) != 2: + sys.exit(f"Usage: {sys.argv[0]} ") + +build_dir = Path(sys.argv[1]) +for path, content in get_generated_files(build_dir): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) diff --git a/toolchain/mfc/params/generators/docs_gen.py b/toolchain/mfc/params/generators/docs_gen.py index 2e9f877b3b..6a6bfa4170 100644 --- a/toolchain/mfc/params/generators/docs_gen.py +++ b/toolchain/mfc/params/generators/docs_gen.py @@ -13,7 +13,7 @@ from .. import definitions # noqa: F401 from ..ast_analyzer import analyze_case_validator, classify_message -from ..descriptions import get_description, get_math_symbol +from ..descriptions import get_math_symbol from ..registry import REGISTRY from ..schema import ParamType @@ -342,6 +342,14 @@ def _format_validator_rules(param_name: str, by_trigger: Dict[str, list], by_par return "; ".join(parts) +def _format_constraints_cell(name: str, param, by_trigger, by_param) -> str: + """Format the Constraints table cell for a single parameter.""" + extra = "; ".join(filter(None, [_format_constraints(param), _format_validator_rules(name, by_trigger, by_param)])) + if not extra: + extra = _format_tag_annotation(name, param) + return _escape_pct_outside_backticks(extra) + + def generate_parameter_docs() -> str: """Generate markdown documentation for all parameters.""" # AST-extract rules from case_validator.py @@ -455,8 +463,8 @@ def generate_parameter_docs() -> str: pattern_has_symbols = False for _pattern, examples in patterns.items(): for ex in examples: - p = REGISTRY.all_params[ex] - if p.constraints or ex in by_trigger or ex in by_param: + p = REGISTRY.all_params.get(ex) + if p and (p.constraints or ex in by_trigger or ex in by_param): pattern_has_constraints = True if get_math_symbol(ex): pattern_has_symbols = True @@ -476,7 +484,8 @@ def generate_parameter_docs() -> str: for pattern, examples in sorted(patterns.items()): example = examples[0] - desc = get_description(example) or "" + _ep = REGISTRY.all_params.get(example) + desc = _ep.description if _ep else "" # Truncate long descriptions if len(desc) > 60: desc = desc[:57] + "..." @@ -489,14 +498,9 @@ def generate_parameter_docs() -> str: sym = get_math_symbol(example) row += f" | {sym}" if pattern_has_constraints: - p = REGISTRY.all_params[example] - constraints = _format_constraints(p) - deps = _format_validator_rules(example, by_trigger, by_param) - extra = "; ".join(filter(None, [constraints, deps])) - if not extra: - extra = _format_tag_annotation(example, p) - extra = _escape_pct_outside_backticks(extra) - row += f" | {extra}" + p = REGISTRY.all_params.get(example) + if p: + row += f" | {_format_constraints_cell(example, p, by_trigger, by_param)}" lines.append(row + " |") lines.append("") @@ -514,24 +518,17 @@ def generate_parameter_docs() -> str: for name, param in params: type_str = _type_to_str(param.param_type) - desc = get_description(name) or "" + desc = param.description # Truncate long descriptions if len(desc) > 80: desc = desc[:77] + "..." - constraints = _format_constraints(param) - deps = _format_validator_rules(name, by_trigger, by_param) - extra = "; ".join(filter(None, [constraints, deps])) - if not extra: - extra = _format_tag_annotation(name, param) - extra = _escape_pct_outside_backticks(extra) - # Escape % for Doxygen (even inside backtick code spans) name_escaped = _escape_percent(name) desc = _escape_percent(desc) row = f"| `{name_escaped}` | {type_str} | {desc}" if full_has_symbols: sym = get_math_symbol(name) row += f" | {sym}" - row += f" | {extra}" + row += f" | {_format_constraints_cell(name, param, by_trigger, by_param)}" lines.append(row + " |") lines.append("") diff --git a/toolchain/mfc/params/generators/fortran_gen.py b/toolchain/mfc/params/generators/fortran_gen.py new file mode 100644 index 0000000000..c68227035c --- /dev/null +++ b/toolchain/mfc/params/generators/fortran_gen.py @@ -0,0 +1,160 @@ +"""Fortran parameter code generator — namelist and scalar decl fragments per target.""" + +from pathlib import Path +from typing import List, Tuple + +from ..definitions import CASE_OPT_PARAMS, FORTRAN_ARRAY_DIMS, NAMELIST_VARS # noqa: F401 - triggers registry population +from ..registry import REGISTRY +from ..schema import ParamDef, ParamType + +TARGETS = [("pre", "pre_process"), ("sim", "simulation"), ("post", "post_process")] +TARGET_FROM_DIR = {full: short for short, full in TARGETS} + +_HEADER = "! AUTO-GENERATED - do not edit directly. Regenerate: cmake reconfigure\n!\n" +_MAX_LINE = 130 +_FIRST_PREFIX = "namelist /user_inputs/ " +_CONT_PREFIX = " & " +_CONT2_PREFIX = " & " # inside #:if block +_DECL_COL = 24 # '::' column for scalars, matches ffmt alignment +_ARRAY_DECL_COL = 36 # '::' column for array decls + +_FORTRAN_TYPES = { + ParamType.INT: "integer", + ParamType.ANALYTIC_INT: "integer", + ParamType.REAL: "real(wp)", + ParamType.ANALYTIC_REAL: "real(wp)", + ParamType.LOG: "logical", +} + +_VALID_TARGETS = ("pre", "sim", "post") + + +def _check_target(target: str) -> None: + if target not in _VALID_TARGETS: + raise ValueError(f"Unknown target {target!r}; expected one of {_VALID_TARGETS}") + + +def get_namelist_var(name: str) -> str: + """Return the Fortran namelist root for a parameter name.""" + if "(" in name and "%" in name: + return name.split("(", 1)[0] + if "%" in name: + return name.split("%", 1)[0] + return name + + +def fortran_type_decl(param: ParamDef) -> str: + if param.param_type == ParamType.STR: + return f"character(LEN={param.str_len})" + return _FORTRAN_TYPES[param.param_type] + + +def _is_simple_scalar(name: str) -> bool: + return "%" not in name and "(" not in name + + +def _vars_for_target(target: str) -> List[str]: + return sorted(v for v, ts in NAMELIST_VARS.items() if target in ts) + + +def _pack_namelist(vars_list: List[str], first_prefix: str, cont_prefix: str, max_line: int) -> List[str]: + """Pack variable names into Fortran continuation lines; all but last end with ', &'.""" + if not vars_list: + return [] + lines: List[str] = [] + prefix = first_prefix + current_vars: List[str] = [] + current_len = len(prefix) + for var in vars_list: + additional = len(var) + (2 if current_vars else 0) + if current_vars and current_len + additional + 3 > max_line: + lines.append(prefix + ", ".join(current_vars) + ", &") + prefix = cont_prefix + current_vars = [var] + current_len = len(cont_prefix) + len(var) + else: + current_vars.append(var) + current_len += additional + if current_vars: + lines.append(prefix + ", ".join(current_vars)) + return lines + + +def _format_namelist(vars_list: List[str]) -> str: + return "\n".join(_pack_namelist(vars_list, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE)) + + +def generate_namelist_fpp(target: str) -> str: + """Return the namelist /user_inputs/ statement for a target as a string.""" + _check_target(target) + all_vars = _vars_for_target(target) + + if target != "sim": + return _HEADER + _format_namelist(all_vars) + "\n" + + normal = [v for v in all_vars if v not in CASE_OPT_PARAMS] + opt = sorted(v for v in CASE_OPT_PARAMS if v in NAMELIST_VARS and "sim" in NAMELIST_VARS[v]) + nl_lines = _pack_namelist(normal, _FIRST_PREFIX, _CONT_PREFIX, _MAX_LINE) + if opt and nl_lines: + opt_lines = _pack_namelist(opt, _CONT_PREFIX, _CONT2_PREFIX, _MAX_LINE) + nl_with_cont = nl_lines[:] + nl_with_cont[-1] += ", &" + parts = [_HEADER.rstrip(), "#:if MFC_CASE_OPTIMIZATION"] + nl_lines + ["#:else"] + nl_with_cont + opt_lines + ["#:endif"] + else: + parts = [_HEADER.rstrip()] + nl_lines + return "\n".join(parts) + "\n" + + +def generate_decls_fpp(target: str) -> str: + """Return Fortran declarations (scalars + known arrays) for a target.""" + _check_target(target) + lines = [_HEADER.rstrip()] + for name in _vars_for_target(target): + if not _is_simple_scalar(name): + continue + if target == "sim" and name in CASE_OPT_PARAMS: + continue + if name in FORTRAN_ARRAY_DIMS: + member = REGISTRY.all_params.get(f"{name}(1)") + if member is None: + raise ValueError(f"FORTRAN_ARRAY_DIMS[{name!r}] has no {name}(1) in the registry. " "Register at least one indexed variant (e.g. _r(f'{name}(1)', ...)).") + ftype = fortran_type_decl(member) + dim = FORTRAN_ARRAY_DIMS[name] + lines.append(f"{(ftype + ', dimension(' + dim + ')').ljust(_ARRAY_DECL_COL)}:: {name}") + continue + param = REGISTRY.all_params.get(name) + if param is None: + continue + if any(k.startswith(f"{name}(") for k in REGISTRY.all_params): + raise ValueError(f"{name!r} has indexed variants (e.g. {name}(1)) but is missing from " "FORTRAN_ARRAY_DIMS. Add it there with its Fortran dimension expression.") + lines.append(f"{fortran_type_decl(param).ljust(_DECL_COL)}:: {name}") + return "\n".join(lines) + "\n" + + +def resolve_namelist_content(fpp_path: Path) -> str: + """Return the namelist content for an fpp file. + + If the file delegates to a generated include, returns the generated content. + Otherwise returns the file's raw text. + """ + text = fpp_path.read_text() + if "#:include 'generated_namelist.fpp'" not in text: + return text + short = TARGET_FROM_DIR.get(fpp_path.parent.name) + if short is None: + raise ValueError(f"Cannot determine MFC target from path: {fpp_path}") + return generate_namelist_fpp(short) + + +def get_generated_files(build_dir: Path) -> List[Tuple[Path, str]]: + """Return (path, content) for all 6 generated .fpp files under build_dir. + + Paths match the cmake include directory structure: + build_dir/include/{full_target}/generated_{namelist,decls}.fpp + """ + result = [] + for short, full in TARGETS: + inc = build_dir / "include" / full + result.append((inc / "generated_namelist.fpp", generate_namelist_fpp(short))) + result.append((inc / "generated_decls.fpp", generate_decls_fpp(short))) + return result diff --git a/toolchain/mfc/params/generators/json_schema_gen.py b/toolchain/mfc/params/generators/json_schema_gen.py index dd39994733..5533f512f9 100644 --- a/toolchain/mfc/params/generators/json_schema_gen.py +++ b/toolchain/mfc/params/generators/json_schema_gen.py @@ -48,19 +48,14 @@ def generate_json_schema(include_descriptions: bool = True) -> Dict[str, Any]: Returns: JSON Schema dict """ - from ..descriptions import get_description - properties = {} all_params = [] for name, param in sorted(REGISTRY.all_params.items()): prop_schema = _param_type_to_json_schema(param.param_type, param.constraints) - if include_descriptions: - # Get description from descriptions module - desc = get_description(name) - if desc: - prop_schema["description"] = desc + if include_descriptions and param.description: + prop_schema["description"] = param.description # Add deprecation notice if applicable if param.dependencies and "deprecated" in param.dependencies: @@ -104,15 +99,13 @@ def write_json_schema(output_path: str, include_descriptions: bool = True) -> No def get_schema_stats() -> Dict[str, int]: """Get statistics about the generated schema.""" - from ..descriptions import get_description - schema = generate_json_schema(include_descriptions=False) props = schema.get("properties", {}) stats = { "total_params": len(props), "with_constraints": sum(1 for p in props.values() if "enum" in p or "minimum" in p or "maximum" in p), - "with_descriptions": sum(1 for name in REGISTRY.all_params if get_description(name)), + "with_descriptions": sum(1 for _, p in REGISTRY.all_params.items() if p.description), } return stats diff --git a/toolchain/mfc/params/namelist_parser.py b/toolchain/mfc/params/namelist_parser.py index 52385255d3..311a0362fa 100644 --- a/toolchain/mfc/params/namelist_parser.py +++ b/toolchain/mfc/params/namelist_parser.py @@ -5,474 +5,71 @@ from each target's namelist definition. This ensures the Python toolchain stays in sync with what the Fortran code actually accepts. -When Fortran sources are unavailable (e.g. Homebrew installs), a built-in -fallback parameter set is used instead. +When Fortran sources are unavailable (e.g. Homebrew installs), the fallback +is computed from NAMELIST_VARS in definitions.py. """ import re from pathlib import Path from typing import Dict, Optional, 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 -_FALLBACK_PARAMS = { - "pre_process": { - "Bx0", - "Ca", - "R0ref", - "Re_inv", - "Web", - "a_x", - "a_y", - "a_z", - "adv_n", - "bc_x", - "bc_y", - "bc_z", - "bub_pp", - "bubbles_euler", - "bubbles_lagrange", - "case_dir", - "cfl_adap_dt", - "cfl_const_dt", - "cont_damage", - "cyl_coord", - "dist_type", - "down_sample", - "elliptic_smoothing", - "elliptic_smoothing_iters", - "fft_wrt", - "file_per_process", - "fluid_pp", - "fluid_rho", - "hyper_cleaning", - "hyperelasticity", - "hypoelasticity", - "ib", - "igr", - "igr_order", - "loops_x", - "loops_y", - "loops_z", - "m", - "mhd", - "mixlayer_perturb", - "mixlayer_perturb_k0", - "mixlayer_perturb_nk", - "mixlayer_vel_coef", - "mixlayer_vel_profile", - "model_eqns", - "mpp_lim", - "muscl_order", - "n", - "n_start", - "n_start_old", - "nb", - "num_bc_patches", - "num_fluids", - "num_ibs", - "num_patches", - "old_grid", - "old_ic", - "p", - "palpha_eps", - "parallel_io", - "patch_bc", - "patch_ib", - "patch_icpp", - "perturb_flow", - "perturb_flow_fluid", - "perturb_flow_mag", - "perturb_sph", - "perturb_sph_fluid", - "pi_fac", - "poly_sigma", - "polydisperse", - "polytropic", - "pre_stress", - "precision", - "pref", - "ptgalpha_eps", - "qbmm", - "recon_type", - "relativity", - "relax", - "relax_model", - "rhoRV", - "rhoref", - "sigR", - "sigV", - "sigma", - "simplex_params", - "simplex_perturb", - "stretch_x", - "stretch_y", - "stretch_z", - "surface_tension", - "t_step_old", - "t_step_start", - "thermal", - "viscous", - "weno_order", - "x_a", - "x_b", - "x_domain", - "y_a", - "y_b", - "y_domain", - "z_a", - "z_b", - "z_domain", - }, - "simulation": { - "Bx0", - "Ca", - "R0ref", - "Re_inv", - "Web", - "acoustic", - "acoustic_source", - "adap_dt", - "adap_dt_max_iters", - "adap_dt_tol", - "adv_n", - "alf_factor", - "alpha_bar", - "alt_soundspeed", - "avg_state", - "bc_x", - "bc_y", - "bc_z", - "bf_x", - "bf_y", - "bf_z", - "bub_pp", - "bubble_model", - "bubbles_euler", - "bubbles_lagrange", - "case_dir", - "cfl_adap_dt", - "cfl_const_dt", - "cfl_target", - "chem_params", - "cont_damage", - "cont_damage_s", - "cyl_coord", - "down_sample", - "dt", - "fd_order", - "fft_wrt", - "file_per_process", - "fluid_pp", - "g_x", - "g_y", - "g_z", - "hyper_cleaning", - "hyper_cleaning_speed", - "hyper_cleaning_tau", - "hyperelasticity", - "hypoelasticity", - "ib", - "ib_state_wrt", - "ic_beta", - "ic_eps", - "igr", - "igr_iter_solver", - "igr_order", - "igr_pres_lim", - "int_comp", - "integral", - "integral_wrt", - "k_x", - "k_y", - "k_z", - "lag_params", - "low_Mach", - "m", - "mapped_weno", - "mhd", - "mixture_err", - "model_eqns", - "mp_weno", - "mpp_lim", - "muscl_lim", - "muscl_order", - "n", - "n_start", - "nb", - "null_weights", - "num_bc_patches", - "num_fluids", - "num_ibs", - "num_igr_iters", - "num_igr_warm_start_iters", - "num_integrals", - "num_probes", - "num_source", - "nv_uvm_igr_temps_on_gpu", - "nv_uvm_out_of_core", - "nv_uvm_pref_gpu", - "p", - "p_x", - "p_y", - "p_z", - "palpha_eps", - "parallel_io", - "patch_ib", - "pi_fac", - "poly_sigma", - "polydisperse", - "polytropic", - "precision", - "pref", - "prim_vars_wrt", - "probe", - "probe_wrt", - "ptgalpha_eps", - "qbmm", - "rdma_mpi", - "recon_type", - "relativity", - "relax", - "relax_model", - "rhoref", - "riemann_solver", - "run_time_info", - "sigma", - "surface_tension", - "t_save", - "t_step_old", - "t_step_print", - "t_step_save", - "t_step_start", - "t_step_stop", - "t_stop", - "tau_star", - "teno", - "teno_CT", - "thermal", - "time_stepper", - "viscous", - "w_x", - "w_y", - "w_z", - "wave_speeds", - "weno_Re_flux", - "weno_avg", - "weno_eps", - "weno_order", - "wenoz", - "wenoz_q", - "x_a", - "x_b", - "x_domain", - "y_a", - "y_b", - "y_domain", - "z_a", - "z_b", - "z_domain", - }, - "post_process": { - "Bx0", - "Ca", - "E_wrt", - "G", - "R0ref", - "Re_inv", - "Web", - "adv_n", - "alpha_rho_e_wrt", - "alpha_rho_wrt", - "alpha_wrt", - "alt_soundspeed", - "avg_state", - "bc_x", - "bc_y", - "bc_z", - "bub_pp", - "bubbles_euler", - "bubbles_lagrange", - "c_wrt", - "case_dir", - "cf_wrt", - "cfl_adap_dt", - "cfl_const_dt", - "cfl_target", - "chem_wrt_T", - "chem_wrt_Y", - "cons_vars_wrt", - "cont_damage", - "cyl_coord", - "down_sample", - "fd_order", - "fft_wrt", - "file_per_process", - "fluid_pp", - "flux_lim", - "flux_wrt", - "format", - "gamma_wrt", - "heat_ratio_wrt", - "hyper_cleaning", - "hyperelasticity", - "hypoelasticity", - "ib", - "ib_state_wrt", - "igr", - "igr_order", - "lag_betaC_wrt", - "lag_betaT_wrt", - "lag_db_wrt", - "lag_dphidt_wrt", - "lag_header", - "lag_id_wrt", - "lag_mg_wrt", - "lag_mv_wrt", - "lag_pos_prev_wrt", - "lag_pos_wrt", - "lag_pres_wrt", - "lag_r0_wrt", - "lag_rad_wrt", - "lag_rmax_wrt", - "lag_rmin_wrt", - "lag_rvel_wrt", - "lag_txt_wrt", - "lag_vel_wrt", - "liutex_wrt", - "m", - "mhd", - "mixture_err", - "model_eqns", - "mom_wrt", - "mpp_lim", - "muscl_order", - "n", - "n_start", - "nb", - "num_bc_patches", - "num_fluids", - "num_ibs", - "omega_wrt", - "output_partial_domain", - "p", - "parallel_io", - "pi_inf_wrt", - "poly_sigma", - "polydisperse", - "polytropic", - "precision", - "pref", - "pres_inf_wrt", - "pres_wrt", - "prim_vars_wrt", - "qbmm", - "qm_wrt", - "recon_type", - "relativity", - "relax", - "relax_model", - "rho_wrt", - "rhoref", - "schlieren_alpha", - "schlieren_wrt", - "sigR", - "sigma", - "sim_data", - "surface_tension", - "t_save", - "t_step_save", - "t_step_start", - "t_step_stop", - "t_stop", - "thermal", - "vel_wrt", - "weno_order", - "x_output", - "y_output", - "z_output", - }, -} +def _fallback_params() -> Dict[str, Set[str]]: + # Lazy import avoids circular dependency (definitions -> namelist_parser). + from .definitions import NAMELIST_VARS + from .generators.fortran_gen import TARGETS -def parse_namelist_from_file(filepath: Path) -> Set[str]: - """ - Parse a Fortran file and extract parameter names from the namelist definition. + return {full: {v for v, ts in NAMELIST_VARS.items() if short in ts} for short, full in TARGETS} - Args: - filepath: Path to the Fortran source file (m_start_up.fpp) - Returns: - Set of parameter names found in the namelist - """ - content = filepath.read_text() +def parse_namelist_from_file(filepath: Path) -> Set[str]: + """Parse parameter names from the namelist /user_inputs/ block in an fpp file.""" + from .generators.fortran_gen import resolve_namelist_content - # Find the namelist block - starts with "namelist /user_inputs/" - # and continues until a line without continuation (&) or a blank line - namelist_match = re.search(r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=)", content, re.DOTALL | re.IGNORECASE) + content = resolve_namelist_content(filepath) + namelist_match = re.search( + r"namelist\s+/user_inputs/\s*(.+?)(?=\n\s*\n|\n\s*!(?!\s*&)|\n\s*[a-zA-Z_]+\s*=|$)", + content, + re.DOTALL | re.IGNORECASE, + ) if not namelist_match: raise ValueError(f"Could not find namelist /user_inputs/ in {filepath}") namelist_text = namelist_match.group(1) - - # Remove Fortran line continuations (&) and join lines namelist_text = re.sub(r"&\s*\n\s*", " ", namelist_text) - - # Remove preprocessor directives (#:if, #:endif, etc.) namelist_text = re.sub(r"#:.*", "", namelist_text) - - # Remove comments (! to end of line, but not inside strings) namelist_text = re.sub(r"!.*", "", namelist_text) - # Extract parameter names - they're comma-separated identifiers - # Parameter names are alphanumeric with underscores found_params = set() for match in re.finditer(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\b", namelist_text): name = match.group(1) - # Skip Fortran keywords that might appear if name.lower() not in {"namelist", "user_inputs", "if", "endif", "not"}: found_params.add(name) - return found_params def parse_all_namelists(mfc_root: Path) -> Dict[str, Set[str]]: - """ - Parse namelist definitions from all MFC targets. - - Args: - mfc_root: Path to MFC root directory + """Parse namelist definitions from all MFC targets. - Returns: - Dict mapping target name to set of valid parameter names. - Falls back to built-in parameter sets when sources are unavailable. + Falls back to NAMELIST_VARS when Fortran sources are unavailable. """ - targets = { - "pre_process": mfc_root / "src" / "pre_process" / "m_start_up.fpp", - "simulation": mfc_root / "src" / "simulation" / "m_start_up.fpp", - "post_process": mfc_root / "src" / "post_process" / "m_start_up.fpp", + src = mfc_root / "src" + target_files = { + "pre_process": src / "pre_process" / "m_start_up.fpp", + "simulation": src / "simulation" / "m_start_up.fpp", + "post_process": src / "post_process" / "m_start_up.fpp", } - result = {} - for target_name, filepath in targets.items(): + for filepath in target_files.values(): if not filepath.exists(): - # Source files not available (e.g. Homebrew install). - # Use built-in fallback parameters. - return dict(_FALLBACK_PARAMS) - result[target_name] = parse_namelist_from_file(filepath) + return _fallback_params() - return result + return {name: parse_namelist_from_file(path) for name, path in target_files.items()} def parse_fortran_constants(filepath: Path) -> Dict[str, int]: - """ - Parse integer parameter constants from a Fortran source file. - - Extracts lines like ``integer, parameter :: name = 123`` and returns - a dict mapping constant names to their integer values. - """ + """Parse integer parameter constants from a Fortran source file.""" constants: Dict[str, int] = {} pattern = re.compile(r"integer\s*,\s*parameter\s*::\s*(\w+)\s*=\s*(\d+)", re.IGNORECASE) try: @@ -484,103 +81,61 @@ def parse_fortran_constants(filepath: Path) -> Dict[str, int]: return constants -# Module-level cache for Fortran constants (None = not yet loaded) _FORTRAN_CONSTANTS_CACHE: Optional[Dict[str, int]] = None def get_fortran_constants() -> Dict[str, int]: - """ - Get Fortran compile-time constants from m_constants.fpp. + """Get Fortran compile-time constants from m_constants.fpp. - Cached after first call. Returns an empty dict when the Fortran source is - unavailable (e.g. Homebrew installs where src/ is not shipped); callers - supply their own inline defaults via _fc(name, default) in definitions.py. + Cached after first call. Returns an empty dict when src/ is unavailable; + callers supply inline defaults via _fc(name, default) in definitions.py. """ global _FORTRAN_CONSTANTS_CACHE # noqa: PLW0603 if _FORTRAN_CONSTANTS_CACHE is None: - root = get_mfc_root() - path = root / "src" / "common" / "m_constants.fpp" + path = get_mfc_root() / "src" / "common" / "m_constants.fpp" _FORTRAN_CONSTANTS_CACHE = parse_fortran_constants(path) return _FORTRAN_CONSTANTS_CACHE def get_mfc_root() -> Path: - """Get the MFC root directory from this file's location.""" - # This file is at toolchain/mfc/params/namelist_parser.py - # MFC root is 4 levels up + """Return the MFC root directory (4 levels above this file).""" return Path(__file__).resolve().parent.parent.parent.parent -# Module-level cache for parsed target params _TARGET_PARAMS_CACHE: Dict[str, Set[str]] = {} def get_target_params() -> Dict[str, Set[str]]: - """ - Get the valid parameters for each target, parsing Fortran if needed. - - Returns: - Dict mapping target name to set of valid parameter names - """ + """Return valid parameters per target, parsing Fortran sources if needed.""" if not _TARGET_PARAMS_CACHE: _TARGET_PARAMS_CACHE.update(parse_all_namelists(get_mfc_root())) return _TARGET_PARAMS_CACHE def is_param_valid_for_target(param_name: str, target_name: str) -> bool: - """ - Check if a parameter is valid for a given target. - - This handles both scalar params (like "m") and indexed params - (like "patch_icpp(1)%geometry") by checking the base name. + """Return True if param_name is valid for target_name. - Args: - param_name: The parameter name (may include indices like "(1)%attr") - target_name: One of 'pre_process', 'simulation', 'post_process' - - Returns: - True if the parameter is valid for the target + Handles indexed params (patch_icpp(1)%geometry) by checking the base name. """ valid_params = get_target_params().get(target_name, set()) - - # Extract base parameter name (before any index or attribute) - # e.g., "patch_icpp(1)%geometry" -> "patch_icpp" - # e.g., "fluid_pp(2)%gamma" -> "fluid_pp" base_match = re.match(r"^([a-zA-Z_][a-zA-Z0-9_]*)", param_name) - if base_match: - return base_match.group(1) in valid_params - - return param_name in valid_params + return base_match.group(1) in valid_params if base_match else param_name in valid_params if __name__ == "__main__": - # Test the parser import sys try: parsed_targets = parse_all_namelists(get_mfc_root()) - print("Parsed namelist parameters:\n") for tgt, tgt_params in sorted(parsed_targets.items()): print(f"{tgt}: {len(tgt_params)} parameters") - # Print first 10 as sample - sorted_list = sorted(tgt_params) - for param in sorted_list[:10]: + for param in sorted(tgt_params)[:10]: print(f" - {param}") if len(tgt_params) > 10: print(f" ... and {len(tgt_params) - 10} more") print() - # Show params unique to each target - print("Parameters unique to each target:\n") - all_param_names = set.union(*parsed_targets.values()) - for tgt, tgt_params in sorted(parsed_targets.items()): - other = set.union(*[p for t, p in parsed_targets.items() if t != tgt]) - unique = tgt_params - other - print(f"{tgt} only ({len(unique)}): {sorted(unique)[:15]}...") - print() - - # Show params in all targets common = set.intersection(*parsed_targets.values()) print(f"Parameters in ALL targets ({len(common)}): {sorted(common)[:20]}...") diff --git a/toolchain/mfc/params/schema.py b/toolchain/mfc/params/schema.py index e0132e19fa..7c89f5657d 100644 --- a/toolchain/mfc/params/schema.py +++ b/toolchain/mfc/params/schema.py @@ -64,6 +64,7 @@ class ParamDef: tags: Set[str] = field(default_factory=set) # Feature tags: "mhd", "bubbles", etc. hint: str = "" # Constraint/usage hint for docs (e.g. "Used with grcbc_in") math_symbol: str = "" # LaTeX math symbol (Doxygen format, e.g. "\\f$\\gamma_k\\f$") + str_len: str = "name_len" # For STR type: Fortran character length constant ("path_len" for case_dir) def __post_init__(self): # Validate name diff --git a/toolchain/mfc/params_cmd.py b/toolchain/mfc/params_cmd.py index 577dfeeb55..e40fdfc874 100644 --- a/toolchain/mfc/params_cmd.py +++ b/toolchain/mfc/params_cmd.py @@ -304,8 +304,6 @@ def _show_families(registry, limit): 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 - query_lower = query.lower() matches = [] desc_matches = set() # Track which params matched via description @@ -315,9 +313,7 @@ def _search_params(registry, query, type_filter, limit, describe=False, search_d desc_match = False if search_descriptions and not name_match: - # Also search in description - desc = get_description(name) - if desc and query_lower in desc.lower(): + if param.description and query_lower in param.description.lower(): desc_match = True desc_matches.add(name) @@ -352,7 +348,7 @@ def _search_params(registry, query, type_filter, limit, describe=False, search_d def _show_collapsed_results(collapsed, describe=False): """Show collapsed search results.""" - from .params.descriptions import get_description, get_pattern_description + from .params.descriptions import get_pattern_description # Check if any items have index ranges to show has_ranges = any(len(item) == 4 and item[2] > 1 for item in collapsed) @@ -365,11 +361,10 @@ def _show_collapsed_results(collapsed, describe=False): count = item[2] range_str = item[3] if len(item) == 4 else "" - # Get description - use pattern description for indexed params if "(N)" in name or "(M)" in name: desc = get_pattern_description(name) else: - desc = get_description(name) + desc = param.description cons.print(f" [cyan]{name}[/cyan]") cons.print(f" Type: {param.param_type.name}") diff --git a/toolchain/mfc/params_tests/test_fortran_gen.py b/toolchain/mfc/params_tests/test_fortran_gen.py new file mode 100644 index 0000000000..24e604d21f --- /dev/null +++ b/toolchain/mfc/params_tests/test_fortran_gen.py @@ -0,0 +1,162 @@ +def test_get_namelist_var_simple(): + from mfc.params.generators.fortran_gen import get_namelist_var + + assert get_namelist_var("m") == "m" + assert get_namelist_var("dt") == "dt" + + +def test_get_namelist_var_indexed_family(): + from mfc.params.generators.fortran_gen import get_namelist_var + + assert get_namelist_var("fluid_pp(1)%gamma") == "fluid_pp" + assert get_namelist_var("patch_icpp(3)%geometry") == "patch_icpp" + + +def test_get_namelist_var_struct_member(): + from mfc.params.generators.fortran_gen import get_namelist_var + + assert get_namelist_var("bc_x%beg") == "bc_x" + assert get_namelist_var("lag_params%solver_approach") == "lag_params" + + +def test_fortran_type_int(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.INT)) == "integer" + + +def test_fortran_type_real(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.REAL)) == "real(wp)" + + +def test_fortran_type_log(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + + assert fortran_type_decl(ParamDef(name="x", param_type=ParamType.LOG)) == "logical" + + +def test_fortran_type_str(): + from mfc.params.generators.fortran_gen import fortran_type_decl + from mfc.params.schema import ParamDef, ParamType + + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert fortran_type_decl(p) == "character(LEN=path_len)" + + +def test_namelist_contains_common_vars(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + + for target in ("pre", "sim", "post"): + c = generate_namelist_fpp(target) + for v in ("m", "n", "p", "bc_x", "case_dir", "fluid_pp"): + assert v in c, f"{v!r} missing from {target} namelist" + + +def test_sim_namelist_case_opt_guard(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + + c = generate_namelist_fpp("sim") + # Case-opt guard: two complete namelist statements wrapped in #:if/#:else/#:endif + assert "#:if MFC_CASE_OPTIMIZATION" in c + assert "#:else" in c + assert "#:endif" in c + assert "weno_order" in c + assert "num_fluids" in c + # No dangling continuation before the #:if block or after #:else + lines = c.splitlines() + for i, line in enumerate(lines): + if line.strip() in ("#:if MFC_CASE_OPTIMIZATION", "#:endif"): + assert not lines[i - 1].rstrip().endswith("&"), f"Dangling & before {line!r}" + + +def test_pre_namelist_has_patch_icpp(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + + c = generate_namelist_fpp("pre") + assert "patch_icpp" in c + assert "run_time_info" not in c + + +def test_post_namelist_has_sim_data(): + from mfc.params.generators.fortran_gen import generate_namelist_fpp + + c = generate_namelist_fpp("post") + assert "sim_data" in c + assert "patch_icpp" not in c + + +def test_decls_contains_simple_scalars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + for target in ("pre", "sim", "post"): + c = generate_decls_fpp(target) + assert "integer" in c + assert "real(wp)" in c + assert "logical" in c + + +def test_decls_dt_for_sim(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + # Column-aligned output: type padded to 24 chars before '::' + assert "real(wp) :: dt" in generate_decls_fpp("sim") + + +def test_decls_no_percent_vars(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + for target in ("pre", "sim", "post"): + c = generate_decls_fpp(target) + assert "bc_x%beg" not in c + assert "fluid_pp(1)" not in c + + +def test_decls_case_dir(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + for target in ("pre", "sim", "post"): + assert "character(LEN=path_len) :: case_dir" in generate_decls_fpp(target) + + +def test_decls_array_dims(): + from mfc.params.generators.fortran_gen import generate_decls_fpp + + post = generate_decls_fpp("post") + assert "dimension(num_fluids_max)" in post and ":: alpha_wrt" in post + assert "dimension(num_fluids_max)" in post and ":: alpha_rho_wrt" in post + assert "dimension(3)" in post and ":: mom_wrt" in post + # Structs and families must NOT appear as bare scalar declarations + assert ":: fluid_pp" not in post + assert ":: bc_x" not in post + + pre = generate_decls_fpp("pre") + assert "dimension(num_fluids_max)" in pre and ":: fluid_rho" in pre + + +def test_check_target_raises_on_bad_target(): + import pytest + + from mfc.params.generators.fortran_gen import generate_decls_fpp, generate_namelist_fpp + + with pytest.raises(ValueError, match="Unknown target"): + generate_namelist_fpp("bad") + with pytest.raises(ValueError, match="Unknown target"): + generate_decls_fpp("bad") + + +def test_get_generated_files_returns_six(): + from pathlib import Path + + from mfc.params.generators.fortran_gen import get_generated_files + + files = get_generated_files(Path("/build")) + assert len(files) == 6 + paths = [str(p) for p, _ in files] + assert any("pre_process/generated_namelist.fpp" in p for p in paths) + assert any("simulation/generated_decls.fpp" in p for p in paths) + assert any("post_process/generated_namelist.fpp" in p for p in paths) diff --git a/toolchain/mfc/params_tests/test_namelist_targets.py b/toolchain/mfc/params_tests/test_namelist_targets.py new file mode 100644 index 0000000000..0b4fd90b5b --- /dev/null +++ b/toolchain/mfc/params_tests/test_namelist_targets.py @@ -0,0 +1,48 @@ +def test_common_vars_in_all_targets(): + from mfc.params.definitions import NAMELIST_VARS + + for var in ["m", "n", "p", "bc_x", "bc_y", "bc_z", "model_eqns", "cyl_coord", "fluid_pp", "case_dir"]: + assert {"pre", "sim", "post"}.issubset(NAMELIST_VARS.get(var, set())), f"{var!r} not marked for all targets" + + +def test_sim_only_vars(): + from mfc.params.definitions import NAMELIST_VARS + + for var in ["run_time_info", "dt", "riemann_solver", "acoustic", "probe"]: + targets = NAMELIST_VARS.get(var, set()) + assert "sim" in targets, f"{var!r} not marked for sim" + assert "pre" not in targets, f"{var!r} incorrectly marked for pre" + assert "post" not in targets, f"{var!r} incorrectly marked for post" + + +def test_pre_only_vars(): + from mfc.params.definitions import NAMELIST_VARS + + for var in ["old_grid", "old_ic", "patch_icpp", "simplex_params"]: + targets = NAMELIST_VARS.get(var, set()) + assert "pre" in targets, f"{var!r} not marked for pre" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + + +def test_post_only_vars(): + from mfc.params.definitions import NAMELIST_VARS + + for var in ["format", "sim_data", "lag_header", "output_partial_domain"]: + targets = NAMELIST_VARS.get(var, set()) + assert "post" in targets, f"{var!r} not marked for post" + assert "sim" not in targets, f"{var!r} incorrectly in sim" + + +def test_case_opt_params_in_namelist(): + from mfc.params.definitions import CASE_OPT_PARAMS + + for var in ["nb", "mapped_weno", "wenoz", "weno_order", "num_fluids"]: + assert var in CASE_OPT_PARAMS + + +def test_case_opt_params_in_sim_namelist(): + from mfc.params.definitions import CASE_OPT_PARAMS, NAMELIST_VARS + + for var in CASE_OPT_PARAMS: + targets = NAMELIST_VARS.get(var, set()) + assert "sim" in targets, f"CASE_OPT_PARAMS var {var!r} must also be in sim namelist" diff --git a/toolchain/mfc/params_tests/test_schema.py b/toolchain/mfc/params_tests/test_schema.py new file mode 100644 index 0000000000..d99913a00f --- /dev/null +++ b/toolchain/mfc/params_tests/test_schema.py @@ -0,0 +1,34 @@ +"""Tests for ParamDef str_len field.""" + + +def test_paramdef_str_len_default(): + from mfc.params.schema import ParamDef, ParamType + + p = ParamDef(name="foo", param_type=ParamType.STR) + assert p.str_len == "name_len" + + +def test_paramdef_str_len_override(): + from mfc.params.schema import ParamDef, ParamType + + p = ParamDef(name="case_dir", param_type=ParamType.STR, str_len="path_len") + assert p.str_len == "path_len" + + +def test_case_dir_has_path_len(): + import mfc.params.definitions # noqa: F401 + from mfc.params.registry import REGISTRY + + p = REGISTRY.get_param_def("case_dir") + assert p is not None + assert p.str_len == "path_len" + + +def test_other_str_param_has_default_len(): + import mfc.params.definitions # noqa: F401 + from mfc.params.registry import REGISTRY + + # cantera_file is another STR param that should use the default name_len + p = REGISTRY.get_param_def("cantera_file") + assert p is not None + assert p.str_len == "name_len" diff --git a/toolchain/mfc/run/archive.py b/toolchain/mfc/run/archive.py index 0c3848060a..4a31754897 100644 --- a/toolchain/mfc/run/archive.py +++ b/toolchain/mfc/run/archive.py @@ -11,6 +11,15 @@ from ..state import ARG, CFG from . import input + +def _relpath_safe(path: str, dirpath: str) -> str: + """Return relpath(path, dirpath), falling back to basename on cross-device paths.""" + try: + return os.path.relpath(path, dirpath) + except ValueError: + return os.path.basename(path) + + ARTIFACT_FILENAMES = [ "equations.dat", "run_time.inf", @@ -74,13 +83,7 @@ def __collect_sources(case: input.MFCInputFile, targets) -> list: def __build_manifest(case: input.MFCInputFile, targets, sources: list, archive_path: str, archive_format: str) -> dict: dirpath = case.dirpath - relative_sources = [] - for src in sources: - try: - rel = os.path.relpath(src, dirpath) - except ValueError: - rel = src - relative_sources.append(rel) + relative_sources = [_relpath_safe(src, dirpath) for src in sources] return { "timestamp": datetime.datetime.now().astimezone().isoformat(), @@ -101,11 +104,7 @@ def __copy_dir(sources: list, case: input.MFCInputFile, dest: str) -> None: dirpath = case.dirpath for src in sources: - try: - rel = os.path.relpath(src, dirpath) - except ValueError: - rel = os.path.basename(src) - + rel = _relpath_safe(src, dirpath) target_path = os.path.join(dest, rel) os.makedirs(os.path.dirname(target_path), exist_ok=True) @@ -120,10 +119,7 @@ def __write_tar(sources: list, case: input.MFCInputFile, dest: str, compressed: arcroot = os.path.basename(dest).removesuffix(".tar.zst").removesuffix(".tar") def rel_for(path: str) -> str: - try: - return os.path.relpath(path, dirpath) - except ValueError: - return os.path.basename(path) + return _relpath_safe(path, dirpath) if compressed: if not does_command_exist("tar"): diff --git a/toolchain/mfc/run/case_dicts.py b/toolchain/mfc/run/case_dicts.py index 7100e9e7dd..ac8438833c 100644 --- a/toolchain/mfc/run/case_dicts.py +++ b/toolchain/mfc/run/case_dicts.py @@ -1,8 +1,7 @@ """ MFC Case Parameter Type Definitions. -This module provides exports from the central parameter registry (mfc.params). -All parameter definitions are sourced from the registry. +Exports from the central parameter registry (mfc.params). Exports: ALL: Family-aware mapping of all parameters {name: ParamType} @@ -20,14 +19,10 @@ class _ParamTypeMapping(Mapping): - """ - Read-only mapping wrapping REGISTRY's all_params for {name: ParamType} access. - - Delegates containment checks and lookup to the registry's family-aware - mapping, so indexed families like ``patch_ib(500000)%geometry`` resolve - in O(1) without enumerating all possible indices. + """Read-only {name: ParamType} view over REGISTRY.all_params. - For iteration, yields scalar params plus one example per family attr. + Delegates containment and lookup to the registry's family-aware mapping, + so indexed families like ``patch_ib(500000)%geometry`` resolve in O(1). """ def __init__(self): @@ -48,115 +43,55 @@ def __len__(self): return len(self._view) -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.""" +def _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() + return REGISTRY -# Parameters to ignore during certain operations IGNORE = ["cantera_file", "chemistry"] - -# Family-aware mapping of all parameters — supports O(1) lookup for indexed families ALL = _ParamTypeMapping() +CASE_OPTIMIZATION = [n for n, p in _registry().all_params.items() if p.case_optimization] +SCHEMA = _registry().get_json_schema() -# Parameters that can be hard-coded for GPU case optimization -CASE_OPTIMIZATION = _load_case_optimization_params() - -# JSON schema for validation -SCHEMA = _build_schema() - -# Regex to extract the base name from indexed params _BASE_NAME_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_]*)") def _is_param_valid_for_target(param_name: str, target_name: str) -> bool: - """ - Check if a parameter is valid for a given target. - - Uses the Fortran namelist definitions as the source of truth. - Handles indexed params like "patch_icpp(1)%geometry" by checking base name. - Args: - param_name: The parameter name (may include indices) - target_name: One of 'pre_process', 'simulation', 'post_process' - - Returns: - True if the parameter is valid for the target - """ - target_params = _get_target_params().get(target_name, set()) + from ..params.namelist_parser import get_target_params - # Extract base parameter name (before any index or attribute) - # e.g., "patch_icpp(1)%geometry" -> "patch_icpp" - # e.g., "fluid_pp(2)%gamma" -> "fluid_pp" - # e.g., "acoustic(1)%loc(1)" -> "acoustic" + target_params = get_target_params().get(target_name, set()) match = _BASE_NAME_RE.match(param_name) - if match: - base_name = match.group(1) - return base_name in target_params - - return param_name in target_params + return match.group(1) in target_params if match else param_name in target_params class _TargetKeySet: - """ - Set-like object for checking if a param is valid for a specific target. + """Set-like object for checking param validity for a specific target. - Supports ``key in target_key_set`` via base-name matching against the - Fortran namelist, plus optionally filtering out case-optimization params. - Does not enumerate all possible indexed family members. + Supports ``key in obj`` via base-name matching against the Fortran namelist, + optionally filtering out case-optimization params. """ def __init__(self, target_name: str, filter_case_opt: bool = False): self._target_name = target_name - self._filter_case_opt = filter_case_opt self._case_opt = set(CASE_OPTIMIZATION) if filter_case_opt else set() def __contains__(self, key): - if self._filter_case_opt and key in self._case_opt: + if key in self._case_opt: return False return _is_param_valid_for_target(key, self._target_name) def get_input_dict_keys(target_name: str): - """ - Get a set-like object for checking parameter validity for a target. - - Returns an object that supports ``key in result`` for O(1) checks. - For indexed families, this does NOT enumerate all possible indices — - it checks the base name against the Fortran namelist. - - Args: - target_name: One of 'pre_process', 'simulation', 'post_process' + """Return a set-like object for checking parameter validity for a target. - Returns: - Set-like object supporting ``in`` operator + Supports ``key in result`` for O(1) checks. Does NOT enumerate indexed family + members — checks the base name against the Fortran namelist. """ filter_case_opt = ARG("case_optimization", dflt=False) and target_name == "simulation" return _TargetKeySet(target_name, filter_case_opt) def get_validator(): - """Get the cached JSON schema validator.""" - return _get_validator_func() + """Return the cached JSON schema validator.""" + return _registry().get_validator() diff --git a/toolchain/mfc/run/input.py b/toolchain/mfc/run/input.py index dda710f602..dd3a122cf3 100644 --- a/toolchain/mfc/run/input.py +++ b/toolchain/mfc/run/input.py @@ -47,22 +47,21 @@ def get_cantera_solution(self): if self.params.get("chemistry", "F") == "T": cantera_file = self.params["cantera_file"] - candidates = [ cantera_file, os.path.join(self.dirpath, cantera_file), os.path.join(common.MFC_MECHANISMS_DIR, cantera_file), ] else: - # If Chemistry is turned off, we return a default (dummy) solution - # that will not be used in the simulation, so that MFC can still - # be compiled. - candidates = ["h2o2.yaml"] + # Chemistry is off — return a dummy solution so MFC still compiles. + cantera_file = "h2o2.yaml" + candidates = [cantera_file] for candidate in candidates: try: return ct.Solution(candidate) - except Exception: + except Exception as e: + cons.print(f"[dim] Cantera: skipping '{candidate}': {e}[/dim]") continue raise common.MFCException(f"Cantera file '{cantera_file}' not found. Searched: {', '.join(candidates)}.") diff --git a/toolchain/mfc/run/run.py b/toolchain/mfc/run/run.py index 82e886c064..6823360423 100644 --- a/toolchain/mfc/run/run.py +++ b/toolchain/mfc/run/run.py @@ -33,31 +33,20 @@ def __validate_job_options() -> None: raise MFCException(f"RUN: {ARG('email')} is not a valid e-mail address.") -def __profiler_prepend() -> typing.List[str]: - if ARG("ncu") is not None: - 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") - - if ARG("nsys") is not None: - if not does_command_exist("nsys"): - raise MFCException("Failed to locate [bold green]NVIDIA Nsight Systems[/bold green] (nsys).") - - return ["nsys", "profile", "--stats=true", "--trace=mpi,nvtx,openacc"] + ARG("nsys") +_PROFILERS = [ + ("ncu", "ncu", "[bold green]NVIDIA Nsight Compute[/bold green]", lambda: ["ncu", "--nvtx", "--mode=launch-and-attach", "--cache-control=none", "--clock-control=none"] + ARG("ncu")), + ("nsys", "nsys", "[bold green]NVIDIA Nsight Systems[/bold green]", lambda: ["nsys", "profile", "--stats=true", "--trace=mpi,nvtx,openacc"] + ARG("nsys")), + ("rcu", "rocprof-compute", "[bold red]ROCM rocprof-compute[/bold red]", lambda: ["rocprof-compute", "profile", "-n", ARG("name").replace("-", "_").replace(".", "_")] + ARG("rcu") + ["--"]), + ("rsys", "rocprof", "[bold red]ROCM rocprof-systems[/bold red]", lambda: ["rocprof"] + ARG("rsys")), +] - if ARG("rcu") is not None: - 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") + ["--"] - - if ARG("rsys") is not None: - if not does_command_exist("rocprof"): - raise MFCException("Failed to locate [bold red]ROCM rocprof-systems[/bold red] (rocprof-systems).") - - return ["rocprof"] + ARG("rsys") +def __profiler_prepend() -> typing.List[str]: + for arg, cmd, label, build_args in _PROFILERS: + if ARG(arg) is not None: + if not does_command_exist(cmd): + raise MFCException(f"Failed to locate {label} ({cmd}).") + return build_args() return [] diff --git a/toolchain/mfc/test/cases.py b/toolchain/mfc/test/cases.py index b72584a032..f059582cbf 100644 --- a/toolchain/mfc/test/cases.py +++ b/toolchain/mfc/test/cases.py @@ -162,6 +162,26 @@ def _h_sweep(case_path, ndim, cons_vars, extra_args, expected, tol, resolutions, ) +def make_3d_box_patches( + z_centroids=(0.05, 0.45, 0.9), + z_lengths=(0.1, 0.7, 0.2), + geometry=9, +) -> dict: + """3-patch 3D box IC: uniform xy plane (centroid=0.5, length=1), z spacing given.""" + d = {} + for pid in range(1, 4): + d[f"patch_icpp({pid})%geometry"] = geometry + for vel in (1, 2, 3): + d[f"patch_icpp({pid})%vel({vel})"] = 0.0 + d[f"patch_icpp({pid})%x_centroid"] = 0.5 + d[f"patch_icpp({pid})%length_x"] = 1 + d[f"patch_icpp({pid})%y_centroid"] = 0.5 + d[f"patch_icpp({pid})%length_y"] = 1 + d[f"patch_icpp({pid})%z_centroid"] = z_centroids[pid - 1] + d[f"patch_icpp({pid})%length_z"] = z_lengths[pid - 1] + return d + + def get_bc_mods(bc: int, dimInfo): params = {} for dimCmp in dimInfo[0]: @@ -2027,25 +2047,7 @@ def mpi_consistency_tests(): "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) # Bubbles with 2 MPI ranks stack.push( @@ -2203,25 +2205,7 @@ def restart_roundtrip_tests(): "bc_z%beg": -3, "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) stack.push("Restart Roundtrip -> 3D", base_3d) cases.append(define_case_d(stack, "", {}, restart_check=True)) stack.pop() @@ -2259,25 +2243,7 @@ def kernel_golden_tests(): "bc_z%beg": -3, "bc_z%end": -3, } - for patchID in range(1, 4): - base_3d[f"patch_icpp({patchID})%geometry"] = 9 - base_3d[f"patch_icpp({patchID})%vel(1)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(2)"] = 0.0 - base_3d[f"patch_icpp({patchID})%vel(3)"] = 0.0 - base_3d[f"patch_icpp({patchID})%x_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_x"] = 1 - base_3d[f"patch_icpp({patchID})%y_centroid"] = 0.5 - base_3d[f"patch_icpp({patchID})%length_y"] = 1 - base_3d.update( - { - "patch_icpp(1)%z_centroid": 0.05, - "patch_icpp(1)%length_z": 0.1, - "patch_icpp(2)%z_centroid": 0.45, - "patch_icpp(2)%length_z": 0.7, - "patch_icpp(3)%z_centroid": 0.9, - "patch_icpp(3)%length_z": 0.2, - } - ) + base_3d.update(make_3d_box_patches()) # 3D grid stretching in all directions. # The cosh-based stretching expands the domain beyond the original diff --git a/toolchain/mfc/viz/interactive.py b/toolchain/mfc/viz/interactive.py index 4eaa5785b5..54ac5887de 100644 --- a/toolchain/mfc/viz/interactive.py +++ b/toolchain/mfc/viz/interactive.py @@ -1693,7 +1693,6 @@ def _update( overlay_vol_nsurf, playing_st, ): - _t0 = time.perf_counter() _GRAPH_SHOW = {"height": "100vh", "display": "block"} _GRAPH_HIDE = {"height": "100vh", "display": "none"} diff --git a/toolchain/pyproject.toml b/toolchain/pyproject.toml index 4f2dd2554e..b814a5070d 100644 --- a/toolchain/pyproject.toml +++ b/toolchain/pyproject.toml @@ -23,9 +23,10 @@ dependencies = [ # Code Health "typos", - "ruff", + "ruff==0.6.5", "ffmt==0.4.1", "ansi2txt", + "pytest", # Profiling "numpy", @@ -57,6 +58,10 @@ dependencies = [ "tqdm", ] +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["."] + [tool.hatch.metadata] allow-direct-references = true