diff --git a/jobs/JGLOBAL_ANALYSIS_STATS b/jobs/JGLOBAL_ANALYSIS_STATS new file mode 100755 index 00000000000..cfdb809f880 --- /dev/null +++ b/jobs/JGLOBAL_ANALYSIS_STATS @@ -0,0 +1,36 @@ +#! /usr/bin/env bash + +source "${HOMEgfs}/ush/preamble.sh" +export DATA=${DATA:-${DATAROOT}/${RUN}statanl_${cyc}} +source "${HOMEgfs}/ush/jjob_header.sh" -e "anlstat" -c "base anlstat" + +############################################## +# Set variables used in the script +############################################## + + +############################################## +# Begin JOB SPECIFIC work +############################################## + + +############################################################### +# 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..d9ce48dc0c8 --- /dev/null +++ b/parm/config/gfs/config.anlstat @@ -0,0 +1,11 @@ +#!/bin/bash -x + +########## config.anlstat ########## +# Analysis Stat + +echo "BEGIN: config.anlstat" + +# Get task specific resources +source "${EXPDIR}/config.resources" anlstat + +echo "END: config.anlstat" diff --git a/parm/config/gfs/config.base b/parm/config/gfs/config.base index 81b18030fad..b8d8a0ad429 100644 --- a/parm/config/gfs/config.base +++ b/parm/config/gfs/config.base @@ -72,9 +72,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.resources b/parm/config/gfs/config.resources index b50e1c5fbb0..1ae71beb38e 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" @@ -707,6 +707,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/scripts/exglobal_analysis_stats.py b/scripts/exglobal_analysis_stats.py new file mode 100755 index 00000000000..23c2a3a7eb2 --- /dev/null +++ b/scripts/exglobal_analysis_stats.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# exglobal_analysis_stats.py +# This script will run the OOPS/JEDI code to + +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) + + # Call StatAnalysis + anl = StatAnalysis(config) + anl.initialize() diff --git a/ush/python/pygfs/task/stat_analysis.py b/ush/python/pygfs/task/stat_analysis.py new file mode 100644 index 00000000000..4b8a072d6ba --- /dev/null +++ b/ush/python/pygfs/task/stat_analysis.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +import os +from logging import getLogger +from typing import Dict, List +from pprint import pformat +import numpy as np +from netCDF4 import Dataset + +from wxflow import (AttrDict, + FileHandler, + to_fv3time, to_YMD, to_YMDH, to_timedelta, add_to_datetime, + rm_p, + parse_j2yaml, save_as_yaml, + Jinja, + logit, + Executable, + WorkflowException) +from pygfs.task.analysis import Analysis + +logger = getLogger(__name__.split('.')[-1]) + + +class StatAnalysis(Analysis): + """ + Class for global stat analysis tasks + """ + + @logit(logger, name="StatAnalysis") + def __init__(self, config): + super().__init__(config) + + _res = int(self.task_config['CASE'][1:]) + _window_begin = add_to_datetime(self.task_config.current_cycle, -to_timedelta(f"{self.task_config['assim_freq']}H") / 2) + _letkfoi_yaml = os.path.join(self.task_config.DATA, f"{self.task_config.RUN}.t{self.task_config['cyc']:02d}z.letkfoi.yaml") + + # Create a local dictionary that is repeatedly used across this class + local_dict = AttrDict( + { + 'APREFIX': f"{self.task_config.RUN}.t{self.task_config.cyc:02d}z.", + 'jedi_yaml': _letkfoi_yaml + } + ) + + # Extend task_config with local_dict + self.task_config = AttrDict(**self.task_config, **local_dict) + + @logit(logger) + def initialize(self: Analysis) -> None: + """ + Initialize global stat analysis. + """ + super().initialize() + + logger.info(f"Copying files to {self.task_config.DATA}/stats") + + aerostat = os.path.join(self.task_config.COM_CHEM_ANALYSIS, f"{self.task_config['APREFIX']}aerostat") + dest = os.path.join(self.task_config.DATA, "stats") + statlist = [aerostat, dest] + FileHandler({'copy': statlist}).sync() diff --git a/workflow/applications/applications.py b/workflow/applications/applications.py index a694129e386..fff66ba8994 100644 --- a/workflow/applications/applications.py +++ b/workflow/applications/applications.py @@ -40,29 +40,29 @@ def __init__(self, conf: Configuration) -> None: f'Valid application modes are:\n' f'{", ".join(self.VALID_MODES)}\n') - self.net = base['NET'] - self.model_app = base.get('APP', 'ATM') - self.do_atm = base.get('DO_ATM', True) - self.do_wave = base.get('DO_WAVE', False) - self.do_wave_bnd = base.get('DOBNDPNT_WAVE', False) - self.do_ocean = base.get('DO_OCN', False) - self.do_ice = base.get('DO_ICE', False) - self.do_aero = base.get('DO_AERO', False) - self.do_prep_obs_aero = base.get('DO_PREP_OBS_AERO', False) - self.do_bufrsnd = base.get('DO_BUFRSND', False) - self.do_gempak = base.get('DO_GEMPAK', False) - self.do_awips = base.get('DO_AWIPS', False) - 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_tracker = base.get('DO_TRACKER', True) - self.do_genesis = base.get('DO_GENESIS', True) - self.do_genesis_fsu = base.get('DO_GENESIS_FSU', False) - self.do_metp = base.get('DO_METP', False) - self.do_upp = not base.get('WRITE_DOPOST', True) - self.do_goes = base.get('DO_GOES', False) - self.do_mos = base.get('DO_MOS', False) - self.do_extractvars = base.get('DO_EXTRACTVARS', False) + self.net = _base['NET'] + self.model_app = _base.get('APP', 'ATM') + self.do_atm = _base.get('DO_ATM', True) + self.do_wave = _base.get('DO_WAVE', False) + self.do_wave_bnd = _base.get('DOBNDPNT_WAVE', False) + self.do_ocean = _base.get('DO_OCN', False) + self.do_ice = _base.get('DO_ICE', False) + self.do_aero = _base.get('DO_AERO', False) + self.do_prep_obs_aero = _base.get('DO_PREP_OBS_AERO', False) + self.do_bufrsnd = _base.get('DO_BUFRSND', False) + self.do_gempak = _base.get('DO_GEMPAK', False) + self.do_awips = _base.get('DO_AWIPS', False) + 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_tracker = _base.get('DO_TRACKER', True) + self.do_genesis = _base.get('DO_GENESIS', True) + self.do_genesis_fsu = _base.get('DO_GENESIS_FSU', False) + self.do_metp = _base.get('DO_METP', False) + self.do_upp = not _base.get('WRITE_DOPOST', True) + self.do_goes = _base.get('DO_GOES', False) + self.do_mos = _base.get('DO_MOS', False) + self.do_extractvars = _base.get('DO_EXTRACTVARS', False) self.do_hpssarch = base.get('HPSSARCH', False) diff --git a/workflow/applications/gfs_cycled.py b/workflow/applications/gfs_cycled.py index 4bb473f454e..8a24dc3a30f 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 89da933d00c..e8ad67443bb 100644 --- a/workflow/rocoto/gfs_tasks.py +++ b/workflow/rocoto/gfs_tasks.py @@ -1739,6 +1739,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'} @@ -2233,6 +2267,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)) @@ -2248,6 +2285,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 df2b0467db7..82ea94e7464 100644 --- a/workflow/rocoto/tasks.py +++ b/workflow/rocoto/tasks.py @@ -21,6 +21,7 @@ class Tasks: 'atmensanlinit', 'atmensanlobs', 'atmensanlsol', 'atmensanlletkf', 'atmensanlfv3inc', 'atmensanlfinal', 'aeroanlinit', 'aeroanlvar', 'aeroanlfinal', 'aeroanlgenb', 'prepsnowobs', 'snowanl', 'esnowrecen', + 'anlstat', 'fcst', 'atmanlupp', 'atmanlprod', 'atmupp', 'goesupp', 'atmos_prod', 'ocean_prod', 'ice_prod',