diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..5068b961f7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,213 @@ +# global-workflow is a collaborative space where contributions come from a variety of sources +# This file is to ensure that new development gets reviewed by the appropriate SME + +# global-workflow default owners (not a complete list) +@KateFriedman-NOAA +@WalterKolczynski-NOAA +@DavidHuber-NOAA + +# Specific directory owners +/ci/ @TerrenceMcGuinness-NOAA @WalterKolczynski-NOAA + +/ecf/ @lgannoaa + +/workflow/ @WalterKolczynski-NOAA @aerorahul @DavidHuber-NOAA + +# Specific file owners +# build scripts +sorc/build_*.sh @WalterKolczynski-NOAA @DavidHuber-NOAA @aerorahul @KateFriedman-NOAA +sorc/link_workflow.sh @WalterKolczynski-NOAA @DavidHuber-NOAA @aerorahul @KateFriedman-NOAA + +# jobs +jobs/JGDAS_AERO_ANALYSIS_GENERATE_BMATRIX @CoryMartin-NOAA +jobs/JGDAS_ATMOS_ANALYSIS_DIAG @RussTreadon-NOAA @CoryMartin-NOAA +jobs/JGDAS_ATMOS_CHGRES_FORENKF @RussTreadon-NOAA @CoryMartin-NOAA +jobs/JGDAS_ATMOS_GEMPAK @GwenChen-NOAA +jobs/JGDAS_ATMOS_GEMPAK_META_NCDC @GwenChen-NOAA +jobs/JGDAS_ATMOS_VERFOZN @EdwardSafford-NOAA +jobs/JGDAS_ATMOS_VERFRAD @EdwardSafford-NOAA +jobs/JGDAS_ENKF_* @RussTreadon-NOAA @CoryMartin-NOAA @CatherineThomas-NOAA +jobs/JGDAS_FIT2OBS @jack-woollen +jobs/JGDAS_GLOBAL_OCEAN_ANALYSIS_ECEN @guillaumevernieres +jobs/JGDAS_GLOBAL_OCEAN_ANALYSIS_VRFY @guillaumevernieres +jobs/JGFS_ATMOS_AWIPS_20KM_1P0DEG @GwenChen-NOAA +jobs/JGFS_ATMOS_CYCLONE_GENESIS @JiayiPeng-NOAA +jobs/JGFS_ATMOS_CYCLONE_TRACKER @JiayiPeng-NOAA +jobs/JGFS_ATMOS_FBWIND @GwenChen-NOAA +jobs/JGFS_ATMOS_FSU_GENESIS +jobs/JGFS_ATMOS_GEMPAK @GwenChen-NOAA +jobs/JGFS_ATMOS_GEMPAK_META @GwenChen-NOAA +jobs/JGFS_ATMOS_GEMPAK_NCDC_UPAPGIF @GwenChen-NOAA +jobs/JGFS_ATMOS_GEMPAK_PGRB2_SPEC @GwenChen-NOAA +jobs/JGFS_ATMOS_PGRB2_SPEC_NPOESS @WenMeng-NOAA +jobs/JGFS_ATMOS_POSTSND @BoCui-NOAA +jobs/JGFS_ATMOS_VERIFICATION +jobs/JGLOBAL_AERO_ANALYSIS_* @CoryMartin-NOAA +jobs/JGLOBAL_ARCHIVE @DavidHuber-NOAA +jobs/JGLOBAL_ATMENS_ANALYSIS_* @RussTreadon-NOAA @CoryMartin-NOAA @DavidNew-NOAA +jobs/JGLOBAL_ATMOS_ANALYSIS @RussTreadon-NOAA @CatherineThomas-NOAA +jobs/JGLOBAL_ATMOS_ANALYSIS_CALC @RussTreadon-NOAA @CatherineThomas-NOAA @CoryMartin-NOAA +jobs/JGLOBAL_ATMOS_EMCSFC_SFC_PREP @GeorgeGayno-NOAA +jobs/JGLOBAL_ATMOS_ENSSTAT +jobs/JGLOBAL_ATMOS_POST_MANAGER +jobs/JGLOBAL_ATMOS_PRODUCTS @WenMeng-NOAA +jobs/JGLOBAL_ATMOS_SFCANL @GeorgeGayno-NOAA +jobs/JGLOBAL_ATMOS_TROPCY_QC_RELOC +jobs/JGLOBAL_ATMOS_UPP @WenMeng-NOAA +jobs/JGLOBAL_ATMOS_VMINMON @EdwardSafford-NOAA +jobs/JGLOBAL_ATM_* @RussTreadon-NOAA @DavidNew-NOAA @CoryMartin-NOAA +jobs/JGLOBAL_CLEANUP @WalterKolczynski-NOAA @DavidHuber-NOAA @KateFriedman-NOAA +jobs/JGLOBAL_EXTRACTVARS @EricSinsky-NOAA +jobs/JGLOBAL_FORECAST @aerorahul +jobs/JGLOBAL_MARINE_* @guillaumevernieres @AndrewEichmann-NOAA +jobs/JGLOBAL_OCEANICE_PRODUCTS @GwenChen-NOAA +jobs/JGLOBAL_PREP_EMISSIONS @bbakernoaa +jobs/JGLOBAL_PREP_OBS_AERO @CoryMartin-NOAA +jobs/JGLOBAL_PREP_OCEAN_OBS @guillaumevernieres @AndrewEichmann-NOAA +jobs/JGLOBAL_*SNOW* @jiaruidong2017 +jobs/JGLOBAL_STAGE_IC @KateFriedman-NOAA +jobs/JGLOBAL_WAVE_* @JessicaMeixner-NOAA @sbanihash +jobs/rocoto/* @WalterKolczynski-NOAA @KateFriedman-NOAA @DavidHuber-NOAA + +# scripts +scripts/exgdas_aero_analysis_generate_bmatrix.py @CoryMartin-NOAA +scripts/exgdas_atmos_chgres_forenkf.sh @RussTreadon-NOAA @CoryMartin-NOAA +scripts/exgdas_atmos_gempak_gif_ncdc.sh @GwenChen-NOAA +scripts/exgdas_atmos_nawips.sh @GwenChen-NOAA +scripts/exgdas_atmos_verfozn.sh @EdwardSafford-NOAA +scripts/exgdas_atmos_verfrad.sh @EdwardSafford-NOAA +scripts/exgdas_enkf_earc.py @DavidHuber-NOAA +scripts/exgdas_enkf_ecen.sh @CoryMartin-NOAA @RussTreadon-NOAA @CatherineThomas-NOAA +scripts/exgdas_enkf_post.sh @CoryMartin-NOAA @RussTreadon-NOAA @CatherineThomas-NOAA +scripts/exgdas_enkf_select_obs.sh @CoryMartin-NOAA @RussTreadon-NOAA @CatherineThomas-NOAA +scripts/exgdas_enkf_sfc.sh @CoryMartin-NOAA @RussTreadon-NOAA @CatherineThomas-NOAA +scripts/exgdas_enkf_snow_recenter.py @jiaruidong2017 +scripts/exgdas_enkf_update.sh @CoryMartin-NOAA @RussTreadon-NOAA @CatherineThomas-NOAA +scripts/exgdas_global_marine_analysis_letkf.py @guillaumevernieres @AndrewEichmann-NOAA +scripts/exgfs_aero_init_aerosol.py @WalterKolczynski-NOAA +scripts/exgfs_atmos_awips_20km_1p0deg.sh @GwenChen-NOAA +scripts/exgfs_atmos_fbwind.sh @GwenChen-NOAA +scripts/exgfs_atmos_gempak_gif_ncdc_skew_t.sh @GwenChen-NOAA +scripts/exgfs_atmos_gempak_meta.sh @GwenChen-NOAA +scripts/exgfs_atmos_goes_nawips.sh @GwenChen-NOAA +scripts/exgfs_atmos_grib2_special_npoess.sh @WenMeng-NOAA +scripts/exgfs_atmos_nawips.sh @GwenChen-NOAA +scripts/exgfs_atmos_postsnd.sh @BoCui-NOAA +scripts/exgfs_pmgr.sh +scripts/exgfs_prdgen_manager.sh +scripts/exgfs_wave_* @JessicaMeixner-NOAA @sbanihash +scripts/exglobal_aero_analysis_* @CoryMartin-NOAA +scripts/exglobal_archive.py @DavidHuber-NOAA +scripts/exglobal_atm_analysis_* @RussTreadon-NOAA @DavidNew-NOAA +scripts/exglobal_atmens_analysis_* @RussTreadon-NOAA @DavidNew-NOAA +scripts/exglobal_atmos_analysis*.sh @RussTreadon-NOAA @CoryMartin-NOAA +scripts/exglobal_atmos_ensstat.sh @RussTreadon-NOAA +scripts/exglobal_atmos_pmgr.sh +scripts/exglobal_atmos_products.sh @WenMeng-NOAA +scripts/exglobal_atmos_sfcanl.sh @GeorgeGayno-NOAA +scripts/exglobal_atmos_tropcy_qc_reloc.sh +scripts/exglobal_atmos_upp.py @WenMeng-NOAA +scripts/exglobal_atmos_vminmon.sh @EdwardSafford-NOAA +scripts/exglobal_cleanup.sh @DavidHuber-NOAA +scripts/exglobal_diag.sh @RussTreadon-NOAA @CoryMartin-NOAA +scripts/exglobal_extractvars.sh @EricSinsky-NOAA +scripts/exglobal_forecast.py @aerorahul +scripts/exglobal_forecast.sh @aerorahul @WalterKolczynski-NOAA +scripts/exglobal_marine_analysis_* @guillaumevernieres @AndrewEichmann-NOAA +scripts/exglobal_marinebmat.py @guillaumevernieres @AndrewEichmann-NOAA +scripts/exglobal_oceanice_products.py @GwenChen-NOAA +scripts/exglobal_prep_emissions.py @bbakernoaa +scripts/exglobal_prep_obs_aero.py @CoryMartin-NOAA +scripts/exglobal_prep_snow_obs.py @jiaruidong2017 +scripts/exglobal_snow_analysis.py @jiaruidong2017 +scripts/exglobal_stage_ic.py @KateFriedman-NOAA + +# ush +WAM_XML_to_ASCII.pl +atmos_ensstat.sh +atmos_extractvars.sh @EricSinsky-NOAA +bash_utils.sh @WalterKolczynski-NOAA +calcanl_gfs.py @CoryMartin-NOAA +calcinc_gfs.py @CoryMartin-NOAA +compare_f90nml.py @WalterKolczynski-NOAA @aerorahul +detect_machine.sh @WalterKolczynski-NOAA +extractvars_tools.sh @EricSinsky-NOAA +file_utils.sh @WalterKolczynski-NOAA +forecast_det.sh @aerorahul @WalterKolczynski-NOAA +forecast_postdet.sh @aerorahul @WalterKolczynski-NOAA +forecast_predet.sh @aerorahul @WalterKolczynski-NOAA +fv3gfs_remap_weights.sh +gaussian_sfcanl.sh @GeorgeGayno-NOAA +getdump.sh @WalterKolczynski-NOAA @KateFriedman-NOAA +getges.sh @WalterKolczynski-NOAA @KateFriedman-NOAA +getgfsnctime @CoryMartin-NOAA +getncdimlen @CoryMartin-NOAA +gfs_bfr2gpk.sh @GwenChen-NOAA +gfs_bufr.sh @GwenChen-NOAA +gfs_bufr_netcdf.sh @GwenChen-NOAA +gfs_sndp.sh @BoCui-NOAA +gfs_truncate_enkf.sh @CoryMartin-NOAA +global_savefits.sh +gsi_utils.py @CoryMartin-NOAA +interp_atmos_master.sh @aerorahul @WenMeng-NOAA @WalterKolczynski-NOAA +interp_atmos_sflux.sh @aerorahul @WenMeng-NOAA @WalterKolczynski-NOAA +jjob_header.sh @WalterKolczynski-NOAA +link_crtm_fix.sh @WalterKolczynski-NOAA +load_fv3gfs_modules.sh @WalterKolczynski-NOAA @aerorahul +load_ufsda_modules.sh @WalterKolczynski-NOAA @aerorahul @CoryMartin-NOAA +load_ufswm_modules.sh @WalterKolczynski-NOAA @aerorahul @JessicaMeixner-NOAA +merge_fv3_aerosol_tile.py @WalterKolczynski-NOAA +minmon_xtrct_*.pl @EdwardSafford-NOAA +module-setup.sh @WalterKolczynski-NOAA @aerorahul +oceanice_nc2grib2.sh @GwenChen-NOAA +ocnice_extractvars.sh @EricSinsky-NOAA +ozn_xtrct.sh @EdwardSafford-NOAA +parse-storm-type.pl +parsing_model_configure_FV3.sh @WalterKolczynski-NOAA @aerorahul @junwang-noaa +parsing_namelists_CICE.sh @WalterKolczynski-NOAA @aerorahul @junwang-noaa @DeniseWorthen +parsing_namelists_FV3.sh @WalterKolczynski-NOAA @aerorahul @junwang-noaa +parsing_namelists_FV3_nest.sh @guoqing-noaa +parsing_namelists_MOM6.sh @WalterKolczynski-NOAA @aerorahul @junwang-noaa @jiandewang +parsing_namelists_WW3.sh @WalterKolczynski-NOAA @aerorahul @JessicaMeixner-NOAA @sbanihash +parsing_ufs_configure.sh @WalterKolczynski-NOAA @aerorahul @junwang-noaa +preamble.sh @WalterKolczynski-NOAA +product_functions.sh @WalterKolczynski-NOAA @aerorahul +radmon_*.sh @EdwardSafford-NOAA +rstprod.sh @WalterKolczynski-NOAA @DavidHuber-NOAA +run_mpmd.sh @WalterKolczynski-NOAA @aerorahul @DavidHuber-NOAA +syndat_getjtbul.sh @JiayiPeng-NOAA +syndat_qctropcy.sh @JiayiPeng-NOAA +tropcy_relocate.sh @JiayiPeng-NOAA +tropcy_relocate_extrkr.sh @JiayiPeng-NOAA +wave_*.sh @JessicaMeixner-NOAA @sbanihash + +# ush/python +ush/python/pygfs/jedi/__init__.py @aerorahul @DavidNew-NOAA +ush/python/pygfs/jedi/jedi.py @DavidNew-NOAA +ush/python/pygfs/task/__init__.py @aerorahul +ush/python/pygfs/task/aero_analysis.py @DavidNew-NOAA @CoryMartin-NOAA +ush/python/pygfs/task/aero_bmatrix.py @DavidNew-NOAA @CoryMartin-NOAA +ush/python/pygfs/task/aero_emissions.py @bbakernoaa +ush/python/pygfs/task/aero_prepobs.py @CoryMartin-NOAA +ush/python/pygfs/task/analysis.py @DavidNew-NOAA @RussTreadon-NOAA +ush/python/pygfs/task/archive.py @DavidHuber-NOAA +ush/python/pygfs/task/atm_analysis.py @DavidNew-NOAA @RussTreadon-NOAA +ush/python/pygfs/task/atmens_analysis.py @DavidNew-NOAA @RussTreadon-NOAA +ush/python/pygfs/task/bmatrix.py @DavidNew-NOAA +ush/python/pygfs/task/gfs_forecast.py @aerorahul +ush/python/pygfs/task/marine_analysis.py @guillaumevernieres @AndrewEichmann-NOAA +ush/python/pygfs/task/marine_bmat.py @guillaumevernieres @AndrewEichmann-NOAA +ush/python/pygfs/task/marine_letkf.py @guillaumevernieres @AndrewEichmann-NOAA +ush/python/pygfs/task/oceanice_products.py @aerorahul @GwenChen-NOAA +ush/python/pygfs/task/snow_analysis.py @jiaruidong2017 +ush/python/pygfs/task/snowens_analysis.py @jiaruidong2017 +ush/python/pygfs/task/stage_ic.py @KateFriedman-NOAA +ush/python/pygfs/task/upp.py @aerorahul @WenMeng-NOAA +ush/python/pygfs/ufswm/__init__.py @aerorahul +ush/python/pygfs/ufswm/gfs.py @aerorahul +ush/python/pygfs/ufswm/ufs.py @aerorahul +ush/python/pygfs/utils/__init__.py @aerorahul +ush/python/pygfs/utils/marine_da_utils.py @guillaumevernieres @AndrewEichmann-NOAA + +# Specific workflow scripts +workflow/generate_workflows.sh @DavidHuber-NOAA diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9a1d61eb30..9e9c9eccfe 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -72,4 +72,5 @@ Example: - [ ] My changes generate no new warnings - [ ] New and existing tests pass with my changes - [ ] This change is covered by an existing CI test or a new one has been added +- [ ] Any new scripts have been added to the .github/CODEOWNERS file with owners - [ ] I have made corresponding changes to the system documentation if necessary diff --git a/ci/cases/gfsv17/C384mx025_3DVarAOWCDA.yaml b/ci/cases/gfsv17/C384mx025_3DVarAOWCDA.yaml index d97c9567e9..99ba7c3661 100644 --- a/ci/cases/gfsv17/C384mx025_3DVarAOWCDA.yaml +++ b/ci/cases/gfsv17/C384mx025_3DVarAOWCDA.yaml @@ -8,7 +8,7 @@ arguments: resdetatmos: 384 resdetocean: 0.25 nens: 0 - gfs_cyc: 4 + interval: 6 start: cold comroot: {{ 'RUNTESTS' | getenv }}/COMROOT expdir: {{ 'RUNTESTS' | getenv }}/EXPDIR diff --git a/ci/cases/pr/C48_S2SWA_gefs.yaml b/ci/cases/pr/C48_S2SWA_gefs.yaml index 98f0fcfadb..f39031f1a1 100644 --- a/ci/cases/pr/C48_S2SWA_gefs.yaml +++ b/ci/cases/pr/C48_S2SWA_gefs.yaml @@ -9,7 +9,7 @@ arguments: resdetocean: 5.0 resensatmos: 48 nens: 2 - gfs_cyc: 1 + interval: 24 start: cold comroot: {{ 'RUNTESTS' | getenv }}/COMROOT expdir: {{ 'RUNTESTS' | getenv }}/EXPDIR diff --git a/ci/cases/pr/C48mx500_3DVarAOWCDA.yaml b/ci/cases/pr/C48mx500_3DVarAOWCDA.yaml index e1b76f0db8..2de5fea7ff 100644 --- a/ci/cases/pr/C48mx500_3DVarAOWCDA.yaml +++ b/ci/cases/pr/C48mx500_3DVarAOWCDA.yaml @@ -13,7 +13,7 @@ arguments: idate: 2021032412 edate: 2021032418 nens: 0 - gfs_cyc: 0 + interval: 0 start: warm yaml: {{ HOMEgfs }}/ci/cases/yamls/soca_gfs_defaults_ci.yaml diff --git a/ci/cases/pr/C96C48_hybatmDA.yaml b/ci/cases/pr/C96C48_hybatmDA.yaml index 7617e39217..b527903d69 100644 --- a/ci/cases/pr/C96C48_hybatmDA.yaml +++ b/ci/cases/pr/C96C48_hybatmDA.yaml @@ -14,6 +14,6 @@ arguments: idate: 2021122018 edate: 2021122106 nens: 2 - gfs_cyc: 1 + interval: 24 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/gfs_defaults_ci.yaml diff --git a/ci/cases/pr/C96C48_hybatmaerosnowDA.yaml b/ci/cases/pr/C96C48_hybatmaerosnowDA.yaml index 7387e55b24..be5ad32238 100644 --- a/ci/cases/pr/C96C48_hybatmaerosnowDA.yaml +++ b/ci/cases/pr/C96C48_hybatmaerosnowDA.yaml @@ -13,7 +13,7 @@ arguments: idate: 2021122012 edate: 2021122100 nens: 2 - gfs_cyc: 1 + interval: 24 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/atmaerosnowDA_defaults_ci.yaml diff --git a/ci/cases/pr/C96C48_ufs_hybatmDA.yaml b/ci/cases/pr/C96C48_ufs_hybatmDA.yaml index b1566d77a0..41a8baa725 100644 --- a/ci/cases/pr/C96C48_ufs_hybatmDA.yaml +++ b/ci/cases/pr/C96C48_ufs_hybatmDA.yaml @@ -13,7 +13,7 @@ arguments: idate: 2024022318 edate: 2024022406 nens: 2 - gfs_cyc: 1 + interval: 24 start: warm yaml: {{ HOMEgfs }}/ci/cases/yamls/ufs_hybatmDA_defaults.ci.yaml diff --git a/ci/cases/pr/C96_S2SWA_gefs_replay_ics.yaml b/ci/cases/pr/C96_S2SWA_gefs_replay_ics.yaml index 1475e81ea0..7118dde53f 100644 --- a/ci/cases/pr/C96_S2SWA_gefs_replay_ics.yaml +++ b/ci/cases/pr/C96_S2SWA_gefs_replay_ics.yaml @@ -9,7 +9,7 @@ arguments: resdetocean: 1.0 resensatmos: 96 nens: 2 - gfs_cyc: 1 + interval: 6 start: warm comroot: {{ 'RUNTESTS' | getenv }}/COMROOT expdir: {{ 'RUNTESTS' | getenv }}/EXPDIR @@ -17,3 +17,6 @@ arguments: edate: 2020110100 yaml: {{ HOMEgfs }}/ci/cases/yamls/gefs_replay_ci.yaml icsdir: {{ 'ICSDIR_ROOT' | getenv }}/C96mx100/20240610 + +skip_ci_on_hosts: + - wcoss2 diff --git a/ci/cases/pr/C96_atm3DVar.yaml b/ci/cases/pr/C96_atm3DVar.yaml index e9e6c2b31c..fc09beeacf 100644 --- a/ci/cases/pr/C96_atm3DVar.yaml +++ b/ci/cases/pr/C96_atm3DVar.yaml @@ -12,7 +12,7 @@ arguments: idate: 2021122018 edate: 2021122106 nens: 0 - gfs_cyc: 1 + interval: 24 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/gfs_defaults_ci.yaml diff --git a/ci/cases/pr/C96_atm3DVar_extended.yaml b/ci/cases/pr/C96_atm3DVar_extended.yaml index cdf69f04e0..8ab67a750e 100644 --- a/ci/cases/pr/C96_atm3DVar_extended.yaml +++ b/ci/cases/pr/C96_atm3DVar_extended.yaml @@ -12,7 +12,7 @@ arguments: idate: 2021122018 edate: 2021122118 nens: 0 - gfs_cyc: 4 + interval: 6 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/gfs_extended_ci.yaml diff --git a/ci/cases/weekly/C384C192_hybatmda.yaml b/ci/cases/weekly/C384C192_hybatmda.yaml index 131ada95d5..6053f73124 100644 --- a/ci/cases/weekly/C384C192_hybatmda.yaml +++ b/ci/cases/weekly/C384C192_hybatmda.yaml @@ -14,6 +14,6 @@ arguments: idate: 2023040118 edate: 2023040200 nens: 2 - gfs_cyc: 1 + interval: 24 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/gfs_defaults_ci.yaml diff --git a/ci/cases/weekly/C384_atm3DVar.yaml b/ci/cases/weekly/C384_atm3DVar.yaml index 40487f3b47..1a14059ab1 100644 --- a/ci/cases/weekly/C384_atm3DVar.yaml +++ b/ci/cases/weekly/C384_atm3DVar.yaml @@ -14,6 +14,6 @@ arguments: idate: 2023040118 edate: 2023040200 nens: 0 - gfs_cyc: 1 + interval: 24 start: cold yaml: {{ HOMEgfs }}/ci/cases/yamls/gfs_defaults_ci.yaml diff --git a/ci/scripts/utils/launch_java_agent.sh b/ci/scripts/utils/launch_java_agent.sh index 183e671b9d..eb78d3b1ef 100755 --- a/ci/scripts/utils/launch_java_agent.sh +++ b/ci/scripts/utils/launch_java_agent.sh @@ -106,7 +106,7 @@ export GH="${HOME}/bin/gh" [[ -f "${GH}" ]] || echo "gh is not installed in ${HOME}/bin" ${GH} --version -check_mark=$(gh auth status -t 2>&1 | grep "Token:" | awk '{print $1}') || true +check_mark=$("${GH}" auth status -t 2>&1 | grep "Token:" | awk '{print $1}') || true if [[ "${check_mark}" != "✓" ]]; then echo "gh not authenticating with emcbot token" exit 1 diff --git a/docs/source/clone.rst b/docs/source/clone.rst index c365f0ed0a..d3f81f2e47 100644 --- a/docs/source/clone.rst +++ b/docs/source/clone.rst @@ -9,7 +9,7 @@ Quick Instructions Quick clone/build/link instructions (more detailed instructions below). .. note:: - Here we are making the assumption that you are using the workflow to run an experiment and so are working from the authoritative repository. If you are using a development branch then follow the instructions in :doc:`development.rst`. Once you do that you can follow the instructions here with the only difference being the repository/fork you are cloning from. + Here we are making the assumption that you are using the workflow to run an experiment and so are working from the authoritative repository. If you are using a development branch then follow the instructions in :doc:`development`. Once you do that you can follow the instructions here with the only difference being the repository/fork you are cloning from. Clone the `global-workflow` and `cd` into the `sorc` directory: diff --git a/docs/source/development.rst b/docs/source/development.rst index 4739d2b602..fc07f3f55e 100644 --- a/docs/source/development.rst +++ b/docs/source/development.rst @@ -12,6 +12,7 @@ Code managers * Kate Friedman - @KateFriedman-NOAA / kate.friedman@noaa.gov * Walter Kolczynski - @WalterKolczynski-NOAA / walter.kolczynski@noaa.gov + * David Huber - @DavidHuber-NOAA / david.huber@noaa.gov .. _development: @@ -70,7 +71,29 @@ The following steps should be followed in order to make changes to the develop b Development Tools ================= -See the ``/test`` folder in global-workflow for available development and testing tools. +Two sets of testing are available for use by developers. The first is the capability to run continuous integration tests locally and the second are a set of comparison tools. + +--------------------------- +Continuous Integration (CI) +--------------------------- + +The global workflow comes fitted with a suite of system tests that run various types of workflow. These tests are commonly run for pull requests before they may be merged into the develop branch. At a minimum, developers are expected to run the CI test(s) that will be impacted by their changes on at least one platform. + +The commonly run tests are written in YAML format and can be found in the ``ci/cases/pr`` directory. The ``workflow/generate_workflows.sh`` tool is available to aid running these cases. See the help documentation by running ``./generate_workflows.sh -h``. The script has the capability to prepare the EXPDIR and COMROOT directories for a specified or implied suite of CI tests (see :doc:`setup` for details on these directories). The script also has options to automatically build and run all tests for a given system (i.e. GFS or GEFS and a placeholder for SFS). For instance, to build the workflow and run all of the GFS tests, one would execute + +:: + + cd workflow + ./generate_workflows.sh -A "your_hpc_account" -b -G -c /path/to/root/directory + +where: + + * ``-A`` is used to specify the HPC (slurm or PBS) account to use + * ``-b`` indicates that the workflow should be built fresh + * ``-G`` specifies that all of the GFS cases should be run (this also influences the build flags to use) + * ``-c`` tells the tool to append the rocotorun commands for each experiment to your crontab + +More details on how to use the tool are provided by running ``generate_workflows.sh -h``. ---------------- Comparison Tools diff --git a/docs/source/hpc.rst b/docs/source/hpc.rst index 643cffdef0..e83851b1a2 100644 --- a/docs/source/hpc.rst +++ b/docs/source/hpc.rst @@ -90,9 +90,9 @@ Optimizing the global workflow on S4 The S4 cluster is relatively small and so optimizations are recommended to improve cycled runtimes. Please contact Innocent Souopgui (innocent.souopgui@noaa.gov) if you are planning on running a cycled experiment on this system to obtain optimized configuration files. -======================================== +================================================== Stacksize on R&Ds (Hera, Orion, Hercules, Jet, S4) -======================================== +================================================== Some GFS components, like the UPP, need an unlimited stacksize. Add the following setting into your appropriate .*rc file to support these components: diff --git a/docs/source/init.rst b/docs/source/init.rst index aa71e4e294..bd1bc1d2ce 100644 --- a/docs/source/init.rst +++ b/docs/source/init.rst @@ -301,7 +301,7 @@ Operations/production output location on HPSS: /NCEPPROD/hpssprod/runhistory/rh | | | | | | | gfs.t. ``hh`` z.sfcanl.nc | | | +----------------+---------------------------------+-----------------------------------------------------------------------------+--------------------------------+ -| v16.2[3]+ ops | gfs.t. ``hh`` z.atmanl.nc | com_gfs_ ``gfs_ver`` _gfs. ``yyyymmdd`` _ ``hh`` .gfs_nca.tar | gfs. ``yyyymmdd`` /``hh``/atmos| +| v16.2[3]+ ops | gfs.t. ``hh`` z.atmanl.nc | com_gfs\_ ``gfs_ver`` _gfs. ``yyyymmdd`` _ ``hh`` .gfs_nca.tar | gfs. ``yyyymmdd`` /``hh``/atmos| | | | | | | | gfs.t. ``hh`` z.sfcanl.nc | | | +----------------+---------------------------------+-----------------------------------------------------------------------------+--------------------------------+ diff --git a/docs/source/jobs.rst b/docs/source/jobs.rst index 0e3700bf20..2cdecb01de 100644 --- a/docs/source/jobs.rst +++ b/docs/source/jobs.rst @@ -8,7 +8,7 @@ GFS Configuration The sequence of jobs that are run for an end-to-end (analysis+forecast+post processing+verification) GFS configuration using the Global Workflow is shown above. The system utilizes a collection of scripts that perform the tasks for each step. -For any cycle the system consists of two suites -- the "gdas" suite which provides the initial guess fields, and the "gfs" suite which creates the initial conditions and forecast of the system. As with the operational system, the gdas runs for each cycle (00, 06, 12, and 18 UTC), however, to save time and space in experiments, the gfs (right side of the diagram) is initially setup to run for only the 00 UTC cycle (See the "run GFS this cycle?" portion of the diagram). The option to run the GFS for all four cycles is available (see the ``gfs_cyc`` variable in configuration file). +For any cycle the system consists of two suites -- the "gdas" suite which provides the initial guess fields, and the "gfs" suite which creates the initial conditions and forecast of the system. An experimental run is different from operations in the following ways: diff --git a/docs/source/setup.rst b/docs/source/setup.rst index 1715899927..0916033cbd 100644 --- a/docs/source/setup.rst +++ b/docs/source/setup.rst @@ -1,3 +1,5 @@ +.. _experiment-setup: + ================ Experiment Setup ================ @@ -32,7 +34,7 @@ The following command examples include variables for reference but users should :: cd workflow - ./setup_expt.py gfs forecast-only --idate $IDATE --edate $EDATE [--app $APP] [--start $START] [--gfs_cyc $GFS_CYC] [--resdetatmos $RESDETATMOS] [--resdetocean $RESDETOCEAN] + ./setup_expt.py gfs forecast-only --idate $IDATE --edate $EDATE [--app $APP] [--start $START] [--interval $INTERVAL_GFS] [--resdetatmos $RESDETATMOS] [--resdetocean $RESDETOCEAN] [--pslot $PSLOT] [--configdir $CONFIGDIR] [--comroot $COMROOT] [--expdir $EXPDIR] where: @@ -51,12 +53,12 @@ where: * ``$START`` is the start type (warm or cold [default]) * ``$IDATE`` is the initial start date of your run (first cycle CDATE, YYYYMMDDCC) - * ``$EDATE`` is the ending date of your run (YYYYMMDDCC) and is the last cycle that will complete + * ``$EDATE`` is the ending date of your run (YYYYMMDDCC) and is the last cycle that will complete [default: $IDATE] * ``$PSLOT`` is the name of your experiment [default: test] * ``$CONFIGDIR`` is the path to the ``/config`` folder under the copy of the system you're using [default: $TOP_OF_CLONE/parm/config/] * ``$RESDETATMOS`` is the resolution of the atmosphere component of the system (i.e. 768 for C768) [default: 384] * ``$RESDETOCEAN`` is the resolution of the ocean component of the system (i.e. 0.25 for 1/4 degree) [default: 0.; determined based on atmosphere resolution] - * ``$GFS_CYC`` is the forecast frequency (0 = none, 1 = 00z only [default], 2 = 00z & 12z, 4 = all cycles) + * ``$INTERVAL_GFS`` is the forecast interval in hours [default: 6] * ``$COMROOT`` is the path to your experiment output directory. Your ``ROTDIR`` (rotating com directory) will be created using ``COMROOT`` and ``PSLOT``. [default: $HOME (but do not use default due to limited space in home directories normally, provide a path to a larger scratch space)] * ``$EXPDIR`` is the path to your experiment directory where your configs will be placed and where you will find your workflow monitoring files (i.e. rocoto database and xml file). DO NOT include PSLOT folder at end of path, it will be built for you. [default: $HOME] @@ -67,7 +69,7 @@ Atm-only: :: cd workflow - ./setup_expt.py gfs forecast-only --pslot test --idate 2020010100 --edate 2020010118 --resdetatmos 384 --gfs_cyc 4 --comroot /some_large_disk_area/Joe.Schmo/comroot --expdir /some_safe_disk_area/Joe.Schmo/expdir + ./setup_expt.py gfs forecast-only --pslot test --idate 2020010100 --edate 2020010118 --resdetatmos 384 --interval 6 --comroot /some_large_disk_area/Joe.Schmo/comroot --expdir /some_safe_disk_area/Joe.Schmo/expdir Coupled: @@ -144,7 +146,8 @@ The following command examples include variables for reference but users should :: cd workflow - ./setup_expt.py gfs cycled --idate $IDATE --edate $EDATE [--app $APP] [--start $START] [--gfs_cyc $GFS_CYC] + ./setup_expt.py gfs cycled --idate $IDATE --edate $EDATE [--app $APP] [--start $START] + [--interval $INTERVAL_GFS] [--sdate_gfs $SDATE_GFS] [--resdetatmos $RESDETATMOS] [--resdetocean $RESDETOCEAN] [--resensatmos $RESENSATMOS] [--nens $NENS] [--run $RUN] [--pslot $PSLOT] [--configdir $CONFIGDIR] [--comroot $COMROOT] [--expdir $EXPDIR] [--icsdir $ICSDIR] @@ -163,9 +166,10 @@ where: - S2SWA: atm-ocean-ice-wave-aerosols * ``$IDATE`` is the initial start date of your run (first cycle CDATE, YYYYMMDDCC) - * ``$EDATE`` is the ending date of your run (YYYYMMDDCC) and is the last cycle that will complete + * ``$EDATE`` is the ending date of your run (YYYYMMDDCC) and is the last cycle that will complete [default: $IDATE] * ``$START`` is the start type (warm or cold [default]) - * ``$GFS_CYC`` is the forecast frequency (0 = none, 1 = 00z only [default], 2 = 00z & 12z, 4 = all cycles) + * ``$INTERVAL_GFS`` is the forecast interval in hours [default: 6] + * ``$SDATE_GFS`` cycle to begin GFS forecast [default: $IDATE + 6] * ``$RESDETATMOS`` is the resolution of the atmosphere component of the deterministic forecast [default: 384] * ``$RESDETOCEAN`` is the resolution of the ocean component of the deterministic forecast [default: 0.; determined based on atmosphere resolution] * ``$RESENSATMOS`` is the resolution of the atmosphere component of the ensemble forecast [default: 192] @@ -177,14 +181,12 @@ where: * ``$EXPDIR`` is the path to your experiment directory where your configs will be placed and where you will find your workflow monitoring files (i.e. rocoto database and xml file). DO NOT include PSLOT folder at end of path, it will be built for you. [default: $HOME] * ``$ICSDIR`` is the path to the ICs for your run if generated separately. [default: None] -.. [#] More Coupled configurations in cycled mode are currently under development and not yet available - Example: :: cd workflow - ./setup_expt.py gfs cycled --pslot test --configdir /home/Joe.Schmo/git/global-workflow/parm/config --idate 2020010100 --edate 2020010118 --comroot /some_large_disk_area/Joe.Schmo/comroot --expdir /some_safe_disk_area/Joe.Schmo/expdir --resdetatmos 384 --resensatmos 192 --nens 80 --gfs_cyc 4 + ./setup_expt.py gfs cycled --pslot test --configdir /home/Joe.Schmo/git/global-workflow/parm/config --idate 2020010100 --edate 2020010118 --comroot /some_large_disk_area/Joe.Schmo/comroot --expdir /some_safe_disk_area/Joe.Schmo/expdir --resdetatmos 384 --resensatmos 192 --nens 80 --interval 6 Example ``setup_expt.py`` on Orion: diff --git a/docs/source/wave.rst b/docs/source/wave.rst index 7b4f7471b8..56aa34ce3b 100644 --- a/docs/source/wave.rst +++ b/docs/source/wave.rst @@ -79,7 +79,7 @@ While the creation of these files are generally considered out of scope of this * Instructions for creating mesh.${WAVEGRID}.nc can be found at https://ufs-weather-model.readthedocs.io/en/latest/InputsOutputs.html#ww3 * The ww3_gint.WHTGRIDINT.bin.${WAVEGRID} can be created by running the ww3_gint routine as desired and then saved. -Once the new fix files have been created, :ref:`open an issue to have the master fix file directory updated`. This is a separate step than the process to update the workflow below. +Once the new fix files have been created, `open an issue to have the master fix file directory updated `. This is a separate step than the process to update the workflow below. ******************************** Updating Config and Script Files @@ -87,8 +87,8 @@ Updating Config and Script Files You will need to update the following files: -* parm/config/*/config.ufs -* parm/config/*/config.wave +* parm/config/\*/config.ufs +* parm/config/\*/config.wave * scripts/exgfs_wave_post_gridded_sbs.sh You will need to add the following files: diff --git a/gempak/ush/gfs_meta_comp.sh b/gempak/ush/gfs_meta_comp.sh index 36d18d8659..38c15a60c7 100755 --- a/gempak/ush/gfs_meta_comp.sh +++ b/gempak/ush/gfs_meta_comp.sh @@ -24,7 +24,7 @@ device="nc | ${metaname}" export COMIN="gfs.multi" mkdir "${COMIN}" -for cycle in $(seq -f "%02g" -s ' ' 0 "${STEP_GFS}" "${cyc}"); do +for cycle in $(seq -f "%02g" -s ' ' 0 "${INTERVAL_GFS}" "${cyc}"); do YMD=${PDY} HH=${cycle} GRID="1p00" declare_from_tmpl gempak_dir:COM_ATMOS_GEMPAK_TMPL for file_in in "${gempak_dir}/gfs_1p00_${PDY}${cycle}f"*; do file_out="${COMIN}/$(basename "${file_in}")" diff --git a/gempak/ush/gfs_meta_mar_comp.sh b/gempak/ush/gfs_meta_mar_comp.sh index d25fc0dc9a..91f8a48876 100755 --- a/gempak/ush/gfs_meta_mar_comp.sh +++ b/gempak/ush/gfs_meta_mar_comp.sh @@ -15,7 +15,7 @@ cp "${HOMEgfs}/gempak/fix/datatype.tbl" datatype.tbl export COMIN="gfs.multi" mkdir -p "${COMIN}" -for cycle in $(seq -f "%02g" -s ' ' 0 "${STEP_GFS}" "${cyc}"); do +for cycle in $(seq -f "%02g" -s ' ' 0 "${INTERVAL_GFS}" "${cyc}"); do YMD=${PDY} HH=${cycle} GRID="1p00" declare_from_tmpl gempak_dir:COM_ATMOS_GEMPAK_TMPL for file_in in "${gempak_dir}/gfs_1p00_${PDY}${cycle}f"*; do file_out="${COMIN}/$(basename "${file_in}")" diff --git a/jobs/JGFS_ATMOS_VERIFICATION b/jobs/JGFS_ATMOS_VERIFICATION index 48133364e5..fde0d73b1e 100755 --- a/jobs/JGFS_ATMOS_VERIFICATION +++ b/jobs/JGFS_ATMOS_VERIFICATION @@ -16,12 +16,6 @@ source "${HOMEgfs}/ush/jjob_header.sh" -e "metp" -c "base metp" ## METPCASE : METplus verification use case (g2g1 | g2o1 | pcp1) ############################################################### -# TODO: This should not be permitted as DATAROOT is set at the job-card level. -# TODO: DATAROOT is being used as DATA in metp jobs. This should be rectified in metp. -# TODO: The temporary directory is DATA and is created at the top of the J-Job. -# TODO: remove this line -export DATAROOT=${DATA} - VDATE=$(date --utc +%Y%m%d%H -d "${PDY} ${cyc} - ${VRFYBACK_HRS} hours") export VDATE=${VDATE:0:8} diff --git a/parm/config/gefs/config.base b/parm/config/gefs/config.base index 6cf8488f91..05aabaa323 100644 --- a/parm/config/gefs/config.base +++ b/parm/config/gefs/config.base @@ -227,7 +227,8 @@ export FHOUT_OCN=3 export FHOUT_ICE=3 # GFS cycle info -export gfs_cyc=@gfs_cyc@ # 0: no GFS cycle, 1: 00Z only, 2: 00Z and 12Z only, 4: all 4 cycles. +export INTERVAL_GFS=@INTERVAL_GFS@ # Frequency of GFS forecast +export SDATE_GFS=@SDATE_GFS@ # set variables needed for use with REPLAY ICs export REPLAY_ICS=@REPLAY_ICS@ @@ -255,11 +256,6 @@ export FHOUT_WAV=3 export FHMAX_HF_WAV=120 export FHOUT_HF_WAV=1 export FHMAX_WAV=${FHMAX_GFS} -if (( gfs_cyc != 0 )); then - export STEP_GFS=$(( 24 / gfs_cyc )) -else - export STEP_GFS="0" -fi export ILPOST=1 # gempak output frequency up to F120 export FHMIN_ENKF=${FHMIN_GFS} diff --git a/parm/config/gfs/config.base b/parm/config/gfs/config.base index 7fa8245057..ccb05abe88 100644 --- a/parm/config/gfs/config.base +++ b/parm/config/gfs/config.base @@ -283,7 +283,8 @@ export FHOUT_ICE=3 export EUPD_CYC="@EUPD_CYC@" # GFS cycle info -export gfs_cyc=@gfs_cyc@ # 0: no GFS cycle, 1: 00Z only, 2: 00Z and 12Z only, 4: all 4 cycles. +export INTERVAL_GFS=@INTERVAL_GFS@ # Frequency of GFS forecast +export SDATE_GFS=@SDATE_GFS@ # GFS output and frequency export FHMIN_GFS=0 @@ -302,11 +303,7 @@ export FHMAX_HF_WAV=120 export FHOUT_HF_WAV=1 export FHMAX_WAV=${FHMAX:-9} export FHMAX_WAV_GFS=${FHMAX_GFS} -if (( gfs_cyc != 0 )); then - export STEP_GFS=$(( 24 / gfs_cyc )) -else - export STEP_GFS="0" -fi + # TODO: Change gempak to use standard out variables (#2348) export ILPOST=${FHOUT_HF_GFS} # gempak output frequency up to F120 if (( FHMAX_HF_GFS < 120 )); then diff --git a/parm/config/gfs/config.resources b/parm/config/gfs/config.resources index 26f6126773..79dbb487db 100644 --- a/parm/config/gfs/config.resources +++ b/parm/config/gfs/config.resources @@ -988,8 +988,8 @@ case ${step} in threads_per_task=1 walltime_gdas="03:00:00" walltime_gfs="06:00:00" - ntasks=4 - tasks_per_node=4 + ntasks=1 + tasks_per_node=1 export memory="80G" ;; diff --git a/parm/config/gfs/config.wave b/parm/config/gfs/config.wave index db4eb9f708..ea68508547 100644 --- a/parm/config/gfs/config.wave +++ b/parm/config/gfs/config.wave @@ -117,7 +117,7 @@ if [[ "${RUN}" == "gdas" ]]; then export WAVNCYC=4 export WAVHCYC=${assim_freq:-6} export FHMAX_WAV_CUR=48 # RTOFS forecasts only out to 8 days -elif [[ ${gfs_cyc} -ne 0 ]]; then +elif (( INTERVAL_GFS > 0 )); then export WAVHCYC=${assim_freq:-6} export FHMAX_WAV_CUR=192 # RTOFS forecasts only out to 8 days else diff --git a/scripts/exgfs_aero_init_aerosol.py b/scripts/exgfs_aero_init_aerosol.py index aed6b88647..bc4e495e42 100755 --- a/scripts/exgfs_aero_init_aerosol.py +++ b/scripts/exgfs_aero_init_aerosol.py @@ -11,13 +11,13 @@ --------- This script requires the following environment variables be set beforehand: -CDATE: Initial time in YYYYMMDDHH format -STEP_GFS: Forecast cadence (frequency) in hours -FHMAX_GFS: Forecast length in hours -RUN: Forecast phase (gfs or gdas). Currently always expected to be gfs. -ROTDIR: Rotating (COM) directory -USHgfs: Path to global-workflow `ush` directory -PARMgfs: Path to global-workflow `parm` directory +CDATE: Initial time in YYYYMMDDHH format +INTERVAL_GFS: Forecast cadence (frequency) in hours +FHMAX_GFS: Forecast length in hours +RUN: Forecast phase (gfs or gdas). Currently always expected to be gfs. +ROTDIR: Rotating (COM) directory +USHgfs: Path to global-workflow `ush` directory +PARMgfs: Path to global-workflow `parm` directory Additionally, the following data files are used: @@ -66,7 +66,7 @@ def main() -> None: # Read in environment variables and make sure they exist cdate = get_env_var("CDATE") - incr = int(get_env_var('STEP_GFS')) + incr = int(get_env_var('INTERVAL_GFS')) fcst_length = int(get_env_var('FHMAX_GFS')) run = get_env_var("RUN") rot_dir = get_env_var("ROTDIR") diff --git a/sorc/verif-global.fd b/sorc/verif-global.fd index e7e6bc4358..b2ee80cac7 160000 --- a/sorc/verif-global.fd +++ b/sorc/verif-global.fd @@ -1 +1 @@ -Subproject commit e7e6bc43584e0b8911819b8f875cc8ee747db76d +Subproject commit b2ee80cac7921a3016fa5a857cc58acfccc4baea diff --git a/workflow/applications/applications.py b/workflow/applications/applications.py index a694129e38..ecd320d708 100644 --- a/workflow/applications/applications.py +++ b/workflow/applications/applications.py @@ -68,6 +68,7 @@ def __init__(self, conf: Configuration) -> None: self.nens = base.get('NMEM_ENS', 0) self.fcst_segments = base.get('FCST_SEGMENTS', None) + self.interval_gfs = to_timedelta(f"{base.get('INTERVAL_GFS')}H") if not AppConfig.is_monotonic(self.fcst_segments): raise ValueError(f'Forecast segments do not increase monotonically: {",".join(self.fcst_segments)}') @@ -109,9 +110,6 @@ def _init_finalize(self, conf: Configuration): # Save base in the internal state since it is often needed base = self.configs['_no_run']['base'] - # Get more configuration options into the class attributes - self.gfs_cyc = base.get('gfs_cyc') - # Get task names for the application self.task_names = self.get_task_names() @@ -199,19 +197,6 @@ def get_task_names(self, run="_no_run") -> Dict[str, List[str]]: ''' pass - @staticmethod - def get_gfs_interval(gfs_cyc: int) -> timedelta: - """ - return interval in hours based on gfs_cyc - """ - - gfs_internal_map = {'1': '24H', '2': '12H', '4': '6H'} - - try: - return to_timedelta(gfs_internal_map[str(gfs_cyc)]) - except KeyError: - raise KeyError(f'Invalid gfs_cyc = {gfs_cyc}') - @staticmethod def is_monotonic(test_list: List, check_decreasing: bool = False) -> bool: """ diff --git a/workflow/applications/gefs.py b/workflow/applications/gefs.py index c15786a2e8..9d1d5c3dc4 100644 --- a/workflow/applications/gefs.py +++ b/workflow/applications/gefs.py @@ -42,7 +42,6 @@ def _get_app_configs(self): def _update_base(base_in): base_out = base_in.copy() - base_out['INTERVAL_GFS'] = AppConfig.get_gfs_interval(base_in['gfs_cyc']) base_out['RUN'] = 'gefs' return base_out diff --git a/workflow/applications/gfs_cycled.py b/workflow/applications/gfs_cycled.py index 19f4dd607b..da78166ede 100644 --- a/workflow/applications/gfs_cycled.py +++ b/workflow/applications/gfs_cycled.py @@ -128,7 +128,7 @@ def _get_app_configs(self): @staticmethod def _update_base(base_in): - return GFSCycledAppConfig.get_gfs_cyc_dates(base_in) + return base_in def get_task_names(self): """ @@ -297,7 +297,7 @@ def get_task_names(self): tasks['enkfgdas'] = enkfgdas_tasks # Add RUN=gfs tasks if running early cycle - if self.gfs_cyc > 0: + if self.interval_gfs > to_timedelta("0H"): tasks['gfs'] = gfs_tasks if self.do_hybvar and 'gfs' in self.eupd_runs: @@ -307,49 +307,3 @@ def get_task_names(self): tasks['enkfgfs'] = enkfgfs_tasks return tasks - - @staticmethod - def get_gfs_cyc_dates(base: Dict[str, Any]) -> Dict[str, Any]: - """ - Generate GFS dates from experiment dates and gfs_cyc choice - """ - - base_out = base.copy() - - sdate = base['SDATE'] - edate = base['EDATE'] - base_out['INTERVAL'] = to_timedelta(f"{base['assim_freq']}H") - - # Set GFS cycling dates - gfs_cyc = base['gfs_cyc'] - if gfs_cyc != 0: - interval_gfs = AppConfig.get_gfs_interval(gfs_cyc) - hrinc = 0 - hrdet = 0 - if gfs_cyc == 1: - hrinc = 24 - sdate.hour - hrdet = edate.hour - elif gfs_cyc == 2: - if sdate.hour in [0, 12]: - hrinc = 12 - elif sdate.hour in [6, 18]: - hrinc = 6 - if edate.hour in [6, 18]: - hrdet = 6 - elif gfs_cyc == 4: - hrinc = 6 - sdate_gfs = sdate + timedelta(hours=hrinc) - edate_gfs = edate - timedelta(hours=hrdet) - if sdate_gfs > edate: - print('W A R N I N G!') - print('Starting date for GFS cycles is after Ending date of experiment') - print(f'SDATE = {sdate.strftime("%Y%m%d%H")}, EDATE = {edate.strftime("%Y%m%d%H")}') - print(f'SDATE_GFS = {sdate_gfs.strftime("%Y%m%d%H")}, EDATE_GFS = {edate_gfs.strftime("%Y%m%d%H")}') - gfs_cyc = 0 - - base_out['gfs_cyc'] = gfs_cyc - base_out['SDATE_GFS'] = sdate_gfs - base_out['EDATE_GFS'] = edate_gfs - base_out['INTERVAL_GFS'] = interval_gfs - - return base_out diff --git a/workflow/applications/gfs_forecast_only.py b/workflow/applications/gfs_forecast_only.py index 93551ac0cc..fb1d2cdb8f 100644 --- a/workflow/applications/gfs_forecast_only.py +++ b/workflow/applications/gfs_forecast_only.py @@ -78,7 +78,7 @@ def _get_app_configs(self): def _update_base(base_in): base_out = base_in.copy() - base_out['INTERVAL_GFS'] = AppConfig.get_gfs_interval(base_in['gfs_cyc']) + base_out['RUN'] = 'gfs' return base_out diff --git a/workflow/generate_workflows.sh b/workflow/generate_workflows.sh new file mode 100755 index 0000000000..c4f89eef6e --- /dev/null +++ b/workflow/generate_workflows.sh @@ -0,0 +1,531 @@ +#!/usr/bin/env bash + +### +function _usage() { + cat << EOF + This script automates the experiment setup process for the global workflow. + Options are also available to update submodules, build the workflow (with + specific build flags), specicy which YAMLs and YAML directory to run, and + whether to automatically update your crontab. + + Usage: generate_workflows.sh [OPTIONS] /path/to/RUNTESTS + or + RUNTESTS=/path/to/RUNTESTS generate_workflows.sh [OPTIONS] + + -H Root directory of the global workflow. + If not specified, then the directory is assumed to be one parent + directory up from this script's residing directory. + + -b Run build_all.sh with default flags + (build the UFS, UPP, UFS_Utils, and GFS-utils only + + -B "build flags" + Run build_all.sh with the build specified flags. Refer to + build_all.sh -h for a list of valid flags. + NOTE: the list of build flags MUST be in quotes. + + -u Update submodules before building and/or generating experiments. + + -y "list of YAMLs to run" + If this option is not specified, the default case (C48_ATM) will be + run. This option is overidden by -G or -E (see below). + Example: -y "C48_ATM C48_S2SW C96C48_hybatmDA" + + -Y /path/to/directory/with/YAMLs + If this option is not specified, then the \${HOMEgfs}/ci/cases/pr + directory is used. + + -G Run all valid GFS cases in the specified YAML directory. + If -b is specified, then "-g -u" (build the GSI and GDASApp) + will be passed to build_all.sh unless -B is also specified. + Note that these builds are disabled on some systems, which + will result in a warning from build_all.sh. + + -E Run all valid GEFS cases in the specified YAML directory. + If -b is specified, then "-w" will be passed to build_all.sh + unless -B is also specified. + + -S (Not yet supported!) + Run all valid SFS cases in the specified YAML directory. + + NOTES: + - Only one of -G -E or -S may be specified + - Valid cases are determined by the experiment:system key as + well as the skip_ci_on_hosts list in each YAML. + + -A "HPC account name" Set the HPC account name. + If this is not set, the default in + \$HOMEgfs/ci/platform/config.\$machine + will be used. + + -c Append the chosen set of tests to your existing crontab + If this option is not chosen, the new entries that would have been + written to your crontab will be printed to stdout. + NOTES: + - This option is not supported on Gaea. Instead, the output will + need to be written to scrontab manually. + - For Orion/Hercules, this option will not work unless run on + the [orion|hercules]-login-1 head node. + + -e "your@email.com" Email address to place in the crontab. + If this option is not specified, then the existing email address in + the crontab will be preserved. + + -v Verbose mode. Prints output of all commands to stdout. + + -V Very verbose mode. Passes -v to all commands and prints to stdout. + + -d Debug mode. Same as -V but also enables logging (set -x). + + -h Display this message. +EOF +} + +set -eu + +# Set default options +HOMEgfs="" +_specified_home=false +_build=false +_build_flags="" +_explicit_build_flags=false +_update_submods=false +declare -a _yaml_list=("C48_ATM") +_specified_yaml_list=false +_yaml_dir="" # Will be set based off of HOMEgfs if not specified explicitly +_specified_yaml_dir=false +_run_all_gfs=false +_run_all_gefs=false +_run_all_sfs=false +_hpc_account="" +_set_account=false +_update_cron=false +_email="" +_set_email=false +_verbose=false +_very_verbose=false +_verbose_flag="--" +_debug="false" +_cwd=$(pwd) +_runtests="${RUNTESTS:-${_runtests:-}}" +_nonflag_option_count=0 + +while [[ $# -gt 0 && "$1" != "--" ]]; do + while getopts ":H:bB:uy:Y:GESA:ce:vVdh" option; do + case "${option}" in + H) + HOMEgfs="${OPTARG}" + _specified_home=true + if [[ ! -d "${HOMEgfs}" ]]; then + echo "Specified HOMEgfs directory (${HOMEgfs}) does not exist" + exit 1 + fi + ;; + b) _build=true ;; + B) _build_flags="${OPTARG}" && _explicit_build_flags=true ;; + u) _update_submods=true ;; + y) # Start over with an empty _yaml_list + declare -a _yaml_list=() + for _yaml in ${OPTARG}; do + # Strip .yaml from the end of each and append to _yaml_list + _yaml_list+=("${_yaml//.yaml/}") + done + _specified_yaml_list=true + ;; + Y) _yaml_dir="${OPTARG}" && _specified_yaml_dir=true ;; + G) _run_all_gfs=true ;; + E) _run_all_gefs=true ;; + S) _run_all_sfs=true ;; + c) _update_cron=true ;; + e) _email="${OPTARG}" && _set_email=true ;; + v) _verbose=true ;; + V) _very_verbose=true && _verbose=true && _verbose_flag="-v" ;; + d) _debug=true && _very_verbose=true && _verbose=true && _verbose_flag="-v" && PS4='${LINENO}: ' ;; + h) _usage && exit 0 ;; + :) + echo "[${BASH_SOURCE[0]}]: ${option} requires an argument" + _usage + ;; + *) + echo "[${BASH_SOURCE[0]}]: Unrecognized option: ${option}" + _usage + ;; + esac + done + + if [[ ${OPTIND:-0} -gt 0 ]]; then + shift $((OPTIND-1)) + fi + + while [[ $# -gt 0 && ! "$1" =~ ^- ]]; do + _runtests=${1} + (( _nonflag_option_count += 1 )) + if [[ ${_nonflag_option_count} -gt 1 ]]; then + echo "Too many arguments specified." + _usage + exit 2 + fi + shift + done +done + +function send_email() { + # Send an email to $_email. + # Only use this once we get to the long steps (building, etc) and on success. + _subject="${_subject:-generate_workflows.sh failure on ${machine}}" + _body="${1}" + + echo "${_body}" | mail -s "${_subject}" "${_email}" +} + +if [[ -z "${_runtests}" ]]; then + echo "Mising run directory (RUNTESTS) argument/environment variable." + sleep 2 + _usage + exit 3 +fi + +# Turn on logging if running in debug mode +if [[ "${_debug}" == "true" ]]; then + set -x +fi + +# Create the RUNTESTS directory +[[ "${_verbose}" == "true" ]] && printf "Creating RUNTESTS in %s\n\n" "${_runtests}" +if [[ ! -d "${_runtests}" ]]; then + set +e + if ! mkdir -p "${_runtests}" "${_verbose_flag}"; then + echo "Unable to create RUNTESTS directory: ${_runtests}" + echo "Rerun with -h for usage examples." + exit 4 + fi + set -e +else + echo "The RUNTESTS directory ${_runtests} already exists." + echo "Would you like to remove it?" + _attempts=0 + while read -r _from_stdin; do + if [[ "${_from_stdin^^}" =~ Y ]]; then + rm -rf "${_runtests}" + mkdir -p "${_runtests}" + break + elif [[ "${_from_stdin^^}" =~ N ]]; then + echo "Continuing without removing the directory" + break + else + (( _attempts+=1 )) + if [[ ${_attempts} == 3 ]]; then + echo "Exiting." + exit 99 + fi + echo "'${_from_stdin}' is not a valid choice. Please type Y or N" + fi + done +fi + +# Test if multiple "run_all" options were set +_count_run_alls=0 +[[ "${_run_all_gfs}" == "true" ]] && ((_count_run_alls+=1)) +[[ "${_run_all_gefs}" == "true" ]] && ((_count_run_alls+=1)) +[[ "${_run_all_sfs}" == "true" ]] && ((_count_run_alls+=1)) + +if (( _count_run_alls > 1 )) ; then + echo "Only one run all option (-G -E -S) may be specified" + echo "Rerun with just one option and/or with -h for usage examples" + exit 5 +fi + +# If -S is specified, exit (for now). +# TODO when SFS tests come online, enable this option. +if [[ "${_run_all_sfs}" == "true" ]]; then + echo "There are no known SFS tests at this time. Aborting." + echo "If you have prepared YAMLs for SFS cases, specify their" + echo "location and names without '-S', e.g." + echo "generate_workflows.sh -y \"C48_S2S_SFS\" -Y \"/path/to/yaml/directory\"" + exit 0 +fi + +# Set HOMEgfs if it wasn't set by the user +if [[ "${_specified_home}" == "false" ]]; then + script_relpath="$(dirname "${BASH_SOURCE[0]}")" + HOMEgfs="$(cd "${script_relpath}/.." && pwd)" + [[ "${_verbose}" == "true" ]] && printf "Setting HOMEgfs to %s\n\n" "${HOMEgfs}" +fi + +# Set the _yaml_dir to HOMEgfs/ci/cases/pr if not explicitly set +[[ "${_specified_yaml_dir}" == false ]] && _yaml_dir="${HOMEgfs}/ci/cases/pr" + +function select_all_yamls() +{ + # A helper function to select all of the YAMLs for a specified system (gfs, gefs, sfs) + + # This function is called if -G, -E, or -S are specified either with or without a + # specified YAML list. If a YAML list was specified, this function will remove any + # YAMLs in that list that are not for the specified system and issue warnings when + # doing so. + + _system="${1}" + _SYSTEM="${_system^^}" + + # Bash cannot return an array from a function and any edits are descoped at + # the end of the function, so use a nameref instead. + local -n _nameref_yaml_list='_yaml_list' + + if [[ "${_specified_yaml_list}" == false ]]; then + # Start over with an empty _yaml_list + _nameref_yaml_list=() + printf "Running all %s cases in %s\n\n" "${_SYSTEM}" "${_yaml_dir}" + _yaml_count=0 + + for _full_path in "${_yaml_dir}/"*.yaml; do + # Skip any YAML that isn't supported + if ! grep -l "system: *${_system}" "${_full_path}" >& /dev/null ; then continue; fi + + # Select only cases for the specified system + _yaml=$(basename "${_full_path}") + # Strip .yaml from the filename to get the case name + _yaml="${_yaml//.yaml/}" + _nameref_yaml_list+=("${_yaml}") + [[ "${_verbose}" == true ]] && echo "Found test ${_yaml//.yaml/}" + (( _yaml_count+=1 )) + done + + if [[ ${_yaml_count} -eq 0 ]]; then + read -r -d '' _message << EOM + "No YAMLs or ${_SYSTEM} were found in the directory (${_yaml_dir})!" + "Please check the directory/YAMLs and try again" +EOM + echo "${_message}" + if [[ "${_set_email}" == true ]]; then + send_email "${_message}" + fi + exit 6 + fi + else + # Check if the specified yamls are for the specified system + for i in "${!_nameref_yaml_list}"; do + _yaml="${_nameref_yaml_list[${i}]}" + _found=$(grep -l "system: *${system}" "${_yaml_dir}/${_yaml}.yaml") + if [[ -z "${_found}" ]]; then + echo "WARNING the yaml file ${_yaml_dir}/${_yaml}.yaml is not designed for the ${_SYSTEM} system" + echo "Removing this yaml from the set of cases to run" + unset '_nameref_yaml_list[${i}]' + # Sleep 2 seconds to give the user a moment to react + sleep 2s + fi + done + fi +} + +# Check if running all GEFS cases +if [[ "${_run_all_gefs}" == "true" ]]; then + # Append -w to build_all.sh flags if -E was specified + if [[ "${_explicit_build_flags}" == "false" && "${_build}" == "true" ]]; then + _build_flags="-w" + fi + + select_all_yamls "gefs" +fi + +# Check if running all SFS cases +if [[ "${_run_all_gfs}" == "true" ]]; then + # Append -g -u to build_all.sh flags if -G was specified + if [[ "${_explicit_build_flags}" == "false" && "${_build}" == "true" ]]; then + _build_flags="-g -u" + fi + + select_all_yamls "gfs" +fi + +# Loading modules sometimes raises unassigned errors, so disable checks +set +u +[[ "${_verbose}" == "true" ]] && printf "Loading modules\n\n" +[[ "${_debug}" == "true" ]] && set +x +if ! source "${HOMEgfs}/workflow/gw_setup.sh" >& stdout; then + cat stdout + echo "Failed to source ${HOMEgfs}/workflow/gw_setup.sh!" + exit 7 +fi +[[ "${_verbose}" == "true" ]] && cat stdout +rm -f stdout +[[ "${_debug}" == "true" ]] && set -x +set -u +machine=${MACHINE_ID} +. "${HOMEgfs}/ci/platforms/config.${machine}" + +# If _yaml_dir is not set, set it to $HOMEgfs/ci/cases/pr +if [[ -z ${_yaml_dir} ]]; then + _yaml_dir="${HOMEgfs}/ci/cases/pr" +fi + +# Update submodules if requested +if [[ "${_update_submods}" == "true" ]]; then + printf "Updating submodules\n\n" + _git_cmd="git submodule update --init --recursive -j 10" + if [[ "${_verbose}" == true ]]; then + ${_git_cmd} + else + if ! ${_git_cmd} 2> stderr 1> stdout; then + cat stdout stderr + read -r -d '' _message << EOM +The git command (${_git_cmd}) failed with a non-zero status +Messages from git: +EOM + _newline=$'\n' + _message="${_message}${_newline}$(cat stdout stderr)" + if [[ "${_set_email}" == true ]]; then + send_email "${_message}" + fi + echo "${_message}" + rm -f stdout stderr + exit 8 + fi + rm -f stdout stderr + fi +fi + +# Build the system if requested +if [[ "${_build}" == "true" ]]; then + printf "Building via build_all.sh %s\n\n" "${_build_flags}" + # Let the output of build_all.sh go to stdout regardless of verbose options + #shellcheck disable=SC2086,SC2248 + ${HOMEgfs}/sorc/build_all.sh ${_build_flags} ${_verbose_flag} +fi + +# Link the workflow silently unless there's an error +[[ "${_verbose}" == true ]] && printf "Linking the workflow\n\n" +if ! "${HOMEgfs}/sorc/link_workflow.sh" >& stdout; then + cat stdout + echo "link_workflow.sh failed!" + if [[ "${_set_email}" == true ]]; then + _stdout=$(cat stdout) + send_email "link_workflow.sh failed with the message"$'\n'"${_stdout}" + fi + rm -f stdout + exit 9 +fi +rm -f stdout + +# Configure the environment for running create_experiment.py +[[ "${_verbose}" == true ]] && printf "Setting up the environment to run create_experiment.py\n\n" +for i in "${!_yaml_list[@]}"; do + _yaml_file="${_yaml_dir}/${_yaml_list[${i}]}.yaml" + # Verify that the YAMLs are where we are pointed + if [[ ! -s "${_yaml_file}" ]]; then + echo "The YAML file ${_yaml_file} does not exist!" + echo "Please check the input yaml list and directory." + if [[ "${_set_email}" == true ]]; then + read -r -d '' _message << EOM + generate_workflows.sh failed to find one of the specified YAMLs (${_yaml_file}) + in the specified YAML directory (${_yaml_dir}). +EOM + send_email "${_message}" + fi + exit 10 + fi + + # Strip any unsupported tests + _unsupported_systems=$(sed '1,/skip_ci_on_hosts/ d' "${_yaml_file}") + + for _system in ${_unsupported_systems}; do + if [[ "${_system}" =~ ${machine} ]]; then + if [[ "${_specified_yaml_list}" == true ]]; then + printf "WARNING %s is unsupported on %s, removing from case list\n\n" "${_yaml}" "${machine}" + if [[ "${_set_email}" == true ]]; then + _final_message="${_final_message:-}"$'\n'"The specified YAML case ${_yaml} is not supported on ${machine} and was skipped." + fi + # Sleep so the user has a moment to notice + sleep 2s + fi + unset '_yaml_list[${i}]' + break + fi + done +done + +# Update the account if specified +[[ "${_set_account}" == true ]] && export HPC_ACCOUNT=${_hpc_account} && \ + [[ "${_verbose}" == true ]] && printf "Setting HPC account to %s\n\n" "${HPC_ACCOUNT}" + +# Create the experiments +rm -f "tests.cron" "${_verbose_flag}" +echo "Running create_experiment.py for ${#_yaml_list[@]} cases" + +[[ "${_verbose}" == true ]] && printf "Selected cases: %s\n\n" "${_yaml_list[*]}" +for _case in "${_yaml_list[@]}"; do + [[ "${_verbose}" == false ]] && echo "${_case}" + _create_exp_cmd="./create_experiment.py -y ../ci/cases/pr/${_case}.yaml --overwrite" + if [[ "${_verbose}" == true ]]; then + pslot=${_case} RUNTESTS=${_runtests} ${_create_exp_cmd} + else + if ! pslot=${_case} RUNTESTS=${_runtests} ${_create_exp_cmd} 2> stderr 1> stdout; then + _output=$(cat stdout stderr) + _message="The create_experiment command (${_create_exp_cmd}) failed with a non-zero status. Output:" + _message="${_message}"$'\n'"${_output}" + if [[ "${_set_email}" == true ]]; then + send_email "${_message}" + fi + echo "${_message}" + rm -f stdout stderr + exit 11 + fi + rm -f stdout stderr + fi + grep "${_case}" "${_runtests}/EXPDIR/${_case}/${_case}.crontab" >> tests.cron +done +echo + +# Update the cron +if [[ "${_update_cron}" == "true" ]]; then + printf "Updating the existing crontab\n\n" + echo + rm -f existing.cron final.cron "${_verbose_flag}" + touch existing.cron final.cron + + # disable -e in case crontab is empty + set +e + crontab -l > existing.cron + set -e + + if [[ "${_debug}" == "true" ]]; then + echo "Existing crontab: " + echo "#######################" + cat existing.cron + echo "#######################" + fi + + if [[ "${_set_email}" == "true" ]]; then + # Replace the existing email in the crontab + [[ "${_verbose}" == "true" ]] && printf "Updating crontab email to %s\n\n" "${_email}" + sed -i "/^MAILTO/d" existing.cron + echo "MAILTO=\"${_email}\"" >> final.cron + fi + + cat existing.cron tests.cron >> final.cron + + if [[ "${_verbose}" == "true" ]]; then + echo "Setting crontab to:" + echo "#######################" + cat final.cron + echo "#######################" + fi + + crontab final.cron +else + _message="Add the following to your crontab or scrontab to start running:" + _cron_tests=$(cat tests.cron) + _message="${_message}"$'\n'"${_cron_tests}" + echo "${_message}" + if [[ "${_set_email}" == true ]]; then + final_message="${final_message:-}"$'\n'"${_message}" + fi +fi + +# Cleanup +[[ "${_debug}" == "false" ]] && rm -f final.cron existing.cron tests.cron "${_verbose_flag}" + +echo "Success!!" +if [[ "${_set_email}" == true ]]; then + final_message=$'Success!\n'"${final_message:-}" + _subject="generate_workflow.sh completed successfully" send_email "${final_message}" +fi diff --git a/workflow/rocoto/gefs_tasks.py b/workflow/rocoto/gefs_tasks.py index 955f631c8e..e9338c90df 100644 --- a/workflow/rocoto/gefs_tasks.py +++ b/workflow/rocoto/gefs_tasks.py @@ -472,7 +472,7 @@ def wavepostpnt(self): def extractvars(self): deps = [] if self.app_config.do_wave: - dep_dict = {'type': 'task', 'name': 'wave_post_grid_mem#member#'} + dep_dict = {'type': 'task', 'name': 'gefs_wave_post_grid_mem#member#'} deps.append(rocoto.add_dependency(dep_dict)) if self.app_config.do_ocean: dep_dict = {'type': 'metatask', 'name': 'gefs_ocean_prod_#member#'} diff --git a/workflow/rocoto/gefs_xml.py b/workflow/rocoto/gefs_xml.py index b25a73fa6c..a5dfd5140e 100644 --- a/workflow/rocoto/gefs_xml.py +++ b/workflow/rocoto/gefs_xml.py @@ -14,19 +14,19 @@ def __init__(self, app_config: AppConfig, rocoto_config: Dict) -> None: super().__init__(app_config, rocoto_config) def get_cycledefs(self): - sdate = self._base['SDATE'] + sdate = self._base['SDATE_GFS'] edate = self._base['EDATE'] - interval = self._base.get('INTERVAL_GFS', to_timedelta('24H')) + interval = self._app_config.interval_gfs sdate_str = sdate.strftime("%Y%m%d%H%M") edate_str = edate.strftime("%Y%m%d%H%M") interval_str = timedelta_to_HMS(interval) strings = [] strings.append(f'\t{sdate_str} {edate_str} {interval_str}') - sdate = sdate + interval - if sdate <= edate: - sdate_str = sdate.strftime("%Y%m%d%H%M") - strings.append(f'\t{sdate_str} {edate_str} {interval_str}') + date2 = sdate + interval + if date2 <= edate: + date2_str = date2.strftime("%Y%m%d%H%M") + strings.append(f'\t{date2_str} {edate_str} {interval_str}') strings.append('') strings.append('') diff --git a/workflow/rocoto/gfs_cycled_xml.py b/workflow/rocoto/gfs_cycled_xml.py index afd663c337..eef77ba7fc 100644 --- a/workflow/rocoto/gfs_cycled_xml.py +++ b/workflow/rocoto/gfs_cycled_xml.py @@ -24,19 +24,38 @@ def get_cycledefs(self): sdate_str = sdate.strftime("%Y%m%d%H%M") strings.append(f'\t{sdate_str} {edate_str} {interval_str}') - if self._app_config.gfs_cyc != 0: + interval_gfs = self._app_config.interval_gfs + + if interval_gfs > to_timedelta("0H"): sdate_gfs = self._base['SDATE_GFS'] - edate_gfs = self._base['EDATE_GFS'] - interval_gfs = self._base['INTERVAL_GFS'] + edate_gfs = sdate_gfs + ((edate - sdate_gfs) // interval_gfs) * interval_gfs sdate_gfs_str = sdate_gfs.strftime("%Y%m%d%H%M") edate_gfs_str = edate_gfs.strftime("%Y%m%d%H%M") interval_gfs_str = timedelta_to_HMS(interval_gfs) strings.append(f'\t{sdate_gfs_str} {edate_gfs_str} {interval_gfs_str}') - sdate_gfs = sdate_gfs + interval_gfs - sdate_gfs_str = sdate_gfs.strftime("%Y%m%d%H%M") - if sdate_gfs <= edate_gfs: - strings.append(f'\t{sdate_gfs_str} {edate_gfs_str} {interval_gfs_str}') + date2_gfs = sdate_gfs + interval_gfs + date2_gfs_str = date2_gfs.strftime("%Y%m%d%H%M") + if date2_gfs <= edate_gfs: + strings.append(f'\t{date2_gfs_str} {edate_gfs_str} {interval_gfs_str}') + + if self._base['DO_METP']: + if interval_gfs < to_timedelta('24H'): + # Run verification at 18z, no matter what if there is more than one gfs per day + sdate_metp = sdate_gfs.replace(hour=18) + edate_metp = edate_gfs.replace(hour=18) + interval_metp = to_timedelta('24H') + sdate_metp_str = sdate_metp.strftime("%Y%m%d%H%M") + edate_metp_str = edate_metp.strftime("%Y%m%d%H%M") + interval_metp_str = timedelta_to_HMS(interval_metp) + else: + # Use same cycledef as gfs if there is no more than one per day + sdate_metp_str = sdate_gfs_str + edate_metp_str = edate_gfs_str + interval_metp_str = interval_gfs_str + + strings.append(f'\t{sdate_metp_str} {edate_metp_str} {interval_metp_str}') + strings.append(f'\t{edate_gfs_str} {edate_gfs_str} 24:00:00') strings.append('') strings.append('') diff --git a/workflow/rocoto/gfs_forecast_only_xml.py b/workflow/rocoto/gfs_forecast_only_xml.py index cf53e685e9..a4d5b0878b 100644 --- a/workflow/rocoto/gfs_forecast_only_xml.py +++ b/workflow/rocoto/gfs_forecast_only_xml.py @@ -12,15 +12,36 @@ def __init__(self, app_config: AppConfig, rocoto_config: Dict) -> None: super().__init__(app_config, rocoto_config) def get_cycledefs(self): - sdate = self._base['SDATE'] - edate = self._base['EDATE'] - interval = self._base.get('INTERVAL_GFS', to_timedelta('24H')) + sdate_gfs = self._base['SDATE_GFS'] + edate_gfs = self._base['EDATE'] + interval_gfs = self._app_config.interval_gfs strings = [] - strings.append(f'\t{sdate.strftime("%Y%m%d%H%M")} {edate.strftime("%Y%m%d%H%M")} {timedelta_to_HMS(interval)}') - - sdate = sdate + interval - if sdate <= edate: - strings.append(f'\t{sdate.strftime("%Y%m%d%H%M")} {edate.strftime("%Y%m%d%H%M")} {timedelta_to_HMS(interval)}') + sdate_gfs_str = sdate_gfs.strftime("%Y%m%d%H%M") + edate_gfs_str = edate_gfs.strftime("%Y%m%d%H%M") + interval_gfs_str = timedelta_to_HMS(interval_gfs) + strings.append(f'\t{sdate_gfs_str} {edate_gfs_str} {interval_gfs_str}') + + date2 = sdate_gfs + interval_gfs + if date2 <= edate_gfs: + date2_gfs_str = date2_gfs.strftime("%Y%m%d%H%M") + strings.append(f'\t{date2_gfs_str} {edate_gfs_str} {interval_gfs_str}') + + if self._base['DO_METP']: + if interval_gfs < to_timedelta('24H'): + # Run verification at 18z, no matter what if there is more than one gfs per day + sdate_metp = sdate_gfs.replace(hour=18) + edate_metp = edate_gfs.replace(hour=18) + interval_metp = to_timedelta('24H') + sdate_metp_str = sdate_metp.strftime("%Y%m%d%H%M") + edate_metp_str = edate_metp.strftime("%Y%m%d%H%M") + interval_metp_str = timedelta_to_HMS(interval_metp) + else: + # Use same cycledef as gfs if there is no more than one per day + sdate_metp_str = sdate_gfs_str + edate_metp_str = edate_gfs_str + interval_metp_str = interval_gfs_str + + strings.append(f'\t{sdate_metp_str} {edate_metp_str} {interval_metp_str}') strings.append('') strings.append('') diff --git a/workflow/rocoto/gfs_tasks.py b/workflow/rocoto/gfs_tasks.py index 7c56f25583..82dfb9f1d4 100644 --- a/workflow/rocoto/gfs_tasks.py +++ b/workflow/rocoto/gfs_tasks.py @@ -1,6 +1,6 @@ from applications.applications import AppConfig from rocoto.tasks import Tasks -from wxflow import timedelta_to_HMS +from wxflow import timedelta_to_HMS, to_timedelta import rocoto.rocoto as rocoto import numpy as np @@ -39,7 +39,6 @@ def stage_ic(self): def prep(self): dump_suffix = self._base["DUMP_SUFFIX"] - gfs_cyc = self._base["gfs_cyc"] dmpdir = self._base["DMPDIR"] atm_hist_path = self._template_to_rocoto_cycstring(self._base["COM_ATMOS_HISTORY_TMPL"], {'RUN': 'gdas'}) dump_path = self._template_to_rocoto_cycstring(self._base["COM_OBSDMP_TMPL"], @@ -48,10 +47,10 @@ def prep(self): gfs_enkf = True if self.app_config.do_hybvar and 'gfs' in self.app_config.eupd_runs else False deps = [] - dep_dict = {'type': 'metatask', 'name': 'gdas_atmos_prod', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'gdas_atmos_prod', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) data = f'{atm_hist_path}/gdas.t@Hz.atmf009.nc' - dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) data = f'{dump_path}/{self.run}.t@Hz.updated.status.tm00.bufr_d' dep_dict = {'type': 'data', 'data': data} @@ -59,7 +58,7 @@ def prep(self): dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) cycledef = self.run - if self.run in ['gfs'] and gfs_enkf and gfs_cyc != 4: + if self.run in ['gfs'] and gfs_enkf and self.app_config.interval_gfs != 6: cycledef = 'gdas' resources = self.get_resource('prep') @@ -89,7 +88,7 @@ def waveinit(self): dep_dict = {'type': 'task', 'name': f'{self.run}_prep'} deps.append(rocoto.add_dependency(dep_dict)) if self.run in ['gdas']: - dep_dict = {'type': 'cycleexist', 'condition': 'not', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'cycleexist', 'condition': 'not', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='or', dep=deps) @@ -189,7 +188,7 @@ def anal(self): dep_dict = {'type': 'task', 'name': f'{self.run}_prep'} deps.append(rocoto.add_dependency(dep_dict)) if self.app_config.do_hybvar: - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) else: @@ -255,7 +254,7 @@ def analcalc(self): dep_dict = {'type': 'task', 'name': f'{self.run}_sfcanl'} deps.append(rocoto.add_dependency(dep_dict)) if self.app_config.do_hybvar and self.run in ['gdas']: - dep_dict = {'type': 'task', 'name': 'enkfgdas_echgres', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'task', 'name': 'enkfgdas_echgres', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -330,17 +329,17 @@ def atmanlinit(self): dep_dict = {'type': 'task', 'name': f'{self.run}_prepatmiodaobs'} deps.append(rocoto.add_dependency(dep_dict)) if self.app_config.do_hybvar: - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) else: dependencies = rocoto.create_dependency(dep=deps) - gfs_cyc = self._base["gfs_cyc"] + interval_gfs = self._base["INTERVAL_GFS"] gfs_enkf = True if self.app_config.do_hybvar and 'gfs' in self.app_config.eupd_runs else False cycledef = self.run - if self.run in ['gfs'] and gfs_enkf and gfs_cyc != 4: + if self.run in ['gfs'] and gfs_enkf and interval_gfs != 6: cycledef = 'gdas' resources = self.get_resource('atmanlinit') @@ -482,7 +481,7 @@ def aeroanlgenb(self): def aeroanlinit(self): deps = [] - dep_dict = {'type': 'task', 'name': 'gdas_aeroanlgenb', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'task', 'name': 'gdas_aeroanlgenb', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dep_dict = {'type': 'task', 'name': f'{self.run}_prep'} deps.append(rocoto.add_dependency(dep_dict)) @@ -514,7 +513,7 @@ def aeroanlvar(self): deps = [] dep_dict = { 'type': 'task', 'name': f'gdas_aeroanlgenb', - 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}", + 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}", } deps.append(rocoto.add_dependency(dep_dict)) dep_dict = { @@ -618,7 +617,7 @@ def esnowrecen(self): deps.append(rocoto.add_dependency(dep_dict)) dep_dict = {'type': 'task', 'name': f'{self.run.replace("enkf","")}_snowanl'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': f'{self.run}_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': f'{self.run}_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -644,7 +643,7 @@ def prepoceanobs(self): deps = [] data = f'{ocean_hist_path}/gdas.ocean.t@Hz.inst.f009.nc' - dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep=deps) @@ -671,7 +670,7 @@ def marinebmat(self): deps = [] data = f'{ocean_hist_path}/gdas.ocean.t@Hz.inst.f009.nc' - dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'data', 'data': data, 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep=deps) @@ -699,7 +698,7 @@ def marineanlinit(self): deps.append(rocoto.add_dependency(dep_dict)) dep_dict = {'type': 'task', 'name': f'{self.run}_marinebmat'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'gdas_fcst', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'gdas_fcst', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -878,9 +877,9 @@ def _fcst_forecast_only(self): # Calculate offset based on RUN = gfs | gdas interval = None if self.run in ['gfs']: - interval = self._base['INTERVAL_GFS'] + interval = to_timedelta(f"{self._base['INTERVAL_GFS']}H") elif self.run in ['gdas']: - interval = self._base['INTERVAL'] + interval = self._base['assim_freq'] offset = timedelta_to_HMS(-interval) deps = [] dep_dict = {'type': 'task', 'name': f'{self.run}_aerosol_init'} @@ -1835,14 +1834,27 @@ def metp(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run}_arch'} deps.append(rocoto.add_dependency(dep_dict)) - dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) + if self.app_config.interval_gfs < to_timedelta('24H'): + n_lookback = self.app_config.interval_gfs // to_timedelta('6H') + for lookback in range(1, n_lookback + 1): + deps2 = [] + dep_dict = {'type': 'taskvalid', 'name': f'{self.run}_arch', 'condition': 'not'} + deps2.append(rocoto.add_dependency(dep_dict)) + for lookback2 in range(1, lookback): + offset = timedelta_to_HMS(-to_timedelta(f'{6*lookback2}H')) + dep_dict = {'type': 'cycleexist', 'condition': 'not', 'offset': offset} + deps2.append(rocoto.add_dependency(dep_dict)) + + offset = timedelta_to_HMS(-to_timedelta(f'{6*lookback}H')) + dep_dict = {'type': 'task', 'name': f'{self.run}_arch', 'offset': offset} + deps2.append(rocoto.add_dependency(dep_dict)) + deps.append(rocoto.create_dependency(dep_condition='and', dep=deps2)) + + dependencies = rocoto.create_dependency(dep_condition='or', dep=deps) metpenvars = self.envars.copy() - if self.app_config.mode in ['cycled']: - metpenvar_dict = {'SDATE_GFS': self._base.get('SDATE_GFS').strftime("%Y%m%d%H"), - 'EDATE_GFS': self._base.get('EDATE_GFS').strftime("%Y%m%d%H")} - elif self.app_config.mode in ['forecast-only']: - metpenvar_dict = {'SDATE_GFS': self._base.get('SDATE').strftime("%Y%m%d%H")} + metpenvar_dict = {'SDATE_GFS': self._base.get('SDATE_GFS').strftime("%Y%m%d%H"), + 'EDATE_GFS': self._base.get('EDATE').strftime("%Y%m%d%H")} metpenvar_dict['METPCASE'] = '#metpcase#' for key, value in metpenvar_dict.items(): metpenvars.append(rocoto.create_envar(name=key, value=str(value))) @@ -1858,7 +1870,7 @@ def metp(self): 'resources': resources, 'dependency': dependencies, 'envars': metpenvars, - 'cycledef': self.run.replace('enkf', ''), + 'cycledef': 'metp,last_gfs', 'command': f'{self.HOMEgfs}/jobs/rocoto/metp.sh', 'job_name': f'{self.pslot}_{task_name}_@H', 'log': f'{self.rotdir}/logs/@Y@m@d@H/{task_name}.log', @@ -2331,8 +2343,13 @@ def cleanup(self): deps.append(rocoto.add_dependency(dep_dict)) if self.app_config.do_metp and self.run in ['gfs']: + deps2 = [] + # taskvalid only handles regular tasks, so just check the first metp job exists + dep_dict = {'type': 'taskvalid', 'name': f'{self.run}_metpg2g1', 'condition': 'not'} + deps2.append(rocoto.add_dependency(dep_dict)) dep_dict = {'type': 'metatask', 'name': f'{self.run}_metp'} - deps.append(rocoto.add_dependency(dep_dict)) + deps2.append(rocoto.add_dependency(dep_dict)) + deps.append(rocoto.create_dependency(dep_condition='or', dep=deps2)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2358,7 +2375,7 @@ def eobs(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run.replace("enkf","")}_prep'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2468,7 +2485,7 @@ def atmensanlinit(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run.replace("enkf","")}_prepatmiodaobs'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2495,7 +2512,7 @@ def atmensanlobs(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run}_atmensanlinit'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2521,7 +2538,7 @@ def atmensanlsol(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run}_atmensanlobs'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2547,7 +2564,7 @@ def atmensanlletkf(self): deps = [] dep_dict = {'type': 'task', 'name': f'{self.run}_atmensanlinit'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) @@ -2576,7 +2593,7 @@ def atmensanlfv3inc(self): else: dep_dict = {'type': 'task', 'name': f'{self.run}_atmensanlletkf'} deps.append(rocoto.add_dependency(dep_dict)) - dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['cycle_interval'])}"} + dep_dict = {'type': 'metatask', 'name': 'enkfgdas_epmn', 'offset': f"-{timedelta_to_HMS(self._base['interval_gdas'])}"} deps.append(rocoto.add_dependency(dep_dict)) dependencies = rocoto.create_dependency(dep_condition='and', dep=deps) diff --git a/workflow/rocoto/rocoto.py b/workflow/rocoto/rocoto.py index 2a20820da8..7bacf99829 100644 --- a/workflow/rocoto/rocoto.py +++ b/workflow/rocoto/rocoto.py @@ -183,6 +183,7 @@ def add_dependency(dep_dict: Dict[str, Any]) -> str: 'metatask': _add_task_tag, 'data': _add_data_tag, 'cycleexist': _add_cycle_tag, + 'taskvalid': _add_taskvalid_tag, 'streq': _add_streq_tag, 'strneq': _add_streq_tag, 'sh': _add_sh_tag} @@ -296,6 +297,27 @@ def _add_cycle_tag(dep_dict: Dict[str, Any]) -> str: return string +def _add_taskvalid_tag(dep_dict: Dict[str, Any]) -> str: + """ + create a validtask tag + :param dep_dict: dependency key-value parameters + :type dep_dict: dict + :return: Rocoto validtask dependency + :rtype: str + """ + + dep_type = dep_dict.get('type', None) + dep_name = dep_dict.get('name', None) + + if dep_name is None: + msg = f'a {dep_type} name is necessary for {dep_type} dependency' + raise KeyError(msg) + + string = f'<{dep_type} task="{dep_name}"/>' + + return string + + def _add_streq_tag(dep_dict: Dict[str, Any]) -> str: """ create a simple string comparison tag diff --git a/workflow/rocoto/tasks.py b/workflow/rocoto/tasks.py index 8a32827377..92ceea73aa 100644 --- a/workflow/rocoto/tasks.py +++ b/workflow/rocoto/tasks.py @@ -57,7 +57,8 @@ def __init__(self, app_config: AppConfig, run: str) -> None: self.nmem = int(self._base['NMEM_ENS_GFS']) else: self.nmem = int(self._base['NMEM_ENS']) - self._base['cycle_interval'] = to_timedelta(f'{self._base["assim_freq"]}H') + self._base['interval_gdas'] = to_timedelta(f'{self._base["assim_freq"]}H') + self._base['interval_gfs'] = to_timedelta(f'{self._base["INTERVAL_GFS"]}H') self.n_tiles = 6 # TODO - this needs to be elsewhere diff --git a/workflow/setup_expt.py b/workflow/setup_expt.py index e213394e20..f32203e600 100755 --- a/workflow/setup_expt.py +++ b/workflow/setup_expt.py @@ -8,7 +8,7 @@ import glob import shutil import warnings -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, SUPPRESS +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, SUPPRESS, ArgumentTypeError from hosts import Host @@ -116,7 +116,8 @@ def edit_baseconfig(host, inputs, yaml_dict): "@COMROOT@": inputs.comroot, "@EXP_WARM_START@": is_warm_start, "@MODE@": inputs.mode, - "@gfs_cyc@": inputs.gfs_cyc, + "@INTERVAL_GFS@": inputs.interval, + "@SDATE_GFS@": datetime_to_YMDH(inputs.sdate_gfs), "@APP@": inputs.app, "@NMEM_ENS@": getattr(inputs, 'nens', 0) } @@ -186,6 +187,19 @@ def input_args(*argv): ufs_apps = ['ATM', 'ATMA', 'ATMW', 'S2S', 'S2SA', 'S2SW', 'S2SWA'] + def _validate_interval(interval_str): + err_msg = f'must be a non-negative integer multiple of 6 ({interval_str} given)' + try: + interval = int(interval_str) + except ValueError: + raise ArgumentTypeError(err_msg) + + # This assumes the gdas frequency (assim_freq) is 6h + # If this changes, the modulus needs to as well + if interval < 0 or interval % 6 != 0: + raise ArgumentTypeError(err_msg) + return interval + def _common_args(parser): parser.add_argument('--pslot', help='parallel experiment name', type=str, required=False, default='test') @@ -199,7 +213,8 @@ def _common_args(parser): type=str, required=False, default=os.getenv('HOME')) parser.add_argument('--idate', help='starting date of experiment, initial conditions must exist!', required=True, type=lambda dd: to_datetime(dd)) - parser.add_argument('--edate', help='end date experiment', required=True, type=lambda dd: to_datetime(dd)) + parser.add_argument('--edate', help='end date experiment', required=False, type=lambda dd: to_datetime(dd)) + parser.add_argument('--interval', help='frequency of forecast (in hours); must be a multiple of 6', type=_validate_interval, required=False, default=6) parser.add_argument('--icsdir', help='full path to user initial condition directory', type=str, required=False, default='') parser.add_argument('--overwrite', help='overwrite previously created experiment (if it exists)', action='store_true', required=False) @@ -219,8 +234,7 @@ def _gfs_args(parser): def _gfs_cycled_args(parser): parser.add_argument('--app', help='UFS application', type=str, choices=ufs_apps, required=False, default='ATM') - parser.add_argument('--gfs_cyc', help='cycles to run forecast', type=int, - choices=[0, 1, 2, 4], default=1, required=False) + parser.add_argument('--sdate_gfs', help='date to start GFS', type=lambda dd: to_datetime(dd), required=False, default=None) return parser def _gfs_or_gefs_ensemble_args(parser): @@ -233,8 +247,6 @@ def _gfs_or_gefs_ensemble_args(parser): def _gfs_or_gefs_forecast_args(parser): parser.add_argument('--app', help='UFS application', type=str, choices=ufs_apps, required=False, default='ATM') - parser.add_argument('--gfs_cyc', help='Number of forecasts per day', type=int, - choices=[1, 2, 4], default=1, required=False) return parser def _gefs_args(parser): @@ -291,7 +303,28 @@ def _gefs_args(parser): for subp in [gefsforecasts]: subp = _gefs_args(subp) - return parser.parse_args(list(*argv) if len(argv) else None) + inputs = parser.parse_args(list(*argv) if len(argv) else None) + + # Validate dates + if inputs.edate is None: + inputs.edate = inputs.idate + + if inputs.edate < inputs.idate: + raise ArgumentTypeError(f'edate ({inputs.edate}) cannot be before idate ({inputs.idate})') + + # For forecast-only, GFS starts in the first cycle + if not hasattr(inputs, 'sdate_gfs'): + inputs.sdate_gfs = inputs.idate + + # For cycled, GFS starts after the half-cycle + if inputs.sdate_gfs is None: + inputs.sdate_gfs = inputs.idate + to_timedelta("6H") + + if inputs.interval > 0: + if inputs.sdate_gfs < inputs.idate or inputs.sdate_gfs > inputs.edate: + raise ArgumentTypeError(f'sdate_gfs ({inputs.sdate_gfs}) must be between idate ({inputs.idate}) and edate ({inputs.edate})') + + return inputs def query_and_clean(dirname, force_clean=False): @@ -303,7 +336,7 @@ def query_and_clean(dirname, force_clean=False): if os.path.exists(dirname): print(f'\ndirectory already exists in {dirname}') if force_clean: - overwrite = True + overwrite = "YES" print(f'removing directory ........ {dirname}\n') else: overwrite = input('Do you wish to over-write [y/N]: ')