diff --git a/env/HERA.env b/env/HERA.env index f10bfcc5378..7c27579db9f 100755 --- a/env/HERA.env +++ b/env/HERA.env @@ -105,6 +105,11 @@ elif [[ "${step}" = "atmanlfv3inc" ]]; then export NTHREADS_ATMANLFV3INC=${NTHREADSmax} export APRUN_ATMANLFV3INC="${APRUN_default} --cpus-per-task=${NTHREADS_ATMANLFV3INC}" +elif [[ "${step}" = "anlstat" ]]; then + + export NTHREADS_ANLSTAT=${NTHREADSmax} + export APRUN_ANLSTAT="${APRUN_default} --cpus-per-task=${NTHREADS_ANLSTAT}" + elif [[ "${step}" = "prepobsaero" ]]; then export NTHREADS_PREPOBSAERO=${NTHREADS1} diff --git a/env/HERCULES.env b/env/HERCULES.env index 15e3928f08c..fccc2f87a51 100755 --- a/env/HERCULES.env +++ b/env/HERCULES.env @@ -145,7 +145,7 @@ case ${step} in export NTHREADS_OCNANALECEN=${threads_per_task_ocnanalecen:-${max_threads_per_task}} [[ ${NTHREADS_OCNANALECEN} -gt ${max_threads_per_task} ]] && export NTHREADS_OCNANALECEN=${max_threads_per_task} export APRUN_OCNANALECEN="${launcher} -n ${ntasks_ocnanalecen} --cpus-per-task=${NTHREADS_OCNANALECEN}" -;; + ;; "marineanlchkpt") export APRUNCFP="${launcher} -n \$ncmd ${mpmd_opt}" @@ -153,6 +153,11 @@ case ${step} in export NTHREADS_OCNANAL=${NTHREADSmax} export APRUN_MARINEANLCHKPT="${APRUN_default} --cpus-per-task=${NTHREADS_OCNANAL}" ;; + "marineanlletkf") + + export NTHREADS_MARINEANLLETKF=${NTHREADSmax} + export APRUN_MARINEANLLETKF="${APRUN_default}" + ;; "anal" | "analcalc") export MKL_NUM_THREADS=4 diff --git a/env/WCOSS2.env b/env/WCOSS2.env index fff8f7b096a..27001bebd7a 100755 --- a/env/WCOSS2.env +++ b/env/WCOSS2.env @@ -107,17 +107,15 @@ elif [[ "${step}" = "marinebmat" ]]; then export APRUNCFP="${launcher} -n \$ncmd --multi-prog" export APRUN_MARINEBMAT="${APRUN_default}" -elif [[ "${step}" = "ocnanalrun" ]]; then +elif [[ "${step}" = "marineanlvar" ]]; then export APRUNCFP="${launcher} -n \$ncmd --multi-prog" + export APRUN_MARINEANLVAR="${APRUN_default}" - export APRUN_OCNANAL="${APRUN_default}" - -elif [[ "${step}" = "ocnanalchkpt" ]]; then +elif [[ "${step}" = "marineanlchkpt" ]]; then export APRUNCFP="${launcher} -n \$ncmd --multi-prog" - - export APRUN_OCNANAL="${APRUN_default}" + export APRUN_MARINEANLCHKPT="${APRUN_default}" elif [[ "${step}" = "ocnanalecen" ]]; then diff --git a/jobs/JGLOBAL_ANALYSIS_STATS b/jobs/JGLOBAL_ANALYSIS_STATS new file mode 100755 index 00000000000..c816796acfa --- /dev/null +++ b/jobs/JGLOBAL_ANALYSIS_STATS @@ -0,0 +1,38 @@ +#! /usr/bin/env bash + +source "${HOMEgfs}/ush/preamble.sh" +source "${HOMEgfs}/ush/jjob_header.sh" -e "anlstat" -c "base anlstat" + +############################################## +# Set variables used in the script +############################################## + + +############################################## +# Begin JOB SPECIFIC work +############################################## + +# Generate COM variables from templates +YMD=${PDY} HH=${cyc} declare_from_tmpl -rx COM_OBS COM_CHEM_ANALYSIS + + +############################################################### +# Run relevant script + +EXSCRIPT=${ANLSTATSPY:-${SCRgfs}/exglobal_analysis_stats.py} +${EXSCRIPT} +status=$? +[[ ${status} -ne 0 ]] && exit "${status}" + +############################################## +# End JOB SPECIFIC work +############################################## + +############################################## +# Final processing +############################################## +if [[ -e "${pgmout}" ]] ; then + cat "${pgmout}" +fi + +exit 0 diff --git a/jobs/rocoto/anlstat.sh b/jobs/rocoto/anlstat.sh new file mode 100755 index 00000000000..ac7d8af16a2 --- /dev/null +++ b/jobs/rocoto/anlstat.sh @@ -0,0 +1,18 @@ +#! /usr/bin/env bash + +source "${HOMEgfs}/ush/preamble.sh" + +############################################################### +# Source UFSDA workflow modules +. "${HOMEgfs}/ush/load_ufsda_modules.sh" +status=$? +[[ ${status} -ne 0 ]] && exit "${status}" + +export job="anlstat" +export jobid="${job}.$$" + +############################################################### +# Execute the JJOB +"${HOMEgfs}/jobs/JGLOBAL_ANALYSIS_STATS" +status=$? +exit "${status}" diff --git a/parm/config/gfs/config.anlstat b/parm/config/gfs/config.anlstat new file mode 100644 index 00000000000..1aed963d65d --- /dev/null +++ b/parm/config/gfs/config.anlstat @@ -0,0 +1,16 @@ +#!/bin/bash -x + +########## config.anlstat ########## +# Analysis Stat + +echo "BEGIN: config.anlstat" + +# Get task specific resources +source "${EXPDIR}/config.resources" anlstat + +export JEDI_CONFIG_YAML="${PARMgfs}/gdas/statanl_jedi_config.yaml.j2" +export JCB_BASE_YAML="${PARMgfs}/gdas/stat/aero/jcb-base.yaml.j2" +export JCB_ALGO_YAML="${PARMgfs}/gdas/jcb-algorithms/anlstat.yaml.j2" +export JEDIEXE=${HOMEgfs}/sorc/gdas.cd/build/bin/ioda-stats.x + +echo "END: config.anlstat" diff --git a/parm/config/gfs/config.atmanl b/parm/config/gfs/config.atmanl index 9a06088eccf..1d700a479c8 100644 --- a/parm/config/gfs/config.atmanl +++ b/parm/config/gfs/config.atmanl @@ -5,8 +5,7 @@ echo "BEGIN: config.atmanl" -export JCB_BASE_YAML="${PARMgfs}/gdas/atm/jcb-base.yaml.j2" -export JCB_ALGO_YAML=@JCB_ALGO_YAML@ +export JCB_ALGO_YAML_VAR=@JCB_ALGO_YAML_VAR@ export STATICB_TYPE=@STATICB_TYPE@ export LOCALIZATION_TYPE="bump" @@ -23,6 +22,8 @@ fi export CRTM_FIX_YAML="${PARMgfs}/gdas/atm_crtm_coeff.yaml.j2" export JEDI_FIX_YAML="${PARMgfs}/gdas/atm_jedi_fix.yaml.j2" + +export JEDI_CONFIG_YAML="${PARMgfs}/gdas/atmanl_jedi_config.yaml.j2" export VAR_BKG_STAGING_YAML="${PARMgfs}/gdas/staging/atm_var_bkg.yaml.j2" export BERROR_STAGING_YAML="${PARMgfs}/gdas/staging/atm_berror_${STATICB_TYPE}.yaml.j2" export FV3ENS_STAGING_YAML="${PARMgfs}/gdas/staging/atm_var_fv3ens.yaml.j2" @@ -33,6 +34,4 @@ export layout_y_atmanl=@LAYOUT_Y_ATMANL@ export io_layout_x=@IO_LAYOUT_X@ export io_layout_y=@IO_LAYOUT_Y@ -export JEDIEXE=${EXECgfs}/gdas.x - echo "END: config.atmanl" diff --git a/parm/config/gfs/config.atmanlfv3inc b/parm/config/gfs/config.atmanlfv3inc index ab7efa3a606..4e7714628e6 100644 --- a/parm/config/gfs/config.atmanlfv3inc +++ b/parm/config/gfs/config.atmanlfv3inc @@ -8,7 +8,4 @@ echo "BEGIN: config.atmanlfv3inc" # Get task specific resources . "${EXPDIR}/config.resources" atmanlfv3inc -export JCB_ALGO=fv3jedi_fv3inc_variational -export JEDIEXE=${EXECgfs}/fv3jedi_fv3inc.x - echo "END: config.atmanlfv3inc" diff --git a/parm/config/gfs/config.atmensanl b/parm/config/gfs/config.atmensanl index f5a12782485..2726f655bdf 100644 --- a/parm/config/gfs/config.atmensanl +++ b/parm/config/gfs/config.atmensanl @@ -5,17 +5,16 @@ echo "BEGIN: config.atmensanl" -export JCB_BASE_YAML="${PARMgfs}/gdas/atm/jcb-base.yaml.j2" -if [[ ${lobsdiag_forenkf} = ".false." ]] ; then - export JCB_ALGO_YAML=@JCB_ALGO_YAML_LETKF@ -else - export JCB_ALGO_YAML=@JCB_ALGO_YAML_OBS@ -fi +export JCB_ALGO_YAML_LETKF=@JCB_ALGO_YAML_LETKF@ +export JCB_ALGO_YAML_OBS=@JCB_ALGO_YAML_OBS@ +export JCB_ALGO_YAML_SOL=@JCB_ALGO_YAML_SOL@ export INTERP_METHOD='barycentric' export CRTM_FIX_YAML="${PARMgfs}/gdas/atm_crtm_coeff.yaml.j2" export JEDI_FIX_YAML="${PARMgfs}/gdas/atm_jedi_fix.yaml.j2" + +export JEDI_CONFIG_YAML="${PARMgfs}/gdas/atmensanl_jedi_config.yaml.j2" export LGETKF_BKG_STAGING_YAML="${PARMgfs}/gdas/staging/atm_lgetkf_bkg.yaml.j2" export layout_x_atmensanl=@LAYOUT_X_ATMENSANL@ @@ -24,6 +23,4 @@ export layout_y_atmensanl=@LAYOUT_Y_ATMENSANL@ export io_layout_x=@IO_LAYOUT_X@ export io_layout_y=@IO_LAYOUT_Y@ -export JEDIEXE=${EXECgfs}/gdas.x - echo "END: config.atmensanl" diff --git a/parm/config/gfs/config.atmensanlfv3inc b/parm/config/gfs/config.atmensanlfv3inc index 2dc73f3f6e1..fe3337e5a28 100644 --- a/parm/config/gfs/config.atmensanlfv3inc +++ b/parm/config/gfs/config.atmensanlfv3inc @@ -8,7 +8,4 @@ echo "BEGIN: config.atmensanlfv3inc" # Get task specific resources . "${EXPDIR}/config.resources" atmensanlfv3inc -export JCB_ALGO=fv3jedi_fv3inc_lgetkf -export JEDIEXE=${EXECgfs}/fv3jedi_fv3inc.x - echo "END: config.atmensanlfv3inc" diff --git a/parm/config/gfs/config.atmensanlobs b/parm/config/gfs/config.atmensanlobs index dff3fa3095a..c7e050b009e 100644 --- a/parm/config/gfs/config.atmensanlobs +++ b/parm/config/gfs/config.atmensanlobs @@ -8,6 +8,4 @@ echo "BEGIN: config.atmensanlobs" # Get task specific resources . "${EXPDIR}/config.resources" atmensanlobs -export JCB_ALGO_YAML=@JCB_ALGO_YAML@ - echo "END: config.atmensanlobs" diff --git a/parm/config/gfs/config.atmensanlsol b/parm/config/gfs/config.atmensanlsol index dac161373b7..8ef905d1bde 100644 --- a/parm/config/gfs/config.atmensanlsol +++ b/parm/config/gfs/config.atmensanlsol @@ -8,6 +8,4 @@ echo "BEGIN: config.atmensanlsol" # Get task specific resources . "${EXPDIR}/config.resources" atmensanlsol -export JCB_ALGO_YAML=@JCB_ALGO_YAML@ - echo "END: config.atmensanlsol" diff --git a/parm/config/gfs/config.base b/parm/config/gfs/config.base index 4f702f96683..c6525f9ddc2 100644 --- a/parm/config/gfs/config.base +++ b/parm/config/gfs/config.base @@ -75,9 +75,6 @@ export DO_NPOESS="@DO_NPOESS@" # NPOESS products export DO_TRACKER="@DO_TRACKER@" # Hurricane track verification export DO_GENESIS="@DO_GENESIS@" # Cyclone genesis verification export DO_GENESIS_FSU="@DO_GENESIS_FSU@" # Cyclone genesis verification (FSU) -export DO_VERFOZN="YES" # Ozone data assimilation monitoring -export DO_VERFRAD="YES" # Radiance data assimilation monitoring -export DO_VMINMON="YES" # GSI minimization monitoring export DO_MOS="NO" # GFS Model Output Statistics - Only supported on WCOSS2 # NO for retrospective parallel; YES for real-time parallel @@ -474,6 +471,14 @@ if [[ ${DO_JEDIATMVAR} = "YES" ]]; then export DO_VERFOZN="NO" # Ozone data assimilation monitoring export DO_VERFRAD="NO" # Radiance data assimilation monitoring export DO_VMINMON="NO" # GSI minimization monitoring + export DO_ANLSTAT="YES" # JEDI-based analysis statistics +else + export DO_VERFOZN="YES" # Ozone data assimilation monitoring + export DO_VERFRAD="YES" # Radiance data assimilation monitoring + export DO_VMINMON="YES" # GSI minimization monitoring + if [[ ${DO_AERO} = "YES" || ${DO_JEDIOCNVAR} = "YES" || ${DO_JEDISNOWDA} = "YES " ]]; then + export DO_ANLSTAT="YES" # JEDI-based analysis statistics + fi fi # If starting ICs that are not at cycle hour diff --git a/parm/config/gfs/config.marineanl b/parm/config/gfs/config.marineanl index a19fc015e2e..0b55fa447db 100644 --- a/parm/config/gfs/config.marineanl +++ b/parm/config/gfs/config.marineanl @@ -5,6 +5,8 @@ echo "BEGIN: config.marineanl" +export JEDI_CONFIG_YAML="${PARMgfs}/gdas/soca_bmat_jedi_config.yaml.j2" + export MARINE_OBS_YAML_DIR="${PARMgfs}/gdas/soca/obs/config" export MARINE_OBS_LIST_YAML=@SOCA_OBS_LIST@ export SOCA_INPUT_FIX_DIR=@SOCA_INPUT_FIX_DIR@ diff --git a/parm/config/gfs/config.marineanlletkf b/parm/config/gfs/config.marineanlletkf index 8b84af4eaa5..93f03f80ff3 100644 --- a/parm/config/gfs/config.marineanlletkf +++ b/parm/config/gfs/config.marineanlletkf @@ -14,7 +14,7 @@ export MARINE_LETKF_STAGE_YAML_TMPL="${PARMgfs}/gdas/soca/letkf/letkf_stage.yaml export MARINE_LETKF_SAVE_YAML_TMPL="${PARMgfs}/gdas/soca/letkf/letkf_save.yaml.j2" export GRIDGEN_EXEC="${EXECgfs}/gdas_soca_gridgen.x" -export GRIDGEN_YAML="${PARMgfs}/gdas/soca/gridgen/gridgen.yaml" +export GRIDGEN_YAML="${HOMEgfs}/sorc/gdas.cd/parm/jcb-gdas/algorithm/marine/soca_gridgen.yaml.j2" export DIST_HALO_SIZE=500000 echo "END: config.marineanlletkf" diff --git a/parm/config/gfs/config.marinebmat b/parm/config/gfs/config.marinebmat index 00352737d0c..d88739dcedc 100644 --- a/parm/config/gfs/config.marinebmat +++ b/parm/config/gfs/config.marinebmat @@ -8,12 +8,4 @@ echo "BEGIN: config.marinebmat" # Get task specific resources . "${EXPDIR}/config.resources" marinebmat -export BERROR_DIAGB_YAML="${PARMgfs}/gdas/soca/berror/soca_diagb.yaml.j2" -export BERROR_VTSCALES_YAML="${PARMgfs}/gdas/soca/berror/soca_vtscales.yaml.j2" -export BERROR_DIFFV_YAML="${PARMgfs}/gdas/soca/berror/soca_parameters_diffusion_vt.yaml.j2" -export BERROR_HZSCALES_YAML="${PARMgfs}/gdas/soca/berror/soca_setcorscales.yaml" -export BERROR_DIFFH_YAML="${PARMgfs}/gdas/soca/berror/soca_parameters_diffusion_hz.yaml.j2" -export BERROR_ENS_RECENTER_YAML="${PARMgfs}/gdas/soca/berror/soca_ensb.yaml.j2" -export BERROR_HYB_WEIGHTS_YAML="${PARMgfs}/gdas/soca/berror/soca_ensweights.yaml.j2" - echo "END: config.marinebmat" diff --git a/parm/config/gfs/config.resources b/parm/config/gfs/config.resources index cddd1643fdd..7d2122d4209 100644 --- a/parm/config/gfs/config.resources +++ b/parm/config/gfs/config.resources @@ -17,7 +17,7 @@ if (( $# != 1 )); then echo "atmensanlinit atmensanlobs atmensanlsol atmensanlletkf atmensanlfv3inc atmensanlfinal" echo "snowanl esnowrecen" echo "prepobsaero aeroanlinit aeroanlvar aeroanlfinal aeroanlgenb" - echo "anal sfcanl analcalc analdiag fcst echgres" + echo "anal sfcanl analcalc analdiag anlstat fcst echgres" echo "upp atmos_products" echo "tracker genesis genesis_fsu" echo "verfozn verfrad vminmon fit2obs metp arch cleanup" @@ -717,6 +717,14 @@ case ${step} in memory="48GB" ;; + "anlstat") + walltime="00:30:00" + ntasks=1 + threads_per_task=1 + tasks_per_node=$(( max_tasks_per_node / threads_per_task )) + memory="24GB" + ;; + "sfcanl") walltime="00:20:00" ntasks=${ntiles:-6} diff --git a/parm/config/gfs/config.resources.ORION b/parm/config/gfs/config.resources.ORION index 461b6f14f7c..fceb8feeb19 100644 --- a/parm/config/gfs/config.resources.ORION +++ b/parm/config/gfs/config.resources.ORION @@ -25,7 +25,8 @@ case ${step} in ;; "atmanlvar") # Run on 8 nodes for memory requirement - export tasks_per_node=8 + export tasks_per_node_gdas=8 + export tasks_per_node_gfs=8 export walltime="00:45:00" ;; "atmensanlobs") diff --git a/parm/config/gfs/yaml/defaults.yaml b/parm/config/gfs/yaml/defaults.yaml index 78caf46f5d8..d8cf76a47bb 100644 --- a/parm/config/gfs/yaml/defaults.yaml +++ b/parm/config/gfs/yaml/defaults.yaml @@ -23,7 +23,7 @@ base: FHMAX_ENKF_GFS: 12 atmanl: - JCB_ALGO_YAML: "${PARMgfs}/gdas/atm/jcb-prototype_3dvar.yaml.j2" + JCB_ALGO_YAML_VAR: "${PARMgfs}/gdas/atm/jcb-prototype_3dvar.yaml.j2" STATICB_TYPE: "gsibec" LAYOUT_X_ATMANL: 8 LAYOUT_Y_ATMANL: 8 @@ -33,16 +33,11 @@ atmanl: atmensanl: JCB_ALGO_YAML_LETKF: "${PARMgfs}/gdas/atm/jcb-prototype_lgetkf.yaml.j2" JCB_ALGO_YAML_OBS: "${PARMgfs}/gdas/atm/jcb-prototype_lgetkf_observer.yaml.j2" + JCB_ALGO_YAML_SOL: "${PARMgfs}/gdas/atm/jcb-prototype_lgetkf_solver.yaml.j2" LAYOUT_X_ATMENSANL: 8 LAYOUT_Y_ATMENSANL: 8 IO_LAYOUT_X: 1 IO_LAYOUT_Y: 1 - -atmensanlobs: - JCB_ALGO_YAML: "${PARMgfs}/gdas/atm/jcb-prototype_lgetkf_observer.yaml.j2" - -atmensanlsol: - JCB_ALGO_YAML: "${PARMgfs}/gdas/atm/jcb-prototype_lgetkf_solver.yaml.j2" aeroanl: IO_LAYOUT_X: 1 diff --git a/parm/gdas/atmanl_jedi_config.yaml.j2 b/parm/gdas/atmanl_jedi_config.yaml.j2 new file mode 100644 index 00000000000..4046ba09315 --- /dev/null +++ b/parm/gdas/atmanl_jedi_config.yaml.j2 @@ -0,0 +1,13 @@ +atmanlvar: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas.x' + mpi_cmd: '{{ APRUN_ATMANLVAR }}' + jedi_args: ['fv3jedi', 'variational'] + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo_yaml: '{{ JCB_ALGO_YAML_VAR }}' +atmanlfv3inc: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/fv3jedi_fv3inc.x' + mpi_cmd: '{{ APRUN_ATMANLFV3INC }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo: fv3jedi_fv3inc_variational diff --git a/parm/gdas/atmensanl_jedi_config.yaml.j2 b/parm/gdas/atmensanl_jedi_config.yaml.j2 new file mode 100644 index 00000000000..9ab2ec6ace1 --- /dev/null +++ b/parm/gdas/atmensanl_jedi_config.yaml.j2 @@ -0,0 +1,27 @@ +atmensanlobs: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas.x' + mpi_cmd: '{{ APRUN_ATMENSANLOBS }}' + jedi_args: ['fv3jedi', 'localensembleda'] + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo_yaml: '{{ JCB_ALGO_YAML_OBS }}' +atmensanlsol: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas.x' + mpi_cmd: '{{ APRUN_ATMENSANLSOL }}' + jedi_args: ['fv3jedi', 'localensembleda'] + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo_yaml: '{{ JCB_ALGO_YAML_SOL }}' +atmensanlfv3inc: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/fv3jedi_fv3inc.x' + mpi_cmd: '{{ APRUN_ATMENSANLFV3INC }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo: fv3jedi_fv3inc_lgetkf +atmensanlletkf: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas.x' + mpi_cmd: '{{ APRUN_ATMENSANLLETKF }}' + jedi_args: ['fv3jedi', 'localensembleda'] + jcb_base_yaml: '{{ PARMgfs }}/gdas/atm/jcb-base.yaml.j2' + jcb_algo_yaml: '{{ JCB_ALGO_YAML_LETKF }}' diff --git a/parm/gdas/soca_bmat_jedi_config.yaml.j2 b/parm/gdas/soca_bmat_jedi_config.yaml.j2 new file mode 100644 index 00000000000..4e476d31177 --- /dev/null +++ b/parm/gdas/soca_bmat_jedi_config.yaml.j2 @@ -0,0 +1,42 @@ +gridgen: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_soca_gridgen.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_gridgen +soca_diagb: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_soca_diagb.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_diagb +soca_parameters_diffusion_vt: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_soca_error_covariance_toolbox.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_parameters_diffusion_vt +soca_setcorscales: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_soca_setcorscales.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_setcorscales +soca_parameters_diffusion_hz: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_soca_error_covariance_toolbox.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_parameters_diffusion_hz +soca_ensb: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_ens_handler.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_ensb +soca_ensweights: + rundir: '{{ DATA }}' + exe_src: '{{ EXECgfs }}/gdas_socahybridweights.x' + mpi_cmd: '{{ APRUN_MARINEBMAT }}' + jcb_base_yaml: '{{ PARMgfs }}/gdas/soca/marine-jcb-base.yaml' + jcb_algo: soca_ensweights diff --git a/parm/gdas/stat/aero/jcb-base.yaml.j2 b/parm/gdas/stat/aero/jcb-base.yaml.j2 new file mode 100644 index 00000000000..f14a2664fd5 --- /dev/null +++ b/parm/gdas/stat/aero/jcb-base.yaml.j2 @@ -0,0 +1,24 @@ +# Search path for model and obs for JCB +# ------------------------------------- +algorithm_path: "{{PARMgfs}}/gdas/jcb-algorithms" +app_path_algorithm: "{{PARMgfs}}/gdas/jcb-gdas/algorithm/obstats/aero" + +window_begin: "{{ STAT_WINDOW_BEGIN | to_isotime }}" +window_YMDH: "{{ STAT_WINDOW_BEGIN | to_YMDH }}" +window_length: "{{ STAT_WINDOW_LENGTH }}" + +# Inputted list of ob spaces +# -------------------------- +obspaces: {{ OBSPACES_LIST }} + +# Obspace variable things +# ----------------------- +aero_obsdatain_path: "{{ DATA }}" +aero_obsdatatin_simulated_variables: ['aerosolOpticalDepth'] +aero_obsdatain_observed_variables: ['aerosolOpticalDepth'] + +# Variables +# --------- +aero_variables: ['aerosolOpticalDepth'] +aero_file_groups: ['bkgmob', 'bkgmob1'] +aero_file_qc_groups: ['EffectiveQC0', 'EffectiveQC1'] \ No newline at end of file diff --git a/parm/gdas/statanl_jedi_config.yaml.j2 b/parm/gdas/statanl_jedi_config.yaml.j2 new file mode 100644 index 00000000000..bbefbaac2cb --- /dev/null +++ b/parm/gdas/statanl_jedi_config.yaml.j2 @@ -0,0 +1,8 @@ +statanl: + rundir: '{{ DATA }}' + exe_src: '{{ JEDIEXE }}' + mpi_cmd: '{{ APRUN_ANLSTAT }}' + jedi_args: None + jcb_base_yaml: '{{ PARMgfs }}/gdas/stat/aero/jcb-base.yaml.j2' + jcb_algo_yaml: '{{ JCB_ALGO_YAML }}' + jcb_algo: 'anlstat' \ No newline at end of file diff --git a/scripts/exglobal_analysis_stats.py b/scripts/exglobal_analysis_stats.py new file mode 100755 index 00000000000..c9dda5a1cbe --- /dev/null +++ b/scripts/exglobal_analysis_stats.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +# exglobal_stat_analysis.py +# This script creates an StatAnalysis class +# and runs the initialize method +# which create and stage the runtime directory +# and create the YAML configuration +# for a global stat analysis +import os + +from wxflow import Logger, cast_strdict_as_dtypedict +from pygfs.task.stat_analysis import StatAnalysis + + +# Initialize root logger +logger = Logger(level='DEBUG', colored_log=True) + + +if __name__ == '__main__': + + # Take configuration from environment and cast it as python dictionary + config = cast_strdict_as_dtypedict(os.environ) + + # Instantiate the atm analysis task + StatAnl = StatAnalysis(config) + + # Initialize JEDI variational analysis + StatAnl.initialize() diff --git a/scripts/exglobal_atm_analysis_fv3_increment.py b/scripts/exglobal_atm_analysis_fv3_increment.py index 72413ddbd4d..c5a3e70943a 100755 --- a/scripts/exglobal_atm_analysis_fv3_increment.py +++ b/scripts/exglobal_atm_analysis_fv3_increment.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atm_analysis_fv3_increment.py # This script creates an AtmAnalysis object -# and runs the initialize_fv3inc and execute methods -# which convert the JEDI increment into an FV3 increment +# and runs the execute method which runs the JEDI +# FV3 increment converter import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,8 +18,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atm analysis object - AtmAnl = AtmAnalysis(config, 'atmanlfv3inc') + AtmAnl = AtmAnalysis(config) # Initialize and execute FV3 increment converter - AtmAnl.initialize_jedi() - AtmAnl.execute(config.APRUN_ATMANLFV3INC) + AtmAnl.execute('atmanlfv3inc') diff --git a/scripts/exglobal_atm_analysis_initialize.py b/scripts/exglobal_atm_analysis_initialize.py index 9deae07bb37..749d320111f 100755 --- a/scripts/exglobal_atm_analysis_initialize.py +++ b/scripts/exglobal_atm_analysis_initialize.py @@ -2,8 +2,8 @@ # exglobal_atm_analysis_initialize.py # This script creates an AtmAnalysis class # and runs the initialize method -# which create and stage the runtime directory -# and create the YAML configuration +# which creates and stages the runtime directory +# and creates the YAML configuration # for a global atm variational analysis import os @@ -20,8 +20,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atm analysis task - AtmAnl = AtmAnalysis(config, 'atmanlvar') + AtmAnl = AtmAnalysis(config) # Initialize JEDI variational analysis - AtmAnl.initialize_jedi() - AtmAnl.initialize_analysis() + AtmAnl.initialize() diff --git a/scripts/exglobal_atm_analysis_variational.py b/scripts/exglobal_atm_analysis_variational.py index 83595320695..9ad121f76cf 100755 --- a/scripts/exglobal_atm_analysis_variational.py +++ b/scripts/exglobal_atm_analysis_variational.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atm_analysis_variational.py # This script creates an AtmAnalysis object -# and runs the execute method -# which executes the global atm variational analysis +# and runs the execute method which runs the JEDI +# variational analysis application import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,7 +18,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atm analysis task - AtmAnl = AtmAnalysis(config, 'atmanlvar') + AtmAnl = AtmAnalysis(config) # Execute JEDI variational analysis - AtmAnl.execute(config.APRUN_ATMANLVAR, ['fv3jedi', 'variational']) + AtmAnl.execute('atmanlvar') diff --git a/scripts/exglobal_atmens_analysis_fv3_increment.py b/scripts/exglobal_atmens_analysis_fv3_increment.py index 48eb6a6a1e0..4506b280337 100755 --- a/scripts/exglobal_atmens_analysis_fv3_increment.py +++ b/scripts/exglobal_atmens_analysis_fv3_increment.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atmens_analysis_fv3_increment.py # This script creates an AtmEnsAnalysis object -# and runs the initialize_fv3inc and execute methods -# which convert the JEDI increment into an FV3 increment +# and runs the execute method which runs the JEDI +# FV3 increment converter application import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,8 +18,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atmens analysis object - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlfv3inc') + AtmEnsAnl = AtmEnsAnalysis(config) # Initialize and execute JEDI FV3 increment converter - AtmEnsAnl.initialize_jedi() - AtmEnsAnl.execute(config.APRUN_ATMENSANLFV3INC) + AtmEnsAnl.execute('atmensanlfv3inc') diff --git a/scripts/exglobal_atmens_analysis_initialize.py b/scripts/exglobal_atmens_analysis_initialize.py index 326fe80628c..124e755594e 100755 --- a/scripts/exglobal_atmens_analysis_initialize.py +++ b/scripts/exglobal_atmens_analysis_initialize.py @@ -2,8 +2,8 @@ # exglobal_atmens_analysis_initialize.py # This script creates an AtmEnsAnalysis class # and runs the initialize method -# which create and stage the runtime directory -# and create the YAML configuration +# which creates and stages the runtime directory +# and creates the YAML configuration # for a global atm local ensemble analysis import os @@ -20,11 +20,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atmens analysis task - if not config.lobsdiag_forenkf: - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlletkf') - else: - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlobs') + AtmEnsAnl = AtmEnsAnalysis(config) # Initialize JEDI ensemble DA analysis - AtmEnsAnl.initialize_jedi() - AtmEnsAnl.initialize_analysis() + AtmEnsAnl.initialize() diff --git a/scripts/exglobal_atmens_analysis_letkf.py b/scripts/exglobal_atmens_analysis_letkf.py index 45b06524fee..dea9ace5b82 100755 --- a/scripts/exglobal_atmens_analysis_letkf.py +++ b/scripts/exglobal_atmens_analysis_letkf.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atmens_analysis_letkf.py # This script creates an AtmEnsAnalysis object -# and runs the execute method which executes -# the global atm local ensemble analysis +# and initializes and runs the full JEDI LETKF +# application import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,7 +18,10 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atmens analysis task - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlletkf') + AtmEnsAnl = AtmEnsAnalysis(config) + + # Initalize JEDI full ensemble DA application + AtmEnsAnl.initialize_letkf() # Execute the JEDI ensemble DA analysis - AtmEnsAnl.execute(config.APRUN_ATMENSANLLETKF, ['fv3jedi', 'localensembleda']) + AtmEnsAnl.execute('atmensanlletkf') diff --git a/scripts/exglobal_atmens_analysis_obs.py b/scripts/exglobal_atmens_analysis_obs.py index c701f8cb4ea..b09c67703f9 100755 --- a/scripts/exglobal_atmens_analysis_obs.py +++ b/scripts/exglobal_atmens_analysis_obs.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atmens_analysis_obs.py # This script creates an AtmEnsAnalysis object -# and runs the execute method -# which executes the global atm local ensemble analysis in observer mode +# and runs the execute method which runs the JEDI LETKF +# application in observer mode import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,7 +18,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atmens analysis task - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlobs') + AtmEnsAnl = AtmEnsAnalysis(config) - # Initialize and execute JEDI ensembler DA analysis in observer mode - AtmEnsAnl.execute(config.APRUN_ATMENSANLOBS, ['fv3jedi', 'localensembleda']) + # Execute JEDI ensembler DA analysis in observer mode + AtmEnsAnl.execute('atmensanlobs') diff --git a/scripts/exglobal_atmens_analysis_sol.py b/scripts/exglobal_atmens_analysis_sol.py index be78e694b1f..85dc228a5ad 100755 --- a/scripts/exglobal_atmens_analysis_sol.py +++ b/scripts/exglobal_atmens_analysis_sol.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # exglobal_atmens_analysis_sol.py # This script creates an AtmEnsAnalysis object -# and runs the execute method -# which executes the global atm local ensemble analysis in solver mode +# and runs the execute method which runs the JEDI LETKF +# application in solver mode import os from wxflow import Logger, cast_strdict_as_dtypedict @@ -18,8 +18,7 @@ config = cast_strdict_as_dtypedict(os.environ) # Instantiate the atmens analysis task - AtmEnsAnl = AtmEnsAnalysis(config, 'atmensanlsol') + AtmEnsAnl = AtmEnsAnalysis(config) - # Initialize and execute JEDI ensemble DA analysis in solver mode - AtmEnsAnl.initialize_jedi() - AtmEnsAnl.execute(config.APRUN_ATMENSANLSOL, ['fv3jedi', 'localensembleda']) + # Execute JEDI ensemble DA analysis in solver mode + AtmEnsAnl.execute('atmensanlsol') diff --git a/sorc/gdas.cd b/sorc/gdas.cd index 764f58cebdf..fc54039b248 160000 --- a/sorc/gdas.cd +++ b/sorc/gdas.cd @@ -1 +1 @@ -Subproject commit 764f58cebdf64f3695d89538994a50183e5884d9 +Subproject commit fc54039b24821a9d0ac9866494d244ecd4819aa8 diff --git a/sorc/gsi_enkf.fd b/sorc/gsi_enkf.fd index 9f44c8798c2..529bb796bea 160000 --- a/sorc/gsi_enkf.fd +++ b/sorc/gsi_enkf.fd @@ -1 +1 @@ -Subproject commit 9f44c8798c2087aca06df8f629699632e57df431 +Subproject commit 529bb796bea0e490f186729cd168a91c034bb12d diff --git a/sorc/gsi_monitor.fd b/sorc/gsi_monitor.fd index 278ee629e87..e1f9f21af16 160000 --- a/sorc/gsi_monitor.fd +++ b/sorc/gsi_monitor.fd @@ -1 +1 @@ -Subproject commit 278ee629e87558822e8d13b3fb3b0e16006aa856 +Subproject commit e1f9f21af16ce912fdc2cd75c5b27094a550a0c5 diff --git a/sorc/gsi_utils.fd b/sorc/gsi_utils.fd index a6ea311e5c8..9382fd01c2a 160000 --- a/sorc/gsi_utils.fd +++ b/sorc/gsi_utils.fd @@ -1 +1 @@ -Subproject commit a6ea311e5c82369d255e3afdc99c1bce0c9a3014 +Subproject commit 9382fd01c2a626c8934c3f553d420a45de2b4dec diff --git a/sorc/link_workflow.sh b/sorc/link_workflow.sh index b35b7ff35af..ef0e3966796 100755 --- a/sorc/link_workflow.sh +++ b/sorc/link_workflow.sh @@ -356,6 +356,7 @@ if [[ -d "${HOMEgfs}/sorc/gdas.cd/build" ]]; then "gdassoca_obsstats.x" \ "gdasapp_land_ensrecenter.x" \ "bufr2ioda.x" \ + "ioda-stats.x" \ "calcfIMS.exe" \ "apply_incr.exe" ) for gdasexe in "${JEDI_EXE[@]}"; do diff --git a/sorc/ufs_utils.fd b/sorc/ufs_utils.fd index 06eec5b6f63..3ef2e6bd725 160000 --- a/sorc/ufs_utils.fd +++ b/sorc/ufs_utils.fd @@ -1 +1 @@ -Subproject commit 06eec5b6f636123835e2dfd9fc5229980c006735 +Subproject commit 3ef2e6bd725d2662fd6ee95897cb7bac222e5144 diff --git a/sorc/verif-global.fd b/sorc/verif-global.fd index b2ee80cac79..92904d2c431 160000 --- a/sorc/verif-global.fd +++ b/sorc/verif-global.fd @@ -1 +1 @@ -Subproject commit b2ee80cac7921a3016fa5a857cc58acfccc4baea +Subproject commit 92904d2c431969345968f74e676717057ec0042a diff --git a/ush/python/pygfs/jedi/jedi.py b/ush/python/pygfs/jedi/jedi.py index 415a0a3c081..2806ba4bce9 100644 --- a/ush/python/pygfs/jedi/jedi.py +++ b/ush/python/pygfs/jedi/jedi.py @@ -4,145 +4,190 @@ import tarfile from logging import getLogger from typing import List, Dict, Any, Optional +from pprint import pformat from jcb import render -from wxflow import (AttrDict, - FileHandler, +from wxflow import (AttrDict, FileHandler, Task, Executable, chdir, rm_p, - parse_j2yaml, + parse_j2yaml, save_as_yaml, logit, - Task, - Executable, WorkflowException) logger = getLogger(__name__.split('.')[-1]) +required_jedi_keys = ['rundir', 'exe_src', 'mpi_cmd'] +optional_jedi_keys = ['jedi_args', 'jcb_base_yaml', 'jcb_algo', 'jcb_algo_yaml'] + class Jedi: """ Class for initializing and executing JEDI applications """ + @logit(logger, name="Jedi") - def __init__(self, task_config: AttrDict, yaml_name: Optional[str] = None) -> None: + def __init__(self, config: Dict[str, Any]) -> None: """Constructor for JEDI objects This method will construct a Jedi object. This includes: - - save a copy of task_config for provenance - - set the default JEDI YAML and executable names - - set an empty AttrDict for the JEDI config - - set the default directory for J2-YAML templates + - create the jedi_config AttrDict and extend it with additional required entries + - save a copy of jedi_config Parameters ---------- - task_config: AttrDict - Attribute-dictionary of all configuration variables associated with a GDAS task. - yaml_name: str, optional - Name of YAML file for JEDI configuration + config: AttrDict + Attribute-dictionary of all configuration variables required for the Jedi class Returns ---------- None """ - # For provenance, save incoming task_config as a private attribute of JEDI object - self._task_config = task_config - - _exe_name = os.path.basename(task_config.JEDIEXE) - - self.exe = os.path.join(task_config.DATA, _exe_name) - if yaml_name: - self.yaml = os.path.join(task_config.DATA, yaml_name + '.yaml') - else: - self.yaml = os.path.join(task_config.DATA, os.path.splitext(_exe_name)[0] + '.yaml') - self.config = AttrDict() - self.j2tmpl_dir = os.path.join(task_config.PARMgfs, 'gdas') + # Make sure input dictionary for Jedi class constructor has the required keys + if 'yaml_name' not in config: + logger.error(f"FATAL ERROR: Key 'yaml_name' not found in config") + raise KeyError(f"FATAL ERROR: Key 'yaml_name' not found in config") + for key in required_jedi_keys: + if key not in config: + logger.error(f"FATAL ERROR: Required key '{key}' not found in config") + raise KeyError(f"FATAL ERROR: Required key '{key}' not found in config") + + # Create the configuration dictionary for JEDI object + local_dict = AttrDict( + { + 'exe': os.path.join(config.rundir, os.path.basename(config.exe_src)), + 'yaml': os.path.join(config.rundir, config.yaml_name + '.yaml'), + 'input_config': None + } + ) + self.jedi_config = AttrDict(**config, **local_dict) + + # Set optional keys in jedi_config to None if not already present + for key in optional_jedi_keys: + if key not in self.jedi_config: + self.jedi_config[key] = None + + # Save a copy of jedi_config + self._jedi_config = self.jedi_config.deepcopy() @logit(logger) - def set_config(self, task_config: AttrDict, algorithm: Optional[str] = None) -> AttrDict: - """Compile a JEDI configuration dictionary from a template file and save to a YAML file + def initialize(self, task_config: AttrDict) -> None: + """Initialize JEDI application + + This method will initialize a JEDI application. + This includes: + - generating JEDI input YAML config + - saving JEDI input YAML config to run directory + - linking the JEDI executable to run directory Parameters ---------- - task_config : AttrDict - Dictionary of all configuration variables associated with a GDAS task. - algorithm (optional) : str - Name of the algorithm used to generate the JEDI configuration dictionary. - It will override the algorithm set in the task_config.JCB_<>_YAML file. + task_config: AttrDict + Attribute-dictionary of all configuration variables associated with a GDAS task. Returns ---------- None """ - if 'JCB_BASE_YAML' in task_config.keys(): - # Step 1: Fill templates of the JCB base YAML file - jcb_config = parse_j2yaml(task_config.JCB_BASE_YAML, task_config) - - # Step 2: If algorithm is present then override the algorithm in the JEDI - # config. Otherwise, if the algorithm J2-YAML is present, fill - # its templates and merge. - if algorithm: - jcb_config['algorithm'] = algorithm - elif 'JCB_ALGO' in task_config.keys(): - jcb_config['algorithm'] = task_config.JCB_ALGO - elif 'JCB_ALGO_YAML' in task_config.keys(): - jcb_algo_config = parse_j2yaml(task_config.JCB_ALGO_YAML, task_config) - jcb_config.update(jcb_algo_config) - - # Step 3: Generate the JEDI YAML using JCB - self.config = render(jcb_config) - elif 'JEDIYAML' in task_config.keys(): - # Generate JEDI YAML without using JCB - self.config = parse_j2yaml(task_config.JEDIYAML, task_config, - searchpath=self.j2tmpl_dir) - else: - logger.exception(f"FATAL ERROR: Unable to compile JEDI configuration dictionary, ABORT!") - raise KeyError(f"FATAL ERROR: Task config must contain JCB_BASE_YAML or JEDIYAML") + # Render JEDI config dictionary + logger.info(f"Generating JEDI YAML config: {self.jedi_config.yaml}") + self.jedi_config.input_config = self.render_jcb(task_config) + logger.debug(f"JEDI config:\n{pformat(self.jedi_config.input_config)}") + + # Save JEDI config dictionary to YAML in run directory + logger.debug(f"Writing JEDI YAML config to: {self.jedi_config.yaml}") + save_as_yaml(self.jedi_config.input_config, self.jedi_config.yaml) + + # Link JEDI executable to run directory + logger.info(f"Linking JEDI executable {self.jedi_config.exe_src} to {self.jedi_config.exe}") + self.link_exe() @logit(logger) - def execute(self, task_config: AttrDict, aprun_cmd: str, jedi_args: Optional[List] = None) -> None: + def execute(self) -> None: """Execute JEDI application Parameters ---------- - task_config: AttrDict - Attribute-dictionary of all configuration variables associated with a GDAS task. - aprun_cmd: str - String comprising the run command for the JEDI executable. - jedi_args (optional): List - List of strings comprising optional input arguments for the JEDI executable. + None Returns ---------- - jedi_config: AttrDict - Attribute-dictionary of JEDI configuration rendered from a template. + None """ - chdir(task_config.DATA) + chdir(self.jedi_config.rundir) - exec_cmd = Executable(aprun_cmd) - exec_cmd.add_default_arg(self.exe) - if jedi_args: - for arg in jedi_args: + exec_cmd = Executable(self.jedi_config.mpi_cmd) + exec_cmd.add_default_arg(self.jedi_config.exe) + if self.jedi_config.jedi_args is not None: + for arg in self.jedi_config.jedi_args: exec_cmd.add_default_arg(arg) - exec_cmd.add_default_arg(self.yaml) + exec_cmd.add_default_arg(self.jedi_config.yaml) + logger.info(f"Executing {exec_cmd}") try: exec_cmd() except OSError: + logger.error(f"FATAL ERROR: Failed to execute {exec_cmd}") raise OSError(f"FATAL ERROR: Failed to execute {exec_cmd}") except Exception: + logger.error(f"FATAL ERROR: An error occurred during execution of {exec_cmd}") raise WorkflowException(f"FATAL ERROR: An error occurred during execution of {exec_cmd}") - @staticmethod @logit(logger) - def link_exe(task_config: AttrDict) -> None: + def render_jcb(self, task_config: AttrDict, algorithm: Optional[str] = None) -> AttrDict: + """Compile a JEDI configuration dictionary from a template file and save to a YAML file + + Parameters + ---------- + task_config : AttrDict + Dictionary of all configuration variables associated with a GDAS task. + algorithm (optional) : str + Name of the algorithm used to generate the JEDI configuration dictionary. + It will override the algorithm set in the jedi_config.jcb_algo_yaml file. + + Returns + ---------- + jedi_input_config: AttrDict + Attribute-dictionary of JEDI configuration rendered from a template. + """ + + # Fill JCB base YAML template and build JCB config dictionary + if self.jedi_config.jcb_base_yaml is not None: + jcb_config = parse_j2yaml(self.jedi_config.jcb_base_yaml, task_config) + else: + logger.error(f"FATAL ERROR: JCB base YAML must be specified in order to render YAML using JCB") + raise KeyError(f"FATAL ERROR: JCB base YAML must be specified in order to render YAML using JCB") + + # Add JCB algorithm YAML, if it exists, to JCB config dictionary + if self.jedi_config.jcb_algo_yaml is not None: + jcb_config.update(parse_j2yaml(self.jedi_config.jcb_algo_yaml, task_config)) + + # Set algorithm in JCB config dictionary + if algorithm is not None: + jcb_config['algorithm'] = algorithm + elif self.jedi_config.jcb_algo is not None: + jcb_config['algorithm'] = self.jedi_config.jcb_algo + elif 'algorithm' in jcb_config: + pass + else: + logger.error(f"FATAL ERROR: JCB algorithm must be specified as input to jedi.render_jcb(), " + + "in JEDI configuration dictionary as jcb_algo, or in JCB algorithm YAML") + raise Exception(f"FATAL ERROR: JCB algorithm must be specified as input to jedi.render_jcb(), " + + "in JEDI configuration dictionary as jcb_algo, or in JCB algorithm YAML") + + # Generate JEDI YAML config by rendering JCB config dictionary + jedi_input_config = render(jcb_config) + + return jedi_input_config + + @logit(logger) + def link_exe(self) -> None: """Link JEDI executable to run directory Parameters ---------- - task_config: AttrDict - Attribute-dictionary of all configuration variables associated with a GDAS task. + None Returns ---------- @@ -152,185 +197,156 @@ def link_exe(task_config: AttrDict) -> None: # TODO: linking is not permitted per EE2. # Needs work in JEDI to be able to copy the exec. [NOAA-EMC/GDASApp#1254] logger.warn("Linking is not permitted per EE2.") - exe_dest = os.path.join(task_config.DATA, os.path.basename(task_config.JEDIEXE)) - if os.path.exists(exe_dest): - rm_p(exe_dest) - os.symlink(task_config.JEDIEXE, exe_dest) + if not os.path.exists(self.jedi_config.exe): + os.symlink(self.jedi_config.exe_src, self.jedi_config.exe) + @staticmethod @logit(logger) - def get_obs_dict(self, task_config: AttrDict) -> Dict[str, Any]: - """Compile a dictionary of observation files to copy - - This method extracts 'observers' from the JEDI yaml and from that list, extracts a list of - observation files that are to be copied to the run directory - from the observation input directory + def get_jedi_dict(jedi_config_yaml: str, task_config: AttrDict, expected_block_names: Optional[list] = None): + """Get dictionary of Jedi objects from YAML specifying their configuration dictionaries Parameters ---------- - task_config: AttrDict - Attribute-dictionary of all configuration variables associated with a GDAS task. + jedi_config_yaml : str + path to YAML specifying configuration dictionaries for Jedi objects + task_config : str + attribute-dictionary of all configuration variables associated with a GDAS task + expected_block_names (optional) : str + list of names of blocks expected to be in jedi_config_yaml YAML file Returns ---------- - obs_dict: Dict - a dictionary containing the list of observation files to copy for FileHandler + None """ - observations = find_value_in_nested_dict(self.config, 'observations') + # Initialize dictionary of Jedi objects + jedi_dict = AttrDict() - copylist = [] - for ob in observations['observers']: - obfile = ob['obs space']['obsdatain']['engine']['obsfile'] - basename = os.path.basename(obfile) - copylist.append([os.path.join(task_config.COM_OBS, basename), obfile]) - obs_dict = { - 'mkdir': [os.path.join(task_config.DATA, 'obs')], - 'copy': copylist - } - return obs_dict + # Parse J2-YAML file for dictionary of JEDI configuration dictionaries + jedi_config_dict = parse_j2yaml(jedi_config_yaml, task_config) - @logit(logger) - def get_bias_dict(self, task_config: AttrDict, bias_file) -> Dict[str, Any]: - """Compile a dictionary of observation files to copy + # Loop through dictionary of Jedi configuration dictionaries + for block_name in jedi_config_dict: + # yaml_name key is set to name for this block + jedi_config_dict[block_name]['yaml_name'] = block_name + + # Make sure all required keys present + for key in required_jedi_keys: + if key not in jedi_config_dict[block_name]: + logger.error(f"FATAL ERROR: Required key {key} not found in {jedi_config_yaml} for block {block_name}.") + raise KeyError(f"FATAL ERROR: Required key {key} not found in {jedi_config_yaml} for block {block_name}.") + + # Set optional keys to None + for key in optional_jedi_keys: + if key not in jedi_config_dict[block_name]: + jedi_config_dict[block_name][key] = None + + # Construct JEDI object + jedi_dict[block_name] = Jedi(jedi_config_dict[block_name]) - This method extracts 'observers' from the JEDI yaml and determines from that list - if bias correction tar files are to be copied to the run directory - from the component directory. + # Make sure jedi_dict has the blocks we expect + if expected_block_names: + for block_name in expected_block_names: + if block_name not in jedi_dict: + logger.error(f"FATAL ERROR: Expected block {block_name} not present {jedi_config_yaml}") + raise Exception(f"FATAL ERROR: Expected block {block_name} not present {jedi_config_yaml}") + if len(jedi_dict) > len(expected_block_names): + logger.error(f"FATAL ERROR: {jedi_config_yaml} specifies more Jedi objects than expected.") + raise Exception(f"FATAL ERROR: {jedi_config_yaml} specifies more Jedi objects than expected.") + + # Return dictionary of JEDI objects + return jedi_dict + + @staticmethod + @logit(logger) + def remove_redundant(input_list: List) -> List: + """Remove reduncancies from list with possible redundant, non-mutable elements Parameters ---------- - task_config: AttrDict - Attribute-dictionary of all configuration variables associated with a GDAS task. - bias_file - name of bias correction tar file + input_list : List + List with possible redundant, non-mutable elements Returns ---------- - bias_dict: Dict - a dictionary containing the list of observation bias files to copy for FileHandler + output_list : List + Input list but with redundancies removed """ - observations = find_value_in_nested_dict(self.config, 'observations') - - copylist = [] - for ob in observations['observers']: - if 'obs bias' in ob.keys(): - obfile = ob['obs bias']['input file'] - obdir = os.path.dirname(obfile) - basename = os.path.basename(obfile) - prefix = '.'.join(basename.split('.')[:-3]) - bfile = f"{prefix}.{bias_file}" - tar_file = os.path.join(obdir, bfile) - copylist.append([os.path.join(task_config.VarBcDir, bfile), tar_file]) - break + output_list = [] + for item in input_list: + if item not in output_list: + output_list.append(item) - bias_dict = { - 'mkdir': [os.path.join(task_config.DATA, 'bc')], - 'copy': copylist - } - - return bias_dict + return output_list @staticmethod @logit(logger) - def extract_tar(tar_file: str) -> None: - """Extract files from a tarball + def extract_tar_from_filehandler_dict(filehandler_dict) -> None: + """Extract tarballs from FileHandler input dictionary - This method extract files from a tarball + This method extracts files from tarballs specified in a FileHander + input dictionary for the 'copy' action. Parameters ---------- - tar_file - path/name of tarball + filehandler_dict + Input dictionary for FileHandler Returns ---------- None """ - # extract files from tar file - tar_path = os.path.dirname(tar_file) - try: - with tarfile.open(tar_file, "r") as tarball: - tarball.extractall(path=tar_path) - logger.info(f"Extract {tarball.getnames()}") - except tarfile.ReadError as err: - if tarfile.is_tarfile(tar_file): - logger.error(f"FATAL ERROR: {tar_file} could not be read") - raise tarfile.ReadError(f"FATAL ERROR: unable to read {tar_file}") + for item in filehandler_dict['copy']: + # Use the filename from the destination entry if it's a file path + # Otherwise, it's a directory, so use the source entry filename + if os.path.isfile(item[1]): + filename = os.path.basename(item[1]) else: - logger.info() - except tarfile.ExtractError as err: - logger.exception(f"FATAL ERROR: unable to extract from {tar_file}") - raise tarfile.ExtractError("FATAL ERROR: unable to extract from {tar_file}") + filename = os.path.basename(item[0]) + + # Check if file is a tar ball + if os.path.splitext(filename)[1] == '.tar': + tar_file = f"{os.path.dirname(item[1])}/{filename}" + + # Extract tarball + logger.info(f"Extract files from {tar_file}") + extract_tar(tar_file) @logit(logger) -def find_value_in_nested_dict(nested_dict: Dict, target_key: str) -> Any: - """ - Recursively search through a nested dictionary and return the value for the target key. - This returns the first target key it finds. So if a key exists in a subsequent - nested dictionary, it will not be found. +def extract_tar(tar_file: str) -> None: + """Extract files from a tarball + + This method extract files from a tarball Parameters ---------- - nested_dict : Dict - Dictionary to search - target_key : str - Key to search for + tar_file + path/name of tarball Returns - ------- - Any - Value of the target key - - Raises - ------ - KeyError - If key is not found in dictionary - - TODO: if this gives issues due to landing on an incorrect key in the nested - dictionary, we will have to implement a more concrete method to search for a key - given a more complete address. See resolved conversations in PR 2387 - - # Example usage: - nested_dict = { - 'a': { - 'b': { - 'c': 1, - 'd': { - 'e': 2, - 'f': 3 - } - }, - 'g': 4 - }, - 'h': { - 'i': 5 - }, - 'j': { - 'k': 6 - } - } - - user_key = input("Enter the key to search for: ") - result = find_value_in_nested_dict(nested_dict, user_key) + ---------- + None """ - if not isinstance(nested_dict, dict): - raise TypeError(f"Input is not of type(dict)") - - result = nested_dict.get(target_key) - if result is not None: - return result - - for value in nested_dict.values(): - if isinstance(value, dict): - try: - result = find_value_in_nested_dict(value, target_key) - if result is not None: - return result - except KeyError: - pass - - raise KeyError(f"Key '{target_key}' not found in the nested dictionary") + # extract files from tar file + tar_path = os.path.dirname(tar_file) + try: + with tarfile.open(tar_file, "r") as tarball: + tarball.extractall(path=tar_path) + logger.info(f"Extract {tarball.getnames()}") + except tarfile.FileExistsError as err: + logger.exception(f"FATAL ERROR: {tar_file} does not exist") + raise tarfile.FileExistsError(f"FATAL ERROR: {tar_file} does not exist") + except tarfile.ReadError as err: + if tarfile.is_tarfile(tar_file): + logger.error(f"FATAL ERROR: tar archive {tar_file} could not be read") + raise tarfile.ReadError(f"FATAL ERROR: tar archive {tar_file} could not be read") + else: + logger.error(f"FATAL ERROR: {tar_file} is not a tar archive") + raise tarfile.ReadError(f"FATAL ERROR: {tar_file} is not a tar archive") + except tarfile.ExtractError as err: + logger.exception(f"FATAL ERROR: unable to extract from {tar_file}") + raise tarfile.ExtractError("FATAL ERROR: unable to extract from {tar_file}") diff --git a/ush/python/pygfs/task/atm_analysis.py b/ush/python/pygfs/task/atm_analysis.py index 5f67ea9d728..5c3aa0f7647 100644 --- a/ush/python/pygfs/task/atm_analysis.py +++ b/ush/python/pygfs/task/atm_analysis.py @@ -6,8 +6,7 @@ import tarfile from logging import getLogger from pprint import pformat -from typing import Optional, Dict, Any - +from typing import Any, Dict, List, Optional from wxflow import (AttrDict, FileHandler, add_to_datetime, to_fv3time, to_timedelta, to_YMDH, @@ -24,20 +23,18 @@ class AtmAnalysis(Task): Class for JEDI-based global atm analysis tasks """ @logit(logger, name="AtmAnalysis") - def __init__(self, config: Dict[str, Any], yaml_name: Optional[str] = None): + def __init__(self, config: Dict[str, Any]): """Constructor global atm analysis task This method will construct a global atm analysis task. This includes: - extending the task_config attribute AttrDict to include parameters required for this task - - instantiate the Jedi attribute object + - instantiate the Jedi attribute objects Parameters ---------- config: Dict dictionary object containing task configuration - yaml_name: str, optional - name of YAML file for JEDI configuration Returns ---------- @@ -73,46 +70,17 @@ def __init__(self, config: Dict[str, Any], yaml_name: Optional[str] = None): # Extend task_config with local_dict self.task_config = AttrDict(**self.task_config, **local_dict) - # Create JEDI object - self.jedi = Jedi(self.task_config, yaml_name) - - @logit(logger) - def initialize_jedi(self): - """Initialize JEDI application - - This method will initialize a JEDI application used in the global atm analysis. - This includes: - - generating and saving JEDI YAML config - - linking the JEDI executable - - Parameters - ---------- - None - - Returns - ---------- - None - """ - - # get JEDI-to-FV3 increment converter config and save to YAML file - logger.info(f"Generating JEDI YAML config: {self.jedi.yaml}") - self.jedi.set_config(self.task_config) - logger.debug(f"JEDI config:\n{pformat(self.jedi.config)}") - - # save JEDI config to YAML file - logger.debug(f"Writing JEDI YAML config to: {self.jedi.yaml}") - save_as_yaml(self.jedi.config, self.jedi.yaml) - - # link JEDI executable - logger.info(f"Linking JEDI executable {self.task_config.JEDIEXE} to {self.jedi.exe}") - self.jedi.link_exe(self.task_config) + # Create dictionary of Jedi objects + expected_keys = ['atmanlvar', 'atmanlfv3inc'] + self.jedi_dict = Jedi.get_jedi_dict(self.task_config.JEDI_CONFIG_YAML, self.task_config, expected_keys) @logit(logger) - def initialize_analysis(self) -> None: + def initialize(self) -> None: """Initialize a global atm analysis This method will initialize a global atm analysis. This includes: + - initialize JEDI applications - staging observation files - staging bias correction files - staging CRTM fix files @@ -129,26 +97,33 @@ def initialize_analysis(self) -> None: ---------- None """ - super().initialize() + + # initialize JEDI variational application + logger.info(f"Initializing JEDI variational DA application") + self.jedi_dict['atmanlvar'].initialize(self.task_config) + + # initialize JEDI FV3 increment conversion application + logger.info(f"Initializing JEDI FV3 increment conversion application") + self.jedi_dict['atmanlfv3inc'].initialize(self.task_config) # stage observations - logger.info(f"Staging list of observation files generated from JEDI config") - obs_dict = self.jedi.get_obs_dict(self.task_config) + logger.info(f"Staging list of observation files") + obs_dict = self.jedi_dict['atmanlvar'].render_jcb(self.task_config, 'atm_obs_staging') FileHandler(obs_dict).sync() logger.debug(f"Observation files:\n{pformat(obs_dict)}") # stage bias corrections - logger.info(f"Staging list of bias correction files generated from JEDI config") - self.task_config.VarBcDir = f"{self.task_config.COM_ATMOS_ANALYSIS_PREV}" - bias_file = f"rad_varbc_params.tar" - bias_dict = self.jedi.get_bias_dict(self.task_config, bias_file) - FileHandler(bias_dict).sync() - logger.debug(f"Bias correction files:\n{pformat(bias_dict)}") - - # extract bias corrections - tar_file = os.path.join(self.task_config.DATA, 'obs', f"{self.task_config.GPREFIX}{bias_file}") - logger.info(f"Extract bias correction files from {tar_file}") - self.jedi.extract_tar(tar_file) + logger.info(f"Staging list of bias correction files") + bias_dict = self.jedi_dict['atmanlvar'].render_jcb(self.task_config, 'atm_bias_staging') + if bias_dict['copy'] is None: + logger.info(f"No bias correction files to stage") + else: + bias_dict['copy'] = Jedi.remove_redundant(bias_dict['copy']) + FileHandler(bias_dict).sync() + logger.debug(f"Bias correction files:\n{pformat(bias_dict)}") + + # extract bias corrections + Jedi.extract_tar_from_filehandler_dict(bias_dict) # stage CRTM fix files logger.info(f"Staging CRTM fix files from {self.task_config.CRTM_FIX_YAML}") @@ -193,29 +168,20 @@ def initialize_analysis(self) -> None: FileHandler({'mkdir': newdirs}).sync() @logit(logger) - def execute(self, aprun_cmd: str, jedi_args: Optional[str] = None) -> None: - """Run JEDI executable - - This method will run JEDI executables for the global atm analysis + def execute(self, jedi_dict_key: str) -> None: + """Execute JEDI application of atm analysis Parameters ---------- - aprun_cmd : str - Run command for JEDI application on HPC system - jedi_args : List - List of additional optional arguments for JEDI application + jedi_dict_key + key specifying particular Jedi object in self.jedi_dict Returns ---------- None """ - if jedi_args: - logger.info(f"Executing {self.jedi.exe} {' '.join(jedi_args)} {self.jedi.yaml}") - else: - logger.info(f"Executing {self.jedi.exe} {self.jedi.yaml}") - - self.jedi.execute(self.task_config, aprun_cmd, jedi_args) + self.jedi_dict[jedi_dict_key].execute() @logit(logger) def finalize(self) -> None: diff --git a/ush/python/pygfs/task/atmens_analysis.py b/ush/python/pygfs/task/atmens_analysis.py index 4b2f8ebbf40..81cae238bbe 100644 --- a/ush/python/pygfs/task/atmens_analysis.py +++ b/ush/python/pygfs/task/atmens_analysis.py @@ -28,20 +28,18 @@ class AtmEnsAnalysis(Task): Class for JEDI-based global atmens analysis tasks """ @logit(logger, name="AtmEnsAnalysis") - def __init__(self, config: Dict[str, Any], yaml_name: Optional[str] = None): + def __init__(self, config: Dict[str, Any]): """Constructor global atmens analysis task This method will construct a global atmens analysis task. This includes: - extending the task_config attribute AttrDict to include parameters required for this task - - instantiate the Jedi attribute object + - instantiate the Jedi attribute objects Parameters ---------- config: Dict dictionary object containing task configuration - yaml_name: str, optional - name of YAML file for JEDI configuration Returns ---------- @@ -73,46 +71,17 @@ def __init__(self, config: Dict[str, Any], yaml_name: Optional[str] = None): # Extend task_config with local_dict self.task_config = AttrDict(**self.task_config, **local_dict) - # Create JEDI object - self.jedi = Jedi(self.task_config, yaml_name) + # Create dictionary of JEDI objects + expected_keys = ['atmensanlobs', 'atmensanlsol', 'atmensanlfv3inc', 'atmensanlletkf'] + self.jedi_dict = Jedi.get_jedi_dict(self.task_config.JEDI_CONFIG_YAML, self.task_config, expected_keys) @logit(logger) - def initialize_jedi(self): - """Initialize JEDI application - - This method will initialize a JEDI application used in the global atmens analysis. - This includes: - - generating and saving JEDI YAML config - - linking the JEDI executable - - Parameters - ---------- - None - - Returns - ---------- - None - """ - - # get JEDI config and save to YAML file - logger.info(f"Generating JEDI config: {self.jedi.yaml}") - self.jedi.set_config(self.task_config) - logger.debug(f"JEDI config:\n{pformat(self.jedi.config)}") - - # save JEDI config to YAML file - logger.info(f"Writing JEDI config to YAML file: {self.jedi.yaml}") - save_as_yaml(self.jedi.config, self.jedi.yaml) - - # link JEDI-to-FV3 increment converter executable - logger.info(f"Linking JEDI executable {self.task_config.JEDIEXE} to {self.jedi.exe}") - self.jedi.link_exe(self.task_config) - - @logit(logger) - def initialize_analysis(self) -> None: + def initialize(self) -> None: """Initialize a global atmens analysis This method will initialize a global atmens analysis. This includes: + - initialize JEDI applications - staging observation files - staging bias correction files - staging CRTM fix files @@ -128,26 +97,34 @@ def initialize_analysis(self) -> None: ---------- None """ - super().initialize() + + # initialize JEDI LETKF observer application + logger.info(f"Initializing JEDI LETKF observer application") + self.jedi_dict['atmensanlobs'].initialize(self.task_config) + + # initialize JEDI LETKF solver application + logger.info(f"Initializing JEDI LETKF solver application") + self.jedi_dict['atmensanlsol'].initialize(self.task_config) + + # initialize JEDI FV3 increment conversion application + logger.info(f"Initializing JEDI FV3 increment conversion application") + self.jedi_dict['atmensanlfv3inc'].initialize(self.task_config) # stage observations - logger.info(f"Staging list of observation files generated from JEDI config") - obs_dict = self.jedi.get_obs_dict(self.task_config) + logger.info(f"Staging list of observation files") + obs_dict = self.jedi_dict['atmensanlobs'].render_jcb(self.task_config, 'atm_obs_staging') FileHandler(obs_dict).sync() logger.debug(f"Observation files:\n{pformat(obs_dict)}") # stage bias corrections - logger.info(f"Staging list of bias correction files generated from JEDI config") - self.task_config.VarBcDir = f"{self.task_config.COM_ATMOS_ANALYSIS_PREV}" - bias_file = f"rad_varbc_params.tar" - bias_dict = self.jedi.get_bias_dict(self.task_config, bias_file) + logger.info(f"Staging list of bias correction files") + bias_dict = self.jedi_dict['atmensanlobs'].render_jcb(self.task_config, 'atm_bias_staging') + bias_dict['copy'] = Jedi.remove_redundant(bias_dict['copy']) FileHandler(bias_dict).sync() logger.debug(f"Bias correction files:\n{pformat(bias_dict)}") # extract bias corrections - tar_file = os.path.join(self.task_config.DATA, 'obs', f"{self.task_config.GPREFIX}{bias_file}") - logger.info(f"Extract bias correction files from {tar_file}") - self.jedi.extract_tar(tar_file) + Jedi.extract_tar_from_filehandler_dict(bias_dict) # stage CRTM fix files logger.info(f"Staging CRTM fix files from {self.task_config.CRTM_FIX_YAML}") @@ -176,28 +153,38 @@ def initialize_analysis(self) -> None: FileHandler({'mkdir': newdirs}).sync() @logit(logger) - def execute(self, aprun_cmd: str, jedi_args: Optional[str] = None) -> None: - """Run JEDI executable + def initialize_letkf(self) -> None: + """Initialize a global atmens analysis - This method will run JEDI executables for the global atmens analysis + Note: This would normally be done in AtmEnsAnalysis.initialize(), but that method + now initializes the split observer-solver. This method is just for testing. Parameters ---------- - aprun_cmd : str - Run command for JEDI application on HPC system - jedi_args : List - List of additional optional arguments for JEDI application + None + Returns ---------- None """ - if jedi_args: - logger.info(f"Executing {self.jedi.exe} {' '.join(jedi_args)} {self.jedi.yaml}") - else: - logger.info(f"Executing {self.jedi.exe} {self.jedi.yaml}") + self.jedi_dict['atmensanlletkf'].initialize(self.task_config) + + @logit(logger) + def execute(self, jedi_dict_key: str) -> None: + """Execute JEDI application of atmens analysis + + Parameters + ---------- + jedi_dict_key + key specifying a particular Jedi object in self.jedi_dict + + Returns + ---------- + None + """ - self.jedi.execute(self.task_config, aprun_cmd, jedi_args) + self.jedi_dict[jedi_dict_key].execute() @logit(logger) def finalize(self) -> None: diff --git a/ush/python/pygfs/task/marine_analysis.py b/ush/python/pygfs/task/marine_analysis.py index e7b7b5e9488..75cc28c7b30 100644 --- a/ush/python/pygfs/task/marine_analysis.py +++ b/ush/python/pygfs/task/marine_analysis.py @@ -15,7 +15,7 @@ from wxflow import (AttrDict, FileHandler, add_to_datetime, to_timedelta, to_YMD, - parse_j2yaml, + parse_j2yaml, parse_yaml, logit, Executable, Task, @@ -200,7 +200,7 @@ def _prep_variational_yaml(self: Task) -> None: # Add the things to the envconfig in order to template JCB files envconfig_jcb['PARMgfs'] = self.task_config.PARMgfs - envconfig_jcb['nmem_ens'] = self.task_config.NMEM_ENS + envconfig_jcb['NMEM_ENS'] = self.task_config.NMEM_ENS envconfig_jcb['berror_model'] = 'marine_background_error_static_diffusion' if self.task_config.NMEM_ENS > 3: envconfig_jcb['berror_model'] = 'marine_background_error_hybrid_diffusion_diffusion' @@ -234,6 +234,9 @@ def _prep_variational_yaml(self: Task) -> None: jcb_config['window_begin'] = self.task_config.MARINE_WINDOW_BEGIN.strftime('%Y-%m-%dT%H:%M:%SZ') jcb_config['window_middle'] = self.task_config.MARINE_WINDOW_MIDDLE.strftime('%Y-%m-%dT%H:%M:%SZ') + # Current hack so that this is not done directly in the JCB base yaml + jcb_config['marine_pseudo_model_states'] = parse_yaml('bkg_list.yaml') + # Render the full JEDI configuration file using JCB jedi_config = render(jcb_config) diff --git a/ush/python/pygfs/task/marine_bmat.py b/ush/python/pygfs/task/marine_bmat.py index a4a5b4f1447..a21699227b2 100644 --- a/ush/python/pygfs/task/marine_bmat.py +++ b/ush/python/pygfs/task/marine_bmat.py @@ -9,21 +9,40 @@ FileHandler, add_to_datetime, to_timedelta, chdir, - parse_j2yaml, + parse_j2yaml, save_as_yaml, logit, Executable, Task) +from pygfs.jedi import Jedi + logger = getLogger(__name__.split('.')[-1]) class MarineBMat(Task): """ - Class for global marine B-matrix tasks + Class for global marine B-matrix tasks. """ @logit(logger, name="MarineBMat") def __init__(self, config): + """Constructor for marine B-matrix task + + This method will construct the marine B-matrix task object + This includes: + - extending the task_config AttrDict to include parameters required for this task + - instantiate the Jedi attribute objects + + Parameters + ---------- + config: Dict + dictionary object containing task configuration + + Returns + ---------- + None + """ super().__init__(config) + _home_gdas = os.path.join(self.task_config.HOMEgfs, 'sorc', 'gdas.cd') _calc_scale_exec = os.path.join(self.task_config.HOMEgfs, 'ush', 'soca', 'calc_scales.py') _window_begin = add_to_datetime(self.task_config.current_cycle, @@ -38,18 +57,26 @@ def __init__(self, config): local_dict = AttrDict( { 'PARMsoca': os.path.join(self.task_config.PARMgfs, 'gdas', 'soca'), + 'CALC_SCALE_EXEC': _calc_scale_exec, 'MARINE_WINDOW_BEGIN': _window_begin, - 'MARINE_WINDOW_END': _window_end, 'MARINE_WINDOW_MIDDLE': self.task_config.current_cycle, + 'MARINE_WINDOW_END': _window_end, + 'MARINE_WINDOW_LENGTH': f"PT{self.task_config['assim_freq']}H", 'ENSPERT_RELPATH': _enspert_relpath, - 'CALC_SCALE_EXEC': _calc_scale_exec, - 'APREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z." + 'MOM6_LEVS': mdau.get_mom6_levels(str(self.task_config.OCNRES)), + 'APREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z.", + 'OPREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z." } ) # Extend task_config with local_dict self.task_config = AttrDict(**self.task_config, **local_dict) + # Create dictionary of Jedi objects + expected_keys = ['gridgen', 'soca_diagb', 'soca_parameters_diffusion_vt', 'soca_setcorscales', + 'soca_parameters_diffusion_hz', 'soca_ensb', 'soca_ensweights'] + self.jedi_dict = Jedi.get_jedi_dict(self.task_config.JEDI_CONFIG_YAML, self.task_config, expected_keys) + @logit(logger) def initialize(self: Task) -> None: """Initialize a global B-matrix @@ -60,10 +87,18 @@ def initialize(self: Task) -> None: - staging SOCA fix files - staging static ensemble members (optional) - staging ensemble members (optional) - - generating the YAML files for the JEDI and GDASApp executables + - initializing the soca_vtscales Python script + - initializing the JEDI applications - creating output directories + + Parameters + ---------- + None + + Returns + ---------- + None """ - super().initialize() # stage fix files logger.info(f"Staging SOCA fix files from {self.task_config.SOCA_INPUT_FIX_DIR}") @@ -78,54 +113,32 @@ def initialize(self: Task) -> None: bkg_list = parse_j2yaml(self.task_config.MARINE_DET_STAGE_BKG_YAML_TMPL, self.task_config) FileHandler(bkg_list).sync() - # stage the soca utility yamls (gridgen, fields and ufo mapping yamls) + # stage the soca utility yamls (fields and ufo mapping yamls) logger.info(f"Staging SOCA utility yaml files") soca_utility_list = parse_j2yaml(self.task_config.MARINE_UTILITY_YAML_TMPL, self.task_config) FileHandler(soca_utility_list).sync() - # generate the variance partitioning YAML file - logger.info(f"Generate variance partitioning YAML file from {self.task_config.BERROR_DIAGB_YAML}") - diagb_config = parse_j2yaml(path=self.task_config.BERROR_DIAGB_YAML, data=self.task_config) - diagb_config.save(os.path.join(self.task_config.DATA, 'soca_diagb.yaml')) - - # generate the vertical decorrelation scale YAML file - logger.info(f"Generate the vertical correlation scale YAML file from {self.task_config.BERROR_VTSCALES_YAML}") - vtscales_config = parse_j2yaml(path=self.task_config.BERROR_VTSCALES_YAML, data=self.task_config) - vtscales_config.save(os.path.join(self.task_config.DATA, 'soca_vtscales.yaml')) - - # generate vertical diffusion scale YAML file - logger.info(f"Generate vertical diffusion YAML file from {self.task_config.BERROR_DIFFV_YAML}") - diffvz_config = parse_j2yaml(path=self.task_config.BERROR_DIFFV_YAML, data=self.task_config) - diffvz_config.save(os.path.join(self.task_config.DATA, 'soca_parameters_diffusion_vt.yaml')) - - # generate the horizontal diffusion YAML files - if True: # TODO(G): skip this section once we have optimized the scales - # stage the correlation scale configuration - logger.info(f"Generate correlation scale YAML file from {self.task_config.BERROR_HZSCALES_YAML}") - FileHandler({'copy': [[self.task_config.BERROR_HZSCALES_YAML, - os.path.join(self.task_config.DATA, 'soca_setcorscales.yaml')]]}).sync() - - # generate horizontal diffusion scale YAML file - logger.info(f"Generate horizontal diffusion scale YAML file from {self.task_config.BERROR_DIFFH_YAML}") - diffhz_config = parse_j2yaml(path=self.task_config.BERROR_DIFFH_YAML, data=self.task_config) - diffhz_config.save(os.path.join(self.task_config.DATA, 'soca_parameters_diffusion_hz.yaml')) + # initialize vtscales python script + vtscales_config = self.jedi_dict['soca_parameters_diffusion_vt'].render_jcb(self.task_config, 'soca_vtscales') + save_as_yaml(vtscales_config, os.path.join(self.task_config.DATA, 'soca_vtscales.yaml')) + FileHandler({'copy': [[os.path.join(self.task_config.CALC_SCALE_EXEC), + os.path.join(self.task_config.DATA, 'calc_scales.x')]]}).sync() + + # initialize JEDI applications + self.jedi_dict['gridgen'].initialize(self.task_config) + self.jedi_dict['soca_diagb'].initialize(self.task_config) + self.jedi_dict['soca_parameters_diffusion_vt'].initialize(self.task_config) + self.jedi_dict['soca_setcorscales'].initialize(self.task_config) + self.jedi_dict['soca_parameters_diffusion_hz'].initialize(self.task_config) + if self.task_config.DOHYBVAR == "YES" or self.task_config.NMEM_ENS > 2: + self.jedi_dict['soca_ensb'].initialize(self.task_config) + self.jedi_dict['soca_ensweights'].initialize(self.task_config) - # hybrid EnVAR case + # stage ensemble members for the hybrid background error if self.task_config.DOHYBVAR == "YES" or self.task_config.NMEM_ENS > 2: - # stage ensemble membersfiles for use in hybrid background error logger.debug(f"Stage ensemble members for the hybrid background error") mdau.stage_ens_mem(self.task_config) - # generate ensemble recentering/rebalancing YAML file - logger.debug("Generate ensemble recentering YAML file") - ensrecenter_config = parse_j2yaml(path=self.task_config.BERROR_ENS_RECENTER_YAML, data=self.task_config) - ensrecenter_config.save(os.path.join(self.task_config.DATA, 'soca_ensb.yaml')) - - # generate ensemble weights YAML file - logger.debug("Generate hybrid-weigths YAML file") - hybridweights_config = parse_j2yaml(path=self.task_config.BERROR_HYB_WEIGHTS_YAML, data=self.task_config) - hybridweights_config.save(os.path.join(self.task_config.DATA, 'soca_ensweights.yaml')) - # create the symbolic link to the static B-matrix directory link_target = os.path.join(self.task_config.DATAstaticb) link_name = os.path.join(self.task_config.DATA, 'staticb') @@ -134,130 +147,44 @@ def initialize(self: Task) -> None: os.symlink(link_target, link_name) @logit(logger) - def gridgen(self: Task) -> None: - # link gdas_soca_gridgen.x - mdau.link_executable(self.task_config, 'gdas_soca_gridgen.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_soca_gridgen.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('gridgen.yaml') - - mdau.run(exec_cmd) + def execute(self) -> None: + """Generate the full B-matrix - @logit(logger) - def variance_partitioning(self: Task) -> None: - # link the variance partitioning executable, gdas_soca_diagb.x - mdau.link_executable(self.task_config, 'gdas_soca_diagb.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_soca_diagb.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_diagb.yaml') + This method will generate the full B-matrix according to the configuration. + This includes: + - running all JEDI application and Python scripts required to generate the B-matrix - mdau.run(exec_cmd) + Parameters + ---------- + None - @logit(logger) - def horizontal_diffusion(self: Task) -> None: - """Generate the horizontal diffusion coefficients + Returns + ---------- + None """ - # link the executable that computes the correlation scales, gdas_soca_setcorscales.x, - # and prepare the command to run it - mdau.link_executable(self.task_config, 'gdas_soca_setcorscales.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_soca_setcorscales.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_setcorscales.yaml') - # create a files containing the correlation scales - mdau.run(exec_cmd) + self.jedi_dict['gridgen'].execute() - # link the executable that computes the correlation scales, gdas_soca_error_covariance_toolbox.x, - # and prepare the command to run it - mdau.link_executable(self.task_config, 'gdas_soca_error_covariance_toolbox.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_soca_error_covariance_toolbox.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_parameters_diffusion_hz.yaml') + # variance partitioning + self.jedi_dict['soca_diagb'].execute() - # compute the coefficients of the diffusion operator - mdau.run(exec_cmd) + # horizontal diffusion + self.jedi_dict['soca_setcorscales'].execute() + self.jedi_dict['soca_parameters_diffusion_hz'].execute() - @logit(logger) - def vertical_diffusion(self: Task) -> None: - """Generate the vertical diffusion coefficients - """ - # compute the vertical correlation scales based on the MLD - FileHandler({'copy': [[os.path.join(self.task_config.CALC_SCALE_EXEC), - os.path.join(self.task_config.DATA, 'calc_scales.x')]]}).sync() + # vertical diffusion exec_cmd = Executable("python") exec_name = os.path.join(self.task_config.DATA, 'calc_scales.x') exec_cmd.add_default_arg(exec_name) exec_cmd.add_default_arg('soca_vtscales.yaml') mdau.run(exec_cmd) - # link the executable that computes the correlation scales, gdas_soca_error_covariance_toolbox.x, - # and prepare the command to run it - mdau.link_executable(self.task_config, 'gdas_soca_error_covariance_toolbox.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_soca_error_covariance_toolbox.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_parameters_diffusion_vt.yaml') - - # compute the coefficients of the diffusion operator - mdau.run(exec_cmd) - - @logit(logger) - def ensemble_perturbations(self: Task) -> None: - """Generate the 3D ensemble of perturbation for the 3DEnVAR - - This method will generate ensemble perturbations re-balanced w.r.t the - deterministic background. - This includes: - - computing a storing the unbalanced ensemble perturbations' statistics - - recentering the ensemble members around the deterministic background and - accounting for the nonlinear steric recentering - - saving the recentered ensemble statistics - """ - mdau.link_executable(self.task_config, 'gdas_ens_handler.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_ens_handler.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_ensb.yaml') + self.jedi_dict['soca_parameters_diffusion_vt'].execute() - # generate the ensemble perturbations - mdau.run(exec_cmd) - - @logit(logger) - def hybrid_weight(self: Task) -> None: - """Generate the hybrid weights for the 3DEnVAR - - This method will generate the 3D fields hybrid weights for the 3DEnVAR for each - variables. - TODO(G): Currently implemented for the specific case of the static ensemble members only - """ - mdau.link_executable(self.task_config, 'gdas_socahybridweights.x') - exec_cmd = Executable(self.task_config.APRUN_MARINEBMAT) - exec_name = os.path.join(self.task_config.DATA, 'gdas_socahybridweights.x') - exec_cmd.add_default_arg(exec_name) - exec_cmd.add_default_arg('soca_ensweights.yaml') - - # compute the ensemble weights - mdau.run(exec_cmd) - - @logit(logger) - def execute(self: Task) -> None: - """Generate the full B-matrix - - This method will generate the full B-matrix according to the configuration. - """ - chdir(self.task_config.DATA) - self.gridgen() # TODO: This should be optional in case the geometry file was staged - self.variance_partitioning() - self.horizontal_diffusion() # TODO: Make this optional once we've converged on an acceptable set of scales - self.vertical_diffusion() # hybrid EnVAR case if self.task_config.DOHYBVAR == "YES" or self.task_config.NMEM_ENS > 2: - self.ensemble_perturbations() # TODO: refactor this from the old scripts - self.hybrid_weight() # TODO: refactor this from the old scripts + self.jedi_dict['soca_ensb'].execute() + self.jedi_dict['soca_ensweights'].execute() @logit(logger) def finalize(self: Task) -> None: @@ -270,6 +197,13 @@ def finalize(self: Task) -> None: - keep the re-balanced ensemble perturbation files in DATAenspert - ... + Parameters + ---------- + None + + Returns + ---------- + None """ # Copy the soca grid if it was created grid_file = os.path.join(self.task_config.DATA, 'soca_gridspec.nc') diff --git a/ush/python/pygfs/task/stat_analysis.py b/ush/python/pygfs/task/stat_analysis.py new file mode 100644 index 00000000000..ab363f8e696 --- /dev/null +++ b/ush/python/pygfs/task/stat_analysis.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +import os +import glob +import gzip +import tarfile +import yaml +from logging import getLogger +from pprint import pformat +from typing import Optional, Dict, Any + +from wxflow import (AttrDict, + FileHandler, + add_to_datetime, to_fv3time, to_timedelta, to_YMDH, + Task, + parse_j2yaml, save_as_yaml, + logit) +from pygfs.jedi import Jedi + +logger = getLogger(__name__.split('.')[-1]) + + +class StatAnalysis(Task): + """ + Class for JEDI-based global stat analysis tasks + """ + @logit(logger, name="StatAnalysis") + def __init__(self, config: Dict[str, Any]): + """ + Constructor global stat analysis task + + This method will construct a global stat analysis task. + This includes: + - extending the task_config attribute AttrDict to include parameters required for this task + - instantiate the Jedi attribute objects + + Parameters + ---------- + config: Dict + dictionary object containing task configuration + + Returns + ---------- + None + """ + super().__init__(config) + + _res = int(self.task_config.CASE[1:]) + # _res_anl = int(self.task_config.CASE_ANL[1:]) + _window_begin = add_to_datetime(self.task_config.current_cycle, -to_timedelta(f"{self.task_config.assim_freq}H") / 2) + + # Create a local dictionary that is repeatedly used across this class + local_dict = AttrDict( + { + 'npx_ges': _res + 1, + 'npy_ges': _res + 1, + 'npz_ges': self.task_config.LEVS - 1, + 'npz': self.task_config.LEVS - 1, + 'npz_anl': self.task_config.LEVS - 1, + 'STAT_WINDOW_BEGIN': _window_begin, + 'STAT_WINDOW_LENGTH': f"PT{self.task_config.assim_freq}H", + 'OPREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z.", + 'APREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z.", + 'GPREFIX': f"gdas.t{self.task_config.previous_cycle.hour:02d}z." + } + ) + + # Extend task_config with local_dict + self.task_config = AttrDict(**self.task_config, **local_dict) + + # Create dictionary of Jedi objects + self.jedi_dict = Jedi.get_jedi_dict(self.task_config.JEDI_CONFIG_YAML, self.task_config) + + # # Create dictionary of JEDI objects + # self.jedi = AttrDict() + + # # statanlvar + # self.jedi['statanl'] = Jedi(AttrDict( + # { + # 'yaml_name': 'statanl', + # 'rundir': self.task_config.DATA, + # 'exe_src': self.task_config.JEDIEXE, + # 'jcb_base_yaml': self.task_config.JCB_BASE_YAML, + # 'jcb_algo': 'anlstat', + # 'jcb_algo_yaml': self.task_config.JCB_ALGO_YAML, + # 'jedi_args': None + # } + # )) + + @logit(logger) + def initialize(self) -> None: + """ + Initialize a global stat analysis + + This method will initialize a global stat analysis. + This includes: + - initialize JEDI applications + - copying stat files + + Parameters + ---------- + None + + Returns + ---------- + None + """ + logger.info(f"Copying files to {self.task_config.DATA}/stats") + + # Copy stat files to DATA path + aerostat = os.path.join(self.task_config.COM_CHEM_ANALYSIS, f"{self.task_config['APREFIX']}aerostat") + dest = os.path.join(self.task_config.DATA, "aerostats") + statlist = [[aerostat, dest]] + FileHandler({'copy': statlist}).sync() + + # Open tar file + logger.info(f"Open tarred stat file in {dest}") + with tarfile.open(dest, "r") as tar: + # Extract all files to the current directory + tar.extractall() + + # Gunzip .nc files + logger.info("Gunzip files from tar file") + gz_files = glob.glob(os.path.join(self.task_config.DATA, "*gz")) + + for diagfile in gz_files: + with gzip.open(diagfile, 'rb') as f_in: + with open(diagfile[:-3], 'wb') as f_out: + f_out.write(f_in.read()) + + # Get list of .nc4 files + obs_space_paths = glob.glob(os.path.join(self.task_config.DATA, "*.nc4")) + + self.task_config.OBSPACES_LIST = ['_'.join(os.path.basename(path).split('_')[1:3]) for path in obs_space_paths] + + # initialize JEDI application + logger.info(f"Initializing JEDI variational DA application") + self.jedi_dict['statanl'].initialize(self.task_config) diff --git a/versions/fix.ver b/versions/fix.ver index b1757911963..4739ce778ab 100644 --- a/versions/fix.ver +++ b/versions/fix.ver @@ -8,7 +8,7 @@ export cice_ver=20240416 export cpl_ver=20230526 export datm_ver=20220805 export gdas_crtm_ver=20220805 -export gdas_fv3jedi_ver=20241022 +export gdas_fv3jedi_ver=20241115 export gdas_soca_ver=20240802 export gdas_gsibec_ver=20240416 export gdas_obs_ver=20240213 diff --git a/workflow/applications/applications.py b/workflow/applications/applications.py index ecd320d708a..4ff5ca9aa5b 100644 --- a/workflow/applications/applications.py +++ b/workflow/applications/applications.py @@ -55,6 +55,7 @@ def __init__(self, conf: Configuration) -> None: self.do_verfozn = base.get('DO_VERFOZN', True) self.do_verfrad = base.get('DO_VERFRAD', True) self.do_vminmon = base.get('DO_VMINMON', True) + self.do_anlstat = base.get('DO_ANLSTAT', True) self.do_tracker = base.get('DO_TRACKER', True) self.do_genesis = base.get('DO_GENESIS', True) self.do_genesis_fsu = base.get('DO_GENESIS_FSU', False) diff --git a/workflow/applications/gfs_cycled.py b/workflow/applications/gfs_cycled.py index e85e8b159f6..2c4d33787e6 100644 --- a/workflow/applications/gfs_cycled.py +++ b/workflow/applications/gfs_cycled.py @@ -75,6 +75,9 @@ def _get_app_configs(self): if self.do_vminmon: configs += ['vminmon'] + if self.do_anlstat: + configs += ['anlstat'] + if self.do_tracker: configs += ['tracker'] @@ -212,6 +215,9 @@ def get_task_names(self): if self.do_vminmon: gdas_tasks += ['vminmon'] + if self.do_anlstat: + gdas_tasks += ['anlstat'] + if self.do_gempak: gdas_tasks += ['gempak', 'gempakmetancdc'] @@ -246,6 +252,9 @@ def get_task_names(self): if self.do_vminmon: gfs_tasks += ['vminmon'] + if self.do_anlstat: + gfs_tasks += ['anlstat'] + if self.do_tracker: gfs_tasks += ['tracker'] diff --git a/workflow/rocoto/gfs_tasks.py b/workflow/rocoto/gfs_tasks.py index 461241450ec..b3e24667680 100644 --- a/workflow/rocoto/gfs_tasks.py +++ b/workflow/rocoto/gfs_tasks.py @@ -1738,6 +1738,40 @@ def vminmon(self): return task + def anlstat(self): + deps = [] + if self.app_config.do_jediatmvar: + dep_dict = {'type': 'task', 'name': f'{self.run}atmanlfinal'} + deps.append(rocoto.add_dependency(dep_dict)) + if self.app_config.do_jediocnvar: + dep_dict = {'type': 'task', 'name': f'{self.run}ocnanalpost'} + deps.append(rocoto.add_dependency(dep_dict)) + if self.app_config.do_jedisnowda: + dep_dict = {'type': 'task', 'name': f'{self.run}snowanl'} + deps.append(rocoto.add_dependency(dep_dict)) + if self.app_config.do_aero: + dep_dict = {'type': 'task', 'name': f'{self.run}aeroanlfinal'} + deps.append(rocoto.add_dependency(dep_dict)) + + dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) + + resources = self.get_resource('anlstat') + task_name = f'{self.run}anlstat' + task_dict = {'task_name': task_name, + 'resources': resources, + 'dependency': dependencies, + 'envars': self.envars, + 'cycledef': self.run.replace('enkf', ''), + 'command': f'{self.HOMEgfs}/jobs/rocoto/anlstat.sh', + 'job_name': f'{self.pslot}_{task_name}_@H', + 'log': f'{self.rotdir}/logs/@Y@m@d@H/{task_name}.log', + 'maxtries': '&MAXTRIES;' + } + + task = rocoto.create_task(task_dict) + + return task + def tracker(self): deps = [] dep_dict = {'type': 'metatask', 'name': f'{self.run}_atmos_prod'} @@ -2245,6 +2279,9 @@ def arch(self): if self.app_config.do_vminmon: dep_dict = {'type': 'task', 'name': f'{self.run}_vminmon'} deps.append(rocoto.add_dependency(dep_dict)) + if self.app_config.do_anlstat: + dep_dict = {'type': 'task', 'name': f'{self.run}anlstat'} + deps.append(rocoto.add_dependency(dep_dict)) elif self.run in ['gdas']: dep_dict = {'type': 'task', 'name': f'{self.run}_atmanlprod'} deps.append(rocoto.add_dependency(dep_dict)) @@ -2260,6 +2297,9 @@ def arch(self): if self.app_config.do_vminmon: dep_dict = {'type': 'task', 'name': f'{self.run}_vminmon'} deps.append(rocoto.add_dependency(dep_dict)) + if self.app_config.do_anlstat: + dep_dict = {'type': 'task', 'name': f'{self.run}anlstat'} + deps.append(rocoto.add_dependency(dep_dict)) if self.run in ['gfs'] and self.app_config.do_tracker: dep_dict = {'type': 'task', 'name': f'{self.run}_tracker'} deps.append(rocoto.add_dependency(dep_dict)) diff --git a/workflow/rocoto/tasks.py b/workflow/rocoto/tasks.py index df56f907181..818d502157b 100644 --- a/workflow/rocoto/tasks.py +++ b/workflow/rocoto/tasks.py @@ -18,9 +18,10 @@ class Tasks: 'marineanlinit', 'marineanlletkf', 'marinebmat', 'marineanlvar', 'ocnanalecen', 'marineanlchkpt', 'marineanlfinal', 'ocnanalvrfy', 'earc', 'ecen', 'echgres', 'ediag', 'efcs', 'eobs', 'eomg', 'epos', 'esfc', 'eupd', - 'atmensanlinit', 'atmensanlobs', 'atmensanlsol', 'atmensanlletkf', 'atmensanlfv3inc', 'atmensanlfinal', - 'aeroanlinit', 'aeroanlvar', 'aeroanlfinal', 'aeroanlgenb', - 'snowanl', 'esnowrecen', + 'atmensanlinit', 'atmensanlletkf', 'atmensanlfv3inc', 'atmensanlfinal', + 'aeroanlinit', 'aeroanlrun', 'aeroanlfinal', + 'prepsnowobs', 'snowanl', + 'anlstat', 'fcst', 'atmanlupp', 'atmanlprod', 'atmupp', 'goesupp', 'atmos_prod', 'ocean_prod', 'ice_prod',