diff --git a/parm/soca/berror/soca_ensrecenter.yaml b/parm/soca/berror/soca_ensrecenter.yaml new file mode 100644 index 000000000..e3e9422b8 --- /dev/null +++ b/parm/soca/berror/soca_ensrecenter.yaml @@ -0,0 +1,102 @@ +# This yaml is for applying deterministic recentering increments to the ensemble members + +geometry: + mom6_input_nml: mom_input.nml + fields metadata: ./fields_metadata.yaml + +date: '{{ ATM_WINDOW_BEGIN }}' + +layers variable: [hocn] + +increment variables: [tocn, socn, uocn, vocn, ssh, hocn, cicen, hicen, hsnon] + +set increment variables to zero: [ssh] + +vertical geometry: + date: '{{ ATM_WINDOW_BEGIN }}' + basename: ./INPUT/ + ocn_filename: MOM.res.nc + read_from_file: 3 + +add recentering increment: false + +soca increments: # Could also be states, but they are read as increments + number of increments: {{ NMEM_ENS }} + pattern: '%mem%' + template: + date: '{{ ATM_WINDOW_BEGIN }}' + basename: ./ens/ + ocn_filename: 'ocean.%mem%.nc' + ice_filename: 'ice.%mem%.nc' + read_from_file: 3 + +steric height: + linear variable changes: + - linear variable change name: BalanceSOCA # Only the steric balance is applied + +#ensemble mean output: +# datadir: ./static_ens +# date: '{{ ATM_WINDOW_BEGIN }}' +# exp: ens_mean +# type: incr + +ssh output: + unbalanced: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: ssh_unbal_stddev + type: incr + + steric: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: ssh_steric_stddev + type: incr + + total: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: ssh_total_stddev + type: incr + + explained variance: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: steric_explained_variance + type: incr + + recentering error: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: ssh_recentering_error + type: incr + +background error output: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: bkgerr_stddev + type: incr + +#linear variable change: +# linear variable changes: +# - linear variable change name: BkgErrFILT +# ocean_depth_min: 500 # zero where ocean is shallower than 500m +# rescale_bkgerr: 1.0 # rescale perturbation +# efold_z: 1500.0 # Apply exponential decay +# - linear variable change name: BalanceSOCA + +trajectory: + state variables: [tocn, socn, uocn, vocn, ssh, hocn, layer_depth, mld, cicen, hicen, hsnon] + date: '{{ ATM_WINDOW_BEGIN }}' + basename: ./INPUT/ + ocn_filename: MOM.res.nc + ice_filename: cice.res.nc + read_from_file: 1 + +output increment: + datadir: ./ + date: '{{ ATM_WINDOW_BEGIN }}' + exp: trash + type: incr + output file: 'ocn.recenter.incr.%mem%.nc' + pattern: '%mem%' diff --git a/parm/soca/obsprep/obsprep_config.yaml b/parm/soca/obsprep/obsprep_config.yaml index 38ca66759..8e82d9643 100644 --- a/parm/soca/obsprep/obsprep_config.yaml +++ b/parm/soca/obsprep/obsprep_config.yaml @@ -22,6 +22,7 @@ observations: window: back: 8 # look back 8 six-hourly obs dumps forward: 1 # look forward 1 six-hourly bin + error ratio: 0.4 # meters per day # Ice concentration - obs space: diff --git a/scripts/exgdas_global_marine_analysis_ecen.py b/scripts/exgdas_global_marine_analysis_ecen.py new file mode 100755 index 000000000..f023f018b --- /dev/null +++ b/scripts/exgdas_global_marine_analysis_ecen.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# exgdas_global_marine_analysis_ecen.py +# This script creates an MarineRecenter class +# and runs the initialize, run, and finalize methods +# which currently are stubs +import os + +from wxflow import Logger, cast_strdict_as_dtypedict +# TODO (AFE): change to from pygfs.task.marine_recenter import MarineRecenter +from soca.marine_recenter import MarineRecenter + +# Initialize root logger +logger = Logger(level='DEBUG', colored_log=True) + + +if __name__ == '__main__': + + # Take configuration from environment and cast it as python dictionary + config = cast_strdict_as_dtypedict(os.environ) + + # Instantiate the aerosol analysis task + MarineRecen = MarineRecenter(config) + MarineRecen.initialize() + MarineRecen.run() + MarineRecen.finalize() diff --git a/test/soca/gw/CMakeLists.txt b/test/soca/gw/CMakeLists.txt index 25d290571..5220c7e40 100644 --- a/test/soca/gw/CMakeLists.txt +++ b/test/soca/gw/CMakeLists.txt @@ -45,6 +45,7 @@ set(jjob_list "JGLOBAL_PREP_OCEAN_OBS" "JGDAS_GLOBAL_OCEAN_ANALYSIS_PREP" "JGDAS_GLOBAL_OCEAN_ANALYSIS_BMAT" "JGDAS_GLOBAL_OCEAN_ANALYSIS_RUN" + "JGDAS_GLOBAL_OCEAN_ANALYSIS_ECEN" "JGDAS_GLOBAL_OCEAN_ANALYSIS_CHKPT" "JGDAS_GLOBAL_OCEAN_ANALYSIS_POST" "JGDAS_GLOBAL_OCEAN_ANALYSIS_VRFY") diff --git a/test/soca/gw/run_jjobs.yaml.test b/test/soca/gw/run_jjobs.yaml.test index 72fef1dfb..356439204 100644 --- a/test/soca/gw/run_jjobs.yaml.test +++ b/test/soca/gw/run_jjobs.yaml.test @@ -35,6 +35,7 @@ gw environement: run scripts: GDASPREPOCNOBSPY: @HOMEgfs@/sorc/gdas.cd/scripts/exglobal_prep_ocean_obs.py + GDASOCNCENPY: @HOMEgfs@/sorc/gdas.cd/scripts/exgdas_global_marine_analysis_ecen.py setup_expt config: base: diff --git a/ush/soca/bkg_utils.py b/ush/soca/bkg_utils.py index d255cfa9a..08be3dd89 100755 --- a/ush/soca/bkg_utils.py +++ b/ush/soca/bkg_utils.py @@ -101,13 +101,13 @@ def gen_bkg_list(bkg_path, out_path, window_begin=' ', yaml_name='bkg.yaml', ice bkg_date = window_begin # Construct list of background file names - GDUMP = os.getenv('GDUMP') + RUN = os.getenv('RUN') cyc = str(os.getenv('cyc')).zfill(2) gcyc = str((int(cyc) - 6) % 24).zfill(2) # previous cycle fcst_hrs = list(range(3, 10, dt_pseudo)) files = [] for fcst_hr in fcst_hrs: - files.append(os.path.join(bkg_path, f'{GDUMP}.ocean.t'+gcyc+'z.inst.f'+str(fcst_hr).zfill(3)+'.nc')) + files.append(os.path.join(bkg_path, f'{RUN}.ocean.t'+gcyc+'z.inst.f'+str(fcst_hr).zfill(3)+'.nc')) # Identify the ocean background that will be used for the vertical coordinate remapping ocn_filename_ic = os.path.splitext(os.path.basename(files[0]))[0]+'.nc' diff --git a/ush/soca/marine_recenter.py b/ush/soca/marine_recenter.py new file mode 100644 index 000000000..4c49bb530 --- /dev/null +++ b/ush/soca/marine_recenter.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 + +from datetime import datetime, timedelta +import f90nml +from logging import getLogger +import os +from soca import bkg_utils +from typing import Dict +import ufsda +from ufsda.stage import soca_fix +from wxflow import (AttrDict, + chdir, + Executable, + FileHandler, + logit, + parse_j2yaml, + Task, + Template, + TemplateConstants, + WorkflowException, + YAMLFile) + +logger = getLogger(__name__.split('.')[-1]) + + +class MarineRecenter(Task): + """ + Class for global ocean analysis recentering task + """ + + @logit(logger, name="MarineRecenter") + def __init__(self, config: Dict) -> None: + """Constructor for ocean recentering task + Parameters: + ------------ + config: Dict + configuration, namely evironment variables + Returns: + -------- + None + """ + + logger.info("init") + super().__init__(config) + + # Variables of convenience + # TODO (AFE) maybe the g- vars should be done in the jjob + PDY = self.runtime_config['PDY'] + cyc = self.runtime_config['cyc'] + DATA = self.runtime_config.DATA + cdate = PDY + timedelta(hours=cyc) + gdate = cdate - timedelta(hours=6) + self.runtime_config['gcyc'] = gdate.strftime("%H") + self.runtime_config['gPDY'] = datetime(gdate.year, + gdate.month, + gdate.day) + + gdas_home = os.path.join(config['HOMEgfs'], 'sorc', 'gdas.cd') + + half_assim_freq = timedelta(hours=int(config['assim_freq'])/2) + window_begin = cdate - half_assim_freq + window_begin_iso = window_begin.strftime('%Y-%m-%dT%H:%M:%SZ') + window_middle_iso = cdate.strftime('%Y-%m-%dT%H:%M:%SZ') + + self.recen_config = AttrDict( + {'window_begin': f"{window_begin.strftime('%Y-%m-%dT%H:%M:%SZ')}", + 'ATM_WINDOW_BEGIN': window_begin_iso, + 'ATM_WINDOW_MIDDLE': window_middle_iso, + 'DATA': DATA, + 'dump': self.runtime_config.RUN, + 'fv3jedi_stage_files': self.config.FV3JEDI_STAGE_YAML, + 'fv3jedi_stage': self.config.FV3JEDI_STAGE_YAML, + 'stage_dir': DATA, + 'soca_input_fix_dir': self.config.SOCA_INPUT_FIX_DIR, + 'NMEM_ENS': self.config.NMEM_ENS, + 'ATM_WINDOW_LENGTH': f"PT{config['assim_freq']}H"}) + + berror_yaml_dir = os.path.join(gdas_home, 'parm', 'soca', 'berror') + self.config['recen_yaml_template'] = os.path.join(berror_yaml_dir, 'soca_ensrecenter.yaml') + self.config['recen_yaml_file'] = os.path.join(DATA, 'soca_ensrecenter.yaml') + self.config['gridgen_yaml'] = os.path.join(gdas_home, 'parm', 'soca', 'gridgen', 'gridgen.yaml') + self.config['BKG_LIST'] = 'bkg_list.yaml' + self.config['window_begin'] = window_begin + self.config['mom_input_nml_src'] = os.path.join(gdas_home, 'parm', 'soca', 'fms', 'input.nml') + self.config['mom_input_nml_tmpl'] = os.path.join(DATA, 'mom_input.nml.tmpl') + self.config['mom_input_nml'] = os.path.join(DATA, 'mom_input.nml') + self.config['bkg_dir'] = os.path.join(DATA, 'bkg') + self.config['INPUT'] = os.path.join(DATA, 'INPUT') + self.config['ens_dir'] = os.path.join(DATA, 'ens') + + @logit(logger) + def initialize(self): + """Method initialize for ocean recentering task + Parameters: + ------------ + None + Returns: + -------- + None + """ + + logger.info("initialize") + RUN = self.runtime_config.RUN + gcyc = self.runtime_config.gcyc + + ufsda.stage.soca_fix(self.recen_config) + + ################################################################################ + # prepare input.nml + FileHandler({'copy': [[self.config.mom_input_nml_src, self.config.mom_input_nml_tmpl]]}).sync() + + # swap date and stack size + domain_stack_size = self.config.DOMAIN_STACK_SIZE + ymdhms = [int(s) for s in self.config.window_begin.strftime('%Y,%m,%d,%H,%M,%S').split(',')] + with open(self.config.mom_input_nml_tmpl, 'r') as nml_file: + nml = f90nml.read(nml_file) + nml['ocean_solo_nml']['date_init'] = ymdhms + nml['fms_nml']['domains_stack_size'] = int(domain_stack_size) + ufsda.disk_utils.removefile(self.config.mom_input_nml) + nml.write(self.config.mom_input_nml) + + FileHandler({'mkdir': [self.config.bkg_dir]}).sync() + bkg_utils.gen_bkg_list(bkg_path=self.config.COM_OCEAN_HISTORY_PREV, + out_path=self.config.bkg_dir, + window_begin=self.config.window_begin, + yaml_name=self.config.BKG_LIST) + + ################################################################################ + # Copy initial condition + + bkg_utils.stage_ic(self.config.bkg_dir, self.runtime_config.DATA, RUN, gcyc) + + ################################################################################ + # stage ensemble members + logger.info("---------------- Stage ensemble members") + FileHandler({'mkdir': [self.config.ens_dir]}).sync() + nmem_ens = self.config.NMEM_ENS + gPDYstr = self.runtime_config.gPDY.strftime("%Y%m%d") + ens_member_list = [] + for mem in range(1, nmem_ens+1): + for domain in ['ocean', 'ice']: + mem_dir = os.path.join(self.config.ROTDIR, + f'enkf{RUN}.{gPDYstr}', + f'{gcyc}', + f'mem{str(mem).zfill(3)}', + 'model_data', + domain, + 'history') + mem_dir_real = os.path.realpath(mem_dir) + f009 = f'enkf{RUN}.{domain}.t{gcyc}z.inst.f009.nc' + + fname_in = os.path.abspath(os.path.join(mem_dir_real, f009)) + fname_out = os.path.realpath(os.path.join(self.config.ens_dir, + domain+"."+str(mem)+".nc")) + ens_member_list.append([fname_in, fname_out]) + + FileHandler({'copy': ens_member_list}).sync() + + ################################################################################ + # generate the YAML file for recenterer + + logger.info(f"---------------- generate soca_ensrecenter.yaml") + + recen_yaml = parse_j2yaml(self.config.recen_yaml_template, self.recen_config) + recen_yaml.save(self.config.recen_yaml_file) + + @logit(logger) + def run(self): + """Method run for ocean recentering task + Parameters: + ------------ + None + Returns: + -------- + None + """ + + logger.info("run") + + chdir(self.runtime_config.DATA) + + exec_cmd_gridgen = Executable(self.config.APRUN_OCNANALECEN) + exec_name_gridgen = os.path.join(self.config.JEDI_BIN, 'soca_gridgen.x') + exec_cmd_gridgen.add_default_arg(exec_name_gridgen) + exec_cmd_gridgen.add_default_arg(self.config.gridgen_yaml) + + try: + logger.debug(f"Executing {exec_cmd_gridgen}") + exec_cmd_gridgen() + except OSError: + raise OSError(f"Failed to execute {exec_cmd_gridgen}") + except Exception: + raise WorkflowException(f"An error occured during execution of {exec_cmd_gridgen}") + pass + + exec_cmd_recen = Executable(self.config.APRUN_OCNANALECEN) + exec_name_recen = os.path.join(self.config.JEDI_BIN, 'gdas_ens_handler.x') + exec_cmd_recen.add_default_arg(exec_name_recen) + exec_cmd_recen.add_default_arg(os.path.basename(self.config.recen_yaml_file)) + + try: + logger.debug(f"Executing {exec_cmd_recen}") + exec_cmd_recen() + except OSError: + raise OSError(f"Failed to execute {exec_cmd_recen}") + except Exception: + raise WorkflowException(f"An error occured during execution of {exec_cmd_recen}") + pass + + @logit(logger) + def finalize(self): + """Method finalize for ocean recentering task + Parameters: + ------------ + None + Returns: + -------- + None + """ + + logger.info("finalize") + + RUN = self.runtime_config.RUN + cyc = self.runtime_config.cyc + incr_file = f'enkf{RUN}.t{cyc}z.ocninc.nc' + nmem_ens = self.config.NMEM_ENS + PDYstr = self.runtime_config.PDY.strftime("%Y%m%d") + mem_dir_list = [] + copy_list = [] + + for mem in range(1, nmem_ens+1): + mem_dir = os.path.join(self.config.ROTDIR, + f'enkf{RUN}.{PDYstr}', + f'{cyc}', + f'mem{str(mem).zfill(3)}', + 'analysis', + 'ocean') + mem_dir_real = os.path.realpath(mem_dir) + mem_dir_list.append(mem_dir_real) + + copy_list.append([f'ocn.recenter.incr.{str(mem)}.nc', + os.path.join(mem_dir_real, incr_file)]) + + FileHandler({'mkdir': mem_dir_list}).sync() + FileHandler({'copy': copy_list}).sync() diff --git a/ush/ufsda/yamltools.py b/ush/ufsda/yamltools.py index b9c93cf53..d5978adcd 100644 --- a/ush/ufsda/yamltools.py +++ b/ush/ufsda/yamltools.py @@ -3,7 +3,7 @@ import re import logging from ufsda.misc_utils import isTrue -from wxflow import YAMLFile, TemplateConstants, Template +from wxflow import save_as_yaml, YAMLFile, TemplateConstants, Template logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S') @@ -34,7 +34,7 @@ def save_check(config, target, app='var'): config['cost function']['observations']['observers'] = cleaned_obs_spaces # save cleaned yaml - wxflow.save_as_yaml(config, target) + save_as_yaml(config, target) def parse_config(input_config_dict, template=None, clean=True):