From adf4190d61c1027e3ba1db93b1862d6a0a242175 Mon Sep 17 00:00:00 2001 From: dooruk Date: Mon, 24 Aug 2026 17:00:54 -0400 Subject: [PATCH 01/13] set diag_table and ice_in according to background_frequency --- src/swell/tasks/prep_coupled_geos_run_dir.py | 23 +++-- src/swell/utilities/geos.py | 89 +++++++++++++++++++- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index 84f1bb1c6..c229c2190 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -32,11 +32,12 @@ def execute(self) -> None: As the name suggests, this task is geared towards coupled GEOSgcm simulations but the only difference between this and dataOcean ones should be in the get_static() method. - xx) Changes HOMDIR and EXPDIR in gcm_run.j to point to the forecast directory - xx) Provides consistency for GEOSgcm version by copying GEOSgcm.x from EXPDIR - xx) (Optional) Checks GEOSDIR, GEOSBIN, GEOSETC, GEOSUTIL are consistent with + 01) Changes HOMDIR and EXPDIR in gcm_run.j to point to the forecast directory + 02) Provides consistency for GEOSgcm version by copying GEOSgcm.x from EXPDIR + 03) Modifies ice_in and diag_table files to match background_frequency + xx) (Not active) (Optional) Checks GEOSDIR, GEOSBIN, GEOSETC, GEOSUTIL are consistent with experiment.yaml value if the override switch is on - xx) Modifies CAP.rc according to cycle_date and forecast_duration + 04) Modifies CAP.rc according to cycle_date and forecast_duration """ # These links were created in get_*_geos_restart task. This step will copy experiment @@ -66,14 +67,20 @@ def execute(self) -> None: # ---------------- self.get_static() + # Modify ice_in and diag_table to allow different DA windows without changing the GEOSgcm + # experiment directory + # -------------------------------- + bkgr_freq = self.config.get_key_for_model('background_frequency', 'geos_marine', 'PT00') + self.geos.process_icein(bkgr_freq) + self.geos.process_diag_table(bkgr_freq) + # IAU augment for MOM6 # -------------------- - if 'geos_marine' == self.get_model(): - self.mom6_iau() + self.mom6_iau() # Modify input.nml if not cold start (default) # -------------------------------------------- - self.geos.process_nml() + self.geos.process_inputnml() # Parse .rc files and convert bool.s to Python format # --------------------------------------------------- @@ -124,7 +131,7 @@ def execute(self) -> None: # else: # outfile.write(line) - # TODO: Still need to rewrite CAP.rc here for now to make sure END_TIME is long enough + # Still need to rewrite CAP.rc here for now to ensure END_TIME is long enough self.cap_dict = self.rewrite_cap(self.cap_dict, self.forecast_dir('CAP.rc')) self.agcm_dict = self.geos.parse_rc(self.forecast_dir('AGCM.rc')) diff --git a/src/swell/utilities/geos.py b/src/swell/utilities/geos.py index 129f846e5..beaa76297 100644 --- a/src/swell/utilities/geos.py +++ b/src/swell/utilities/geos.py @@ -12,6 +12,7 @@ import glob import isodate import os +import re from typing import Tuple, Optional from swell.utilities.datetime_util import datetime_formats @@ -309,7 +310,7 @@ def write_rc( # ---------------------------------------------------------------------------------------------- - def process_nml( + def process_inputnml( self, combine_fvcore: bool = False, cold_restart: bool = False @@ -342,6 +343,92 @@ def process_nml( with open(os.path.join(self.forecast_dir, 'input.nml'), 'w') as f: f90nml.write(nml_comb, f, sort=False) + # ---------------------------------------------------------------------------------------------- + + def process_diag_table( + self, + bkg_freq: str = "PT00", + ) -> None: + """ + Adjusts the GEOS diag_table history entry to match the background frequency. + + This file is not a namelist, so the change is implemented as a targeted text rewrite + for the history template line that uses the background interval. + + TODO: In newer FMS version, a YAML file is used for diag_table, so this should be more + straightforward to set eventually. + + Args: + bkg_freq (str): Frequency for background processing. Default "PT00" is not used. + """ + + diag_table_path = os.path.join(self.forecast_dir, 'diag_table') + if not os.path.exists(diag_table_path): + self.logger.abort(f"diag_table not found: {diag_table_path}") + + bkg_duration = isodate.parse_duration(bkg_freq) + bkg_hours = int(bkg_duration.total_seconds() / 3600) + + self.logger.info(f"Updating diag_table history frequency with background frequency: {bkg_freq}") + + with open(diag_table_path, 'r') as infile: + content = infile.read() + + pattern = re.compile( + r'("his%4yr%2mo%2dy%2hr"\s*,\s*)(\d+)(\s*,\s*"hours"\s*,\s*\d+\s*,\s*"hours"\s*,\s*"time"\s*,\s*)(\d+)(\s*,\s*"hours")' + ) + updated_content, count = pattern.subn( + lambda match: ( + f"{match.group(1)}{bkg_hours}{match.group(3)}{bkg_hours}{match.group(5)}" + ), + content, + ) + + if count == 0: + pattern = re.compile(r'("his%4yr%2mo%2dy%2hr"\s*,\s*)(\d+)(\s*,\s*"hours")') + updated_content, count = pattern.subn( + lambda match: f"{match.group(1)}{bkg_hours}{match.group(3)}", + content, + ) + + if count == 0: + self.logger.warning('No diag_table history entry matched the expected pattern') + return + + with open(diag_table_path, 'w') as outfile: + outfile.write(updated_content) + + # -------------------------------------------------------------------------------------------------- + + def process_icein( + self, + bkg_freq: str = "PT00", + ) -> None: + """ + Adjusts the ice_in namelist file for history output to match the background frequency. + + Args: + bkg_freq (str): Frequency for background processing. Defaults to "PT00". + """ + + # Make sure icein.nml is set up properly for hot/cold restart + nml_comb = f90nml.read(os.path.join(self.forecast_dir, 'ice_in')) + + # Convert background frequency to hours for histfreq_n + bkg_duration = isodate.parse_duration(bkg_freq) + bkg_hours = int(bkg_duration.total_seconds() / 3600) + + self.logger.info(f"Updating ice_in history frequency with background frequency: {bkg_freq}") + + # Replace `h` with background frequency + # histfreq = 'h', 'd', 'x', 'x', 'x' + # histfreq_n = 6, 1, 1, 1, 1 + nml_comb['setup_nml']['histfreq'] = ['h', 'd', 'x', 'x', 'x'] + nml_comb['setup_nml']['histfreq_n'][0] = bkg_hours + + with open(os.path.join(self.forecast_dir, 'ice_in'), 'w') as f: + f90nml.write(nml_comb, f, sort=False) + # -------------------------------------------------------------------------------------------------- def rc_to_bool( From a297dc1241edd60a747f89a4d0bd0f19cf9d621c Mon Sep 17 00:00:00 2001 From: dooruk Date: Mon, 24 Aug 2026 17:02:01 -0400 Subject: [PATCH 02/13] add 5day suite options, though they don't work --- .../3dfgat_marine_cycle/suite_config.py | 57 ++++++++++++++++++- src/swell/suites/3dvar_marine/suite_config.py | 18 ++++++ src/swell/tasks/get_coupled_geos_restart.py | 3 +- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/swell/suites/3dfgat_marine_cycle/suite_config.py b/src/swell/suites/3dfgat_marine_cycle/suite_config.py index da954da5c..284009e0c 100644 --- a/src/swell/suites/3dfgat_marine_cycle/suite_config.py +++ b/src/swell/suites/3dfgat_marine_cycle/suite_config.py @@ -141,8 +141,6 @@ class SuiteConfig(QuestionContainer, Enum): "insitu_profile_tao", "icec_amsr2_north", "icec_amsr2_south", - "icec_nsidc_nh", - "icec_nsidc_sh", "sst_ostia", "sss_smos", "sss_smapv5", @@ -151,7 +149,6 @@ class SuiteConfig(QuestionContainer, Enum): "sst_avhrrf_mc_l3u", "sst_viirs_n20_l3u", "sst_viirs_npp_l3u", - "temp_profile_xbt" ]), qd.number_of_iterations([50]), qd.background_time_offset("P1DT12H"), @@ -159,3 +156,57 @@ class SuiteConfig(QuestionContainer, Enum): ) # -------------------------------------------------------------------------------------------------- + + _3dfgat_marine_cycle_5day = QuestionList( + list_name="3dfgat_marine_cycle_5day", + questions=[ + _3dfgat_marine_cycle_tier2, + qd.start_cycle_point("2023-01-08T12:00:00Z"), + qd.final_cycle_point("2023-02-27T12:00:00Z"), + qd.forecast_duration("P10D"), + qd.geos_homdir("/discover/nobackup/projects/gmao/soca/dardag/GEOS_FORWARD/GEOS_v12_rc20/" + "dataatm_025deg_om4_swell"), + ], + geos_marine=[ + qd.cycle_times([ + "P5D", + ]), + qd.marine_models([ + "mom6", + ]), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_jason3n", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "adt_sentinel6a", + "adt_swot_nadir", + "insitu_profile_argo", + "insitu_profile_ctd", + "insitu_profile_pirata", + "insitu_profile_rama", + "insitu_profile_tao", + "sss_smos", + "sss_smapv5", + "sst_avhrrf_mb_l3u", + "sst_avhrrf_mc_l3u", + "sst_viirs_n20_l3u", + "sst_viirs_npp_l3u", + ]), + qd.analysis_variables([ + "sea_water_salinity", + "sea_water_potential_temperature", + "sea_surface_height_above_geoid", + "sea_water_cell_thickness", + ]), + qd.window_length("P5D"), + qd.background_frequency("PT12H"), + qd.mom6_iau_nhours("PT18H"), + qd.background_time_offset("P7DT12H"), + qd.number_of_iterations([75]), + ] + ) + + # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_marine/suite_config.py b/src/swell/suites/3dvar_marine/suite_config.py index 002432e0c..f52b7ff22 100644 --- a/src/swell/suites/3dvar_marine/suite_config.py +++ b/src/swell/suites/3dvar_marine/suite_config.py @@ -112,3 +112,21 @@ class SuiteConfig(QuestionContainer, Enum): ) # -------------------------------------------------------------------------------------------------- + + _3dvar_marine_5day = QuestionList( + list_name="3dvar_marine_5day", + questions=[ + _3dvar_marine, + qd.start_cycle_point("2023-01-07T12:00:00Z"), + qd.final_cycle_point("2023-01-17T12:00:00Z"), + qd.forecast_duration("P10D"), + ], + geos_marine=[ + qd.cycle_times(['T120']), + qd.window_length("P5D"), + qd.background_frequency("PT12H"), + qd.background_time_offset("P7DT12H"), + ] + ) + + # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/get_coupled_geos_restart.py b/src/swell/tasks/get_coupled_geos_restart.py index 75c1d57c0..d089de20f 100644 --- a/src/swell/tasks/get_coupled_geos_restart.py +++ b/src/swell/tasks/get_coupled_geos_restart.py @@ -21,7 +21,8 @@ class GetCoupledGeosRestart(taskBase): # ---------------------------------------------------------------------------------------------- def execute(self) -> None: - """Copies coupled GEOS restart files to the forecast directory. + """Copies coupled GEOS restart files to the forecast directory. Coupled here indicate that the + simulation involves both the atmosphere and marine (MOM6 + CICE6) components. The files copied include: - *_rst files (including atmosphere and tile interface files) From ee312653a040a6d5aa26b2a823cb9ef33327b6ec Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 10:28:11 -0400 Subject: [PATCH 03/13] use suite_to_run in rendering to define localization and halo only for letkf application --- .../geos_marine/observations/adt_sentinel6a.yaml | 4 ++++ src/swell/suites/suite_questions.py | 1 + src/swell/tasks/get_observations.py | 3 +++ src/swell/tasks/render_jedi_observations.py | 3 +++ src/swell/tasks/task_questions.py | 2 ++ src/swell/utilities/question_defaults.py | 9 +++++++++ src/swell/utilities/render_jedi_interface_files.py | 1 + 7 files changed, 23 insertions(+) diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_sentinel6a.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_sentinel6a.yaml index 91796a752..ca5ae8a87 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_sentinel6a.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_sentinel6a.yaml @@ -8,9 +8,11 @@ obs space: engine: type: H5File obsfile: '{{cycle_dir}}/{{experiment_id}}.adt_sentinel6a.{{window_begin}}.nc4' +{% if 'letkf' in suite_to_run %} distribution: name: Halo halo size: 3500.0e3 +{% endif %} simulated variables: [absoluteDynamicTopography] obs operator: name: ADT @@ -41,6 +43,7 @@ obs filters: - variable: { name: GeoVaLs/sea_ice_area_fraction} maxvalue: 0.00001 {% endif %} +{% if 'letkf' in suite_to_run %} obs localizations: - localization method: Rossby base value: 100.0e3 @@ -48,3 +51,4 @@ obs localizations: min grid mult: 2.0 min value: 200.0e3 max value: 900.0e3 +{% endif %} diff --git a/src/swell/suites/suite_questions.py b/src/swell/suites/suite_questions.py index 31a58acdc..802ce9712 100644 --- a/src/swell/suites/suite_questions.py +++ b/src/swell/suites/suite_questions.py @@ -45,6 +45,7 @@ class SuiteQuestions(QuestionContainer, Enum): qd.r2d2_server(), qd.r2d2_datastore(), qd.skip_r2d2(), + qd.suite_to_run(), ] ) diff --git a/src/swell/tasks/get_observations.py b/src/swell/tasks/get_observations.py index b792500f1..5e0fded09 100644 --- a/src/swell/tasks/get_observations.py +++ b/src/swell/tasks/get_observations.py @@ -261,6 +261,9 @@ def execute(self) -> None: self.jedi_rendering.add_key('window_begin', window_begin) self.jedi_rendering.add_key('marine_models', self.config.marine_models(None)) + # Needed for localization templating + self.jedi_rendering.add_key('suite_to_run', self.config.suite_to_run()) + # Read observation ioda names ioda_names_list = get_ioda_names_list() diff --git a/src/swell/tasks/render_jedi_observations.py b/src/swell/tasks/render_jedi_observations.py index a2c43f26a..be6312cdb 100644 --- a/src/swell/tasks/render_jedi_observations.py +++ b/src/swell/tasks/render_jedi_observations.py @@ -49,6 +49,9 @@ def execute(self) -> None: self.jedi_rendering.add_key('crtm_coeff_dir', crtm_coeff_dir) self.jedi_rendering.add_key('marine_models', marine_models) + # Needed for localization templating + self.jedi_rendering.add_key('suite_to_run', self.config.suite_to_run()) + cwd = os.getcwd() if self.config.mock_experiment(False): diff --git a/src/swell/tasks/task_questions.py b/src/swell/tasks/task_questions.py index f2e085ed0..fc217ac47 100644 --- a/src/swell/tasks/task_questions.py +++ b/src/swell/tasks/task_questions.py @@ -475,6 +475,7 @@ class TaskQuestions(QuestionContainer, Enum): qd.obs_experiment(), qd.observation_providers(), qd.observing_system_records_path(), + qd.suite_to_run(), qd.window_length(), ] ) @@ -658,6 +659,7 @@ class TaskQuestions(QuestionContainer, Enum): qd.background_time_offset(), qd.observing_system_records_path(), qd.observations(), + qd.suite_to_run(), qd.window_length(), qd.mock_experiment() ] diff --git a/src/swell/utilities/question_defaults.py b/src/swell/utilities/question_defaults.py index f05e59afb..1ed626a62 100644 --- a/src/swell/utilities/question_defaults.py +++ b/src/swell/utilities/question_defaults.py @@ -271,6 +271,15 @@ class start_cycle_point(SuiteQuestion): # -------------------------------------------------------------------------------------------------- + @dataclass + class suite_to_run(SuiteQuestion): + default_value: str = "test" + question_name: str = "suite_to_run" + prompt: str = "Record of the suite being executed" + widget_type: WType = WType.STRING + + # -------------------------------------------------------------------------------------------------- + @dataclass class window_type(SuiteQuestion): default_value: str = "defer_to_model" diff --git a/src/swell/utilities/render_jedi_interface_files.py b/src/swell/utilities/render_jedi_interface_files.py index fa06fcf1f..8c800f1b4 100644 --- a/src/swell/utilities/render_jedi_interface_files.py +++ b/src/swell/utilities/render_jedi_interface_files.py @@ -130,6 +130,7 @@ def __init__( 'skip_ensemble_hofx', 'swell_static_files', 'start_cycle_point', + 'suite_to_run', 'total_processors', 'vertical_localization_apply_log_transform', 'vertical_localization_function', From f11c8abbd96c23e829b02805761677405214cd9c Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 10:28:52 -0400 Subject: [PATCH 04/13] use window length in save obs diags --- src/swell/tasks/save_obs_diags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swell/tasks/save_obs_diags.py b/src/swell/tasks/save_obs_diags.py index eacf9ba58..33e6b0f2d 100644 --- a/src/swell/tasks/save_obs_diags.py +++ b/src/swell/tasks/save_obs_diags.py @@ -89,7 +89,7 @@ def execute(self) -> None: experiment=self.config.r2d2_experiment_id(), observation_type=name, file_extension=obs_path_file.split('.')[-1], - window_length='PT6H', + window_length=window_length, window_start=window_begin, source_file=obs_path_file, member=-9999, From 8e6358e0404dc7f38cf0906ffa43c997d2e4b838 Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 14:36:30 -0400 Subject: [PATCH 05/13] make other localizations and halo letkf dependent --- .../jedi/interfaces/geos_marine/observations/adt_jason3.yaml | 4 ++++ .../geos_marine/observations/insitu_profile_argo.yaml | 4 ++++ .../jedi/interfaces/geos_marine/observations/sss_smos.yaml | 4 ++++ .../geos_marine/observations/sst_viirs_n20_l3u.yaml | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_jason3.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_jason3.yaml index 60940310a..0a3da411c 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_jason3.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/observations/adt_jason3.yaml @@ -8,9 +8,11 @@ obs space: engine: type: H5File obsfile: '{{cycle_dir}}/{{experiment_id}}.adt_jason3.{{window_begin}}.nc4' +{% if 'letkf' in suite_to_run %} distribution: name: Halo halo size: 3500.0e3 +{% endif %} simulated variables: [absoluteDynamicTopography] obs operator: name: ADT @@ -41,6 +43,7 @@ obs filters: - variable: { name: GeoVaLs/sea_ice_area_fraction} maxvalue: 0.00001 {% endif %} +{% if 'letkf' in suite_to_run %} obs localizations: - localization method: Rossby base value: 100.0e3 @@ -48,3 +51,4 @@ obs localizations: min grid mult: 2.0 min value: 200.0e3 max value: 900.0e3 +{% endif %} diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/observations/insitu_profile_argo.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/observations/insitu_profile_argo.yaml index 3460dbf7f..0a8ea1417 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/observations/insitu_profile_argo.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/observations/insitu_profile_argo.yaml @@ -8,9 +8,11 @@ obs space: engine: type: H5File obsfile: '{{cycle_dir}}/{{experiment_id}}.insitu_profile_argo.{{window_begin}}.nc4' +{% if 'letkf' in suite_to_run %} distribution: name: Halo halo size: 3500.0e3 +{% endif %} simulated variables: [waterTemperature, salinity] obs operator: name: Composite @@ -59,6 +61,7 @@ obs filters: where: - variable: {name: GeoVaLs/distance_from_coast} minvalue: 100e3 +{% if 'letkf' in suite_to_run %} obs localizations: - localization method: Rossby base value: 100.0e3 @@ -66,3 +69,4 @@ obs localizations: min grid mult: 2.0 min value: 200.0e3 max value: 900.0e3 +{% endif %} diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/observations/sss_smos.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/observations/sss_smos.yaml index 9666efaae..5285288dc 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/observations/sss_smos.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/observations/sss_smos.yaml @@ -8,9 +8,11 @@ obs space: engine: type: H5File obsfile: '{{cycle_dir}}/{{experiment_id}}.sss_smos.{{window_begin}}.nc4' +{% if 'letkf' in suite_to_run %} distribution: name: Halo halo size: 3500.0e3 +{% endif %} simulated variables: [seaSurfaceSalinity] obs operator: name: Identity @@ -43,6 +45,7 @@ obs filters: where: - variable: {name: GeoVaLs/distance_from_coast} minvalue: 100e3 +{% if 'letkf' in suite_to_run %} obs localizations: - localization method: Rossby base value: 100.0e3 @@ -50,3 +53,4 @@ obs localizations: min grid mult: 2.0 min value: 200.0e3 max value: 900.0e3 +{% endif %} diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/observations/sst_viirs_n20_l3u.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/observations/sst_viirs_n20_l3u.yaml index 5e2d62698..6b8c2e4a2 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/observations/sst_viirs_n20_l3u.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/observations/sst_viirs_n20_l3u.yaml @@ -8,9 +8,11 @@ obs space: engine: type: H5File obsfile: '{{cycle_dir}}/{{experiment_id}}.sst_viirs_n20_l3u.{{window_begin}}.nc4' +{% if 'letkf' in suite_to_run %} distribution: name: Halo halo size: 3500.0e3 +{% endif %} simulated variables: [seaSurfaceTemperature] get values: time interpolation: linear @@ -53,6 +55,7 @@ obs filters: - variable: { name: GeoVaLs/sea_ice_area_fraction} maxvalue: 0.00001 {% endif %} +{% if 'letkf' in suite_to_run %} obs localizations: - localization method: Rossby base value: 100.0e3 @@ -60,3 +63,4 @@ obs localizations: min grid mult: 2.0 min value: 200.0e3 max value: 900.0e3 +{% endif %} From fdd0b5dcdabafaf8c210996a9f157d33d523ea4c Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 22:36:51 -0400 Subject: [PATCH 06/13] suite question alternative --- src/swell/tasks/eva_observations.py | 3 +++ src/swell/tasks/save_obs_diags.py | 3 +++ src/swell/tasks/task_questions.py | 4 +--- src/swell/utilities/config.py | 6 ++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/swell/tasks/eva_observations.py b/src/swell/tasks/eva_observations.py index a545e2ad3..a331a4545 100644 --- a/src/swell/tasks/eva_observations.py +++ b/src/swell/tasks/eva_observations.py @@ -50,6 +50,9 @@ def execute(self) -> None: self.jedi_rendering.add_key('crtm_coeff_dir', self.config.crtm_coeff_dir(None)) self.jedi_rendering.add_key('window_begin', window_begin) + # Needed for localization templating + self.jedi_rendering.add_key('suite_to_run', self.config.suite_to_run()) + # Get the model # ------------- model = self.get_model() diff --git a/src/swell/tasks/save_obs_diags.py b/src/swell/tasks/save_obs_diags.py index 33e6b0f2d..6c1f9b9b0 100644 --- a/src/swell/tasks/save_obs_diags.py +++ b/src/swell/tasks/save_obs_diags.py @@ -53,6 +53,9 @@ def execute(self) -> None: self.jedi_rendering.add_key('crtm_coeff_dir', crtm_coeff_dir) self.jedi_rendering.add_key('window_begin', window_begin) + # Needed for localization templating + self.jedi_rendering.add_key('suite_to_run', self.config.suite_to_run()) + # Loop over observation operators # ------------------------------- for observation in observations: diff --git a/src/swell/tasks/task_questions.py b/src/swell/tasks/task_questions.py index fc217ac47..e04d03d33 100644 --- a/src/swell/tasks/task_questions.py +++ b/src/swell/tasks/task_questions.py @@ -475,7 +475,6 @@ class TaskQuestions(QuestionContainer, Enum): qd.obs_experiment(), qd.observation_providers(), qd.observing_system_records_path(), - qd.suite_to_run(), qd.window_length(), ] ) @@ -659,7 +658,6 @@ class TaskQuestions(QuestionContainer, Enum): qd.background_time_offset(), qd.observing_system_records_path(), qd.observations(), - qd.suite_to_run(), qd.window_length(), qd.mock_experiment() ] @@ -909,7 +907,7 @@ class TaskQuestions(QuestionContainer, Enum): questions=[ background_crtm_obs, qd.window_length(), - qd.marine_models() + qd.marine_models(), ] ) diff --git a/src/swell/utilities/config.py b/src/swell/utilities/config.py index 649e6dd0a..fab1f0d14 100644 --- a/src/swell/utilities/config.py +++ b/src/swell/utilities/config.py @@ -64,6 +64,12 @@ def __init__(self, input_file: str, logger: Logger, task_name: str, model: str) self.__final_cycle_point__ = experiment_dict.get('final_cycle_point') self.__suite_to_run__ = experiment_dict.get('suite_to_run') + # Create getter methods for suite-level variables so they can be used without + # adding them to task_questions + for suite_var in ['experiment_root', 'experiment_id', 'platform', + 'start_cycle_point', 'final_cycle_point', 'suite_to_run']: + setattr(self, suite_var, self.get(suite_var)) + # If experiment_dict contains models key add the model components to the object if 'models' in experiment_dict.keys(): self.__model_components__ = list(experiment_dict['models'].keys()) From b3e32c3269ab1ec439fb2c6507e06199c3c1499c Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 22:46:01 -0400 Subject: [PATCH 07/13] fix codestyle --- src/swell/suites/3dfgat_marine_cycle/suite_config.py | 4 ++-- src/swell/tasks/get_coupled_geos_restart.py | 4 ++-- src/swell/tasks/prep_coupled_geos_run_dir.py | 2 +- src/swell/utilities/geos.py | 6 ++++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/swell/suites/3dfgat_marine_cycle/suite_config.py b/src/swell/suites/3dfgat_marine_cycle/suite_config.py index 284009e0c..cdf8ab577 100644 --- a/src/swell/suites/3dfgat_marine_cycle/suite_config.py +++ b/src/swell/suites/3dfgat_marine_cycle/suite_config.py @@ -164,8 +164,8 @@ class SuiteConfig(QuestionContainer, Enum): qd.start_cycle_point("2023-01-08T12:00:00Z"), qd.final_cycle_point("2023-02-27T12:00:00Z"), qd.forecast_duration("P10D"), - qd.geos_homdir("/discover/nobackup/projects/gmao/soca/dardag/GEOS_FORWARD/GEOS_v12_rc20/" - "dataatm_025deg_om4_swell"), + qd.geos_homdir("/discover/nobackup/projects/gmao/soca/dardag/GEOS_FORWARD/" + "GEOS_v12_rc20/dataatm_025deg_om4_swell"), ], geos_marine=[ qd.cycle_times([ diff --git a/src/swell/tasks/get_coupled_geos_restart.py b/src/swell/tasks/get_coupled_geos_restart.py index d089de20f..dc4ba385e 100644 --- a/src/swell/tasks/get_coupled_geos_restart.py +++ b/src/swell/tasks/get_coupled_geos_restart.py @@ -21,8 +21,8 @@ class GetCoupledGeosRestart(taskBase): # ---------------------------------------------------------------------------------------------- def execute(self) -> None: - """Copies coupled GEOS restart files to the forecast directory. Coupled here indicate that the - simulation involves both the atmosphere and marine (MOM6 + CICE6) components. + """Copies coupled GEOS restart files to the forecast directory. Coupled here indicate that + the simulation involves both the atmosphere and marine (MOM6 + CICE6) components. The files copied include: - *_rst files (including atmosphere and tile interface files) diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index c229c2190..4e5d8a204 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -67,7 +67,7 @@ def execute(self) -> None: # ---------------- self.get_static() - # Modify ice_in and diag_table to allow different DA windows without changing the GEOSgcm + # Modify ice_in and diag_table to allow different DA windows without changing the GEOSgcm # experiment directory # -------------------------------- bkgr_freq = self.config.get_key_for_model('background_frequency', 'geos_marine', 'PT00') diff --git a/src/swell/utilities/geos.py b/src/swell/utilities/geos.py index beaa76297..e0e4103e7 100644 --- a/src/swell/utilities/geos.py +++ b/src/swell/utilities/geos.py @@ -369,13 +369,15 @@ def process_diag_table( bkg_duration = isodate.parse_duration(bkg_freq) bkg_hours = int(bkg_duration.total_seconds() / 3600) - self.logger.info(f"Updating diag_table history frequency with background frequency: {bkg_freq}") + self.logger.info(f"Change diag_table history frequency to background frequency: {bkg_freq}") with open(diag_table_path, 'r') as infile: content = infile.read() pattern = re.compile( - r'("his%4yr%2mo%2dy%2hr"\s*,\s*)(\d+)(\s*,\s*"hours"\s*,\s*\d+\s*,\s*"hours"\s*,\s*"time"\s*,\s*)(\d+)(\s*,\s*"hours")' + r'("his%4yr%2mo%2dy%2hr"\s*,\s*)(\d+)' + r'(\s*,\s*"hours"\s*,\s*\d+\s*,\s*"hours"\s*,\s*"time"\s*,\s*)' + r'(\d+)(\s*,\s*"hours")' ) updated_content, count = pattern.subn( lambda match: ( From 24a49893274a1b1e4964f97a597fbcd77f667c4e Mon Sep 17 00:00:00 2001 From: dooruk Date: Tue, 25 Aug 2026 22:50:04 -0400 Subject: [PATCH 08/13] update mock configs due to letkf condiitonals --- .../jedi_3dfgat_marine_cycle_config.yaml | 40 ------------------- .../jedi_3dvar_marine_config.yaml | 40 ------------------- .../jedi_3dvar_marine_cycle_config.yaml | 40 ------------------- 3 files changed, 120 deletions(-) diff --git a/src/swell/test/jedi_configs/jedi_3dfgat_marine_cycle_config.yaml b/src/swell/test/jedi_configs/jedi_3dfgat_marine_cycle_config.yaml index 52e589832..b6e13450e 100644 --- a/src/swell/test/jedi_configs/jedi_3dfgat_marine_cycle_config.yaml +++ b/src/swell/test/jedi_configs/jedi_3dfgat_marine_cycle_config.yaml @@ -152,9 +152,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.adt_jason3.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - absoluteDynamicTopography obs operator: @@ -189,13 +186,6 @@ cost function: - variable: name: GeoVaLs/sea_ice_area_fraction maxvalue: 1e-05 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: adt_jason3 - obs space: name: adt_saral @@ -342,9 +332,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.insitu_profile_argo.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - waterTemperature - salinity @@ -392,13 +379,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: insitu_profile_argo - obs space: name: icec_amsr2_north @@ -655,9 +635,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sss_smos.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceSalinity obs operator: @@ -695,13 +672,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sss_smos - obs space: name: sss_smapv5 @@ -873,9 +843,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sst_viirs_n20_l3u.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceTemperature get values: @@ -921,13 +888,6 @@ cost function: - variable: name: GeoVaLs/sea_ice_area_fraction maxvalue: 1e-05 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sst_viirs_n20_l3u - obs space: name: temp_profile_xbt diff --git a/src/swell/test/jedi_configs/jedi_3dvar_marine_config.yaml b/src/swell/test/jedi_configs/jedi_3dvar_marine_config.yaml index 288fd73b4..3626dc230 100644 --- a/src/swell/test/jedi_configs/jedi_3dvar_marine_config.yaml +++ b/src/swell/test/jedi_configs/jedi_3dvar_marine_config.yaml @@ -121,9 +121,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.adt_jason3.20210701T000000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - absoluteDynamicTopography obs operator: @@ -153,13 +150,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: adt_jason3 - obs space: name: adt_saral @@ -291,9 +281,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.insitu_profile_argo.20210701T000000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - waterTemperature - salinity @@ -341,13 +328,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: insitu_profile_argo - obs space: name: sst_ostia @@ -405,9 +385,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sss_smos.20210701T000000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceSalinity obs operator: @@ -445,13 +422,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sss_smos - obs space: name: sss_smapv5 @@ -613,9 +583,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sst_viirs_n20_l3u.20210701T000000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceTemperature get values: @@ -656,13 +623,6 @@ cost function: - ObsError/seaSurfaceTemperature coefs: - 1.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sst_viirs_n20_l3u - obs space: name: temp_profile_xbt diff --git a/src/swell/test/jedi_configs/jedi_3dvar_marine_cycle_config.yaml b/src/swell/test/jedi_configs/jedi_3dvar_marine_cycle_config.yaml index f32f108db..c2b9d4ba0 100644 --- a/src/swell/test/jedi_configs/jedi_3dvar_marine_cycle_config.yaml +++ b/src/swell/test/jedi_configs/jedi_3dvar_marine_cycle_config.yaml @@ -121,9 +121,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.adt_jason3.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - absoluteDynamicTopography obs operator: @@ -153,13 +150,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: adt_jason3 - obs space: name: adt_saral @@ -291,9 +281,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.insitu_profile_argo.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - waterTemperature - salinity @@ -341,13 +328,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: insitu_profile_argo - obs space: name: sst_ostia @@ -405,9 +385,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sss_smos.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceSalinity obs operator: @@ -445,13 +422,6 @@ cost function: - variable: name: GeoVaLs/distance_from_coast minvalue: 100000.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sss_smos - obs space: name: sss_smapv5 @@ -613,9 +583,6 @@ cost function: engine: type: H5File obsfile: cycle_dir/experiment_id.sst_viirs_n20_l3u.20210701T090000Z.nc4 - distribution: - name: Halo - halo size: 3500000.0 simulated variables: - seaSurfaceTemperature get values: @@ -656,13 +623,6 @@ cost function: - ObsError/seaSurfaceTemperature coefs: - 1.0 - obs localizations: - - localization method: Rossby - base value: 100000.0 - rossby mult: 1.0 - min grid mult: 2.0 - min value: 200000.0 - max value: 900000.0 observation_name: sst_viirs_n20_l3u - obs space: name: temp_profile_xbt From 1ae5353e18311f58c72c0f39bb7dc8e6997b0513 Mon Sep 17 00:00:00 2001 From: dooruk Date: Wed, 26 Aug 2026 11:12:21 -0400 Subject: [PATCH 09/13] fix default --- src/swell/tasks/prep_coupled_geos_run_dir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index 4e5d8a204..c8bb9de32 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -68,9 +68,9 @@ def execute(self) -> None: self.get_static() # Modify ice_in and diag_table to allow different DA windows without changing the GEOSgcm - # experiment directory + # experiment directory. If background frequency is not available, use default PT3H # -------------------------------- - bkgr_freq = self.config.get_key_for_model('background_frequency', 'geos_marine', 'PT00') + bkgr_freq = self.config.get_key_for_model('background_frequency', 'geos_marine', 'PT3H') self.geos.process_icein(bkgr_freq) self.geos.process_diag_table(bkgr_freq) From 353cdb357073ecf59780a3b6a94d0632e6fa871a Mon Sep 17 00:00:00 2001 From: dooruk Date: Wed, 26 Aug 2026 12:36:28 -0400 Subject: [PATCH 10/13] retrigger ci From c7afeb0ae9d71d426cdbf55cfceef47e2b93e807 Mon Sep 17 00:00:00 2001 From: dooruk Date: Wed, 26 Aug 2026 12:41:15 -0400 Subject: [PATCH 11/13] just to trigger ci --- src/swell/tasks/prep_coupled_geos_run_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index c8bb9de32..31b3220a0 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -24,7 +24,7 @@ class PrepCoupledGeosRunDir(taskBase): def execute(self) -> None: """ - Copies GEOS HOMDIR files to the cycle forecast directory to prepare before executing + Copies GEOS HOMDIR files to the GEOSfcm forecast directory and prepares it before executing gcm_run.j. Modifies certain resource files using python's re package according to cycle_date such as: CAP.rc, AGCM.rc, input.nml, and gcm_run.j. From dc0f7eb75543e240bfa96327678d927ed167c885 Mon Sep 17 00:00:00 2001 From: dooruk Date: Wed, 26 Aug 2026 16:19:18 -0400 Subject: [PATCH 12/13] long description of running coupled GEOS experiments --- docs/_sidebar.md | 2 + .../cycling_geos/coupled_marine_geos_runs.md | 628 ++++++++++++++++++ 2 files changed, 630 insertions(+) create mode 100644 docs/practical_examples/cycling_geos/coupled_marine_geos_runs.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 014bc5f36..8bbead56a 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -49,6 +49,8 @@ - [3DVAR GEOS-CF Cycle](/practical_examples/geos_cf/3dvar_cf_cycle.md) - Background and Observation Ingestion - [Storing Observations and Backgrounds in R2D2](/practical_examples/r2d2/r2d2_ingest.md) + - GEOS Model Runs + - [Coupled marine GEOS runs and workflow](/practical_examples/cycling_geos/coupled_marine_geos_runs.md) - Comparison and Evaluation - [Comparing Experiment Outputs](/practical_examples/generic_suites/comparison_workflows.md) diff --git a/docs/practical_examples/cycling_geos/coupled_marine_geos_runs.md b/docs/practical_examples/cycling_geos/coupled_marine_geos_runs.md new file mode 100644 index 000000000..4b6d4cfc0 --- /dev/null +++ b/docs/practical_examples/cycling_geos/coupled_marine_geos_runs.md @@ -0,0 +1,628 @@ +# Coupled Marine GEOS Cycling in SWELL + +This document describes how GEOS forecasts are executed and cycled within SWELL workflows, particularly for coupled data assimilation experiments with marine (ocean+sea-ice components). + +**Note:** Currently, this workflow only handles marine DA but can handle both coupled and dataAtm modes of executing GEOSgcm. + +## Overview + +The GEOS cycling workflow involves several key tasks that prepare, execute, and post-process coupled atmosphere-ocean-ice forecasts. The cycle typically follows this pattern: + +1. **Initial Setup**: Obtain restart files and prepare the experiment directory +2. **Cycle Preparation**: Configure the `GEOSgcm/forecast` directory for the current forecast +3. **Forecast Execution**: Run the GEOS coupled model (`gcm_run.j`) +4. **Post-Processing & DA**: Link outputs for JEDI, calculate & save analyses, and move restart files to the next cycle + +## Calling `gcm_run.j` in Cylc + +### Workflow Definition + +In the `flow.cylc` file, GEOS is executed through the `RunGeos` task. This task directly calls the `gcm_run.j` script that was prepared in the forecast directory: + +```jinja2 +[[RunGeos]] + script = "{{experiment_path}}/GEOSgcm/forecast/gcm_run.j" + platform = {{platform}} + [[[directives]]] + {%- for key, value in scheduling["RunGeos"]["directives"]["all"].items() %} + --{{key}} = {{value}} + {%- endfor %} +``` + +The `gcm_run.j` script is a GEOS-native job script that: +- Sets up the computational environment with SLURM directives (see [SLURM Configuration](/configuration_reference/slurm_configuration.md) for more details.) +- Defines directory paths (HOMDIR, EXPDIR, GEOSDIR, GEOSBIN, etc.) +- Loads required modules and libraries +- Executes the GEOSgcm.x binary +- Manages model output and restart files. Typically this part is manually modified or taken out as SWELL can handle this part already. In future gcm_run versions, this might be handled in a more modular fashion to better integrate with workflow management systems like SWELL. + +### Task Dependencies + +The `RunGeos` task has specific dependencies defined in the workflow: + +```jinja2 +# Model cannot run without code +BuildGeosByLinking? | BuildGeos => RunGeos + +# Need first set of restarts to run model +GetCoupledGeosRestart => PrepCoupledGeosRunDir + +# Model preperation +MoveDaRestart-{{model_component}}[-{{window_length}}] => PrepCoupledGeosRunDir +PrepCoupledGeosRunDir => RunGeos +``` + +This ensures that: +- GEOS source code is built or linked before execution +- Initial restarts are obtained for the first cycle +- For subsequent cycles, analysis restarts from the previous cycle are moved first +- The `forecast` directory is prepared before model execution + +## Getting GEOS Restart Files: `GetCoupledGeosRestart` + +The `GetCoupledGeosRestart` task handles obtaining the initial restart files needed to start the GEOS coupled simulation. It is important to note that there is no control mechanism within GEOSgcm or SWELL for the time validity of these restarts; users must ensure that the restarts correspond to the correct cycle date. + +### Restart Sources + +The task supports three methods for obtaining restart files, controlled by the `initial_restarts_method` configuration: + +#### 1. From a GEOS Experiment Directory (`geos_expdir`) + +This is the most common method. Restart files are copied from an existing GEOS experiment: + +```yaml +initial_restarts_method: geos_expdir +``` + +The task copies: +- **Atmosphere grid and boundary restarts**: All `*_rst` files (e.g., `fvcore_internal_rst`, `moist_internal_rst`, etc.) +- **CICE6 restart**: `RESTART/iced.nc` +- **MOM6 restarts**: All `RESTART/MOM.res*.nc` files +- **Optional files**: `RESTART/mom6_increment.nc` (for IAU mode) +- **Binary files**: `GEOSgcm.x` (model executable) and `linkbcs` (boundary condition links) +- **RC directory**: Complete directory of resource configuration files + +#### 2. From R2D2 (`r2d2`) + +```yaml +initial_restarts_method: r2d2 +``` + +This method retrieves restarts from the R2D2 data repository. **Note**: This functionality is not yet fully implemented. + +#### 3. Hotstart (`hotstart`) + +```yaml +initial_restarts_method: hotstart +``` + +In hotstart mode, the task assumes restart files already exist in the forecast directory (e.g., manually placed or to resume from a previous run). No files are copied. + +### Directory Structure Setup + +The task also establishes the internal GEOS directory structure within the SWELL experiment: + +``` +{experiment_path}/ +└── GEOSgcm/ + ├── GEOS_homdir/ → symlink to geos_homdir + ├── GEOS_expdir/ → symlink to geos_expdir (if different from homdir) + └── forecast/ + ├── RESTART/ + │ ├── iced.nc + │ ├── MOM.res.nc + │ └── MOM.res_1.nc (if using multiple restart files) + ├── *_rst files + ├── GEOSgcm.x + └── linkbcs +``` + +### Configuration Options + +Key configuration parameters: + +```yaml +# Location of GEOS HOMDIR (model settings and RC files) +geos_homdir: /path/to/geos/homdir + +# Is EXPDIR different from HOMDIR? +geos_expdir_different: false + +# If true, specify EXPDIR location +geos_expdir: /path/to/geos/expdir + +# Method for obtaining initial restarts +initial_restarts_method: geos_expdir +``` + +## Preparing the Run Directory: `PrepCoupledGeosRunDir` + +The `PrepCoupledGeosRunDir` task configures the forecast directory for the current cycle. This task is executed before every forecast (not just the initial cycle). + +Some of the DA required setup is assumed to happen in the `geos_homdir` already as they are not handled by `gcm_setup` automatically. One critical component is including the `MOM_oda_incupd` file for MOM6 IAU configuration in the `forecast` directory. Most of this is described under the appropriate [model configurations](../../configuration_reference/model_configurations) page. + +### Main Operations + +#### 1. Copy Static Files + +The task copies required model configuration files from GEOS_homdir/GEOS_expdir: + +**Required files**: +- `AGCM.rc` - Atmosphere model configuration +- `CAP.rc` - Coupled model controller configuration +- `gcm_run.j` - Job submission script +- `HISTORY.rc` - History output configuration +- `fvcore_layout.rc` - FV3 core layout settings +- `input.nml` - Namelist inputs +- `ice_in` - CICE6 configuration +- `MOM_input` - MOM6 main configuration +- `MOM_override` - MOM6 parameter overrides +- `diag_table` - Diagnostic output table +- `data_table` - Data input table +- Other supporting files + +**Optional files** (if present): +- `MOM_oda_incupd` - MOM6 IAU (Incremental Analysis Update) configuration +- `MOM_saltrestore` - MOM6 salt restoring configuration (this is not recommended when SSS is assimilated) + +**Directories**: +- `RC/` - Complete resource configuration directory +- `GEOSgcm.x` - Model executable +- `linkbcs` - Boundary condition links + +#### 2. Modify Path Configurations in `gcm_run.j` + +The task updates directory paths in the job script to point to the current forecast directory: + +```python +with open(self.forecast_dir('gcm_run.j'), "r") as infile: + lines = infile.readlines() + +with open(self.forecast_dir('gcm_run.j'), "w") as outfile: + for line in lines: + # Update EXPDIR to current forecast directory + if re.match(r'^\s*setenv\s+EXPDIR\b', line): + outfile.write(f"setenv EXPDIR {self.forecast_dir()}\n") + # Update HOMDIR to current forecast directory + elif re.match(r'^\s*setenv\s+HOMDIR\b', line): + outfile.write(f"setenv HOMDIR {self.forecast_dir()}\n") + else: + outfile.write(line) +``` + +This ensures GEOS uses the experiment-specific directory rather than the original experiment directory. + +#### 3. Adjust Model Configuration Files + +**Background Frequency** (`ice_in` and `diag_table`): +```python +bkgr_freq = self.config.get_key_for_model('background_frequency', 'geos_marine', 'PT3H') +self.geos.process_icein(bkgr_freq) +self.geos.process_diag_table(bkgr_freq) +``` + +This allows GEOS to output backgrounds at the correct frequency for different DA window configurations without modifying the original experiment files. If no `background_frequency` is set (e.g., for 3DVar or LETKF) the default is `PT3H`. + +**MOM6 IAU Configuration**: + +If MOM6 IAU (Incremental Analysis Update) is enabled and a `mom6_increment.nc` file exists in the RESTART directory: + +```python +if self.config.get_key_for_model('mom6_iau', 'geos_marine', False): + if os.path.exists(self.forecast_dir('RESTART/mom6_increment.nc')): + # Augment MOM_input with MOM_oda_incupd configuration + # Set ODA_INCUPD_NHOURS based on configuration +``` + +This enables gradual application of analysis increments over the forecast window. + +**Cold Start vs. Warm Start**: +```python +self.geos.process_inputnml() +``` + +Modifies `input.nml` to indicate warm restart (default) or cold start. This is to make sure `n` is switched to `r` in `input.nml`. Otherwise, the model will bootstrap and initiate a cold start. + +#### 4. Modify Resource Configuration Files + +**CAP.rc** (Coupled model controller): +```python +self.cap_dict = self.rewrite_cap(self.cap_dict, self.forecast_dir('CAP.rc')) +``` + +Updates: +- `JOB_SGMT`: Segment duration matching `forecast_duration` +- `NUM_SGMT`: Set to 1 (run one segment per cycle) +- `END_DATE`: Set far into future to avoid premature termination + +Example modification for a PT12H forecast: +``` +NUM_SGMT: 1 +JOB_SGMT: 0000000 120000 # 12 hours in HHMMSS format +END_DATE: 50010101 000000 # Far future date +``` + +**AGCM.rc** (Atmosphere model): +```python +if 'RECORD_FREQUENCY' in self.agcm_dict: + self.rewrite_agcm(self.agcm_dict, self.forecast_dir('AGCM.rc')) +``` + +Updates restart record parameters: +- `RECORD_FREQUENCY`: Interval for writing restart checkpoints +- `RECORD_REF_DATE`: Reference date for restart output timing +- `RECORD_REF_TIME`: Reference time for restart output timing + +This ensures restarts are written at the DA window boundaries for seamless cycling. + +#### 5. Create `cap_restart` File + +Creates the `cap_restart` file with the forecast start time: + +```python +with open(self.forecast_dir('cap_restart'), 'w') as file: + file.write(self.fc_dto.strftime("%Y%m%d %H%M%S")) +``` + +Format: `YYYYMMDD HHMMSS` (e.g., `20210701 000000`) + +### Timing Calculation + +The task calculates the forecast start time based on the cycle time and forecast duration: + +```python +# Forecast starts 3/4 of forecast_duration before cycle time +# This accounts for the DA window offset +self.fc_dto = self.cycle_time_dto() - isodate.parse_duration(self.forecast_duration) * 3 / 4 +``` + +For example, with: +- `cycle_time`: 2021-07-01T12:00:00Z +- `forecast_duration`: PT12H +- Forecast starts at: 2021-07-01T03:00:00Z (9 hours before cycle time) + +**Note**: This part could be adjusted depending on the specific DA window configuration and forecast duration, however notice that there are forecast only suites without the DA parameters. + +## File Movement Between `forecast` and `scratch` Folders + +Understanding the data flow between directories is crucial for managing GEOS cycles in SWELL. + +### Directory Structure + +``` +{experiment_path}/GEOSgcm/ +└── forecast/ # Prepared run directory (cycle-specific) + ├── scratch/ # GEOS runtime output directory + │ ├── RESTART/ # Model restart files written during forecast + │ └── his_*.nc # History output files + ├── RESTART/ # Restart files for next forecast + ├── gcm_run.j # Job script + ├── *_rst # Atmosphere restart files + ├── CAP.rc, AGCM.rc # Configuration files + └── ... +``` + +### GEOS Output During Forecast + +When `gcm_run.j` executes, GEOS writes outputs to the `scratch/` subdirectory: + +1. **History Files**: `scratch/his_YYYY_MM_DD_HH.nc` (MOM6 ocean backgrounds) and `scratch/iceh_{hour_prefix}.{date_str}-{seconds:05d}.nc` (CICE6 sea ice) +2. **Restart Files**: + - `scratch/*_checkpoint` or `scratch/*_checkpoint.YYYYMMDD_HHMMz.nc4` (atmosphere) + - `scratch/RESTART/iced.nc` (CICE6 sea ice) + - `scratch/RESTART/MOM.res*.nc` (MOM6 ocean) + - `scratch/tile.bin` (tile interface file) +3. **History Restart Files**: `scratch/*.rcx` (for history file continuation) + +### Linking Outputs for JEDI: `LinkCoupledGeosOutput` + +After the forecast completes, the `LinkCoupledGeosOutput` task creates symbolic links from `scratch/` to the cycle directory for JEDI to access: + +For **3DVar** (single background): +- One ocean history file at the background time +- One CICE6 restart file one history file + +For **4D methods** (3DFGAT, 4DVar): +- Multiple ocean history files at different time slots +- Multiple CICE6 history files at different time slots and one restart file +- Based on `background_frequency` configuration + +### Moving Restart Files: `MoveDaRestart` + +The `MoveDaRestart` task moves restart files from `scratch/` to the forecast directory's `RESTART/` subdirectory for the next cycle: + +#### Atmosphere Restarts + +```python +# Move checkpoint files +src = self.forecast_dir(['scratch', '*_checkpoint']) +``` + +If `RECORD_FREQUENCY` is enabled in AGCM.rc, restarts have timestamps: +```python +# Time-stamped format +src = self.forecast_dir(['scratch', rst_dto.strftime('*_checkpoint.%Y%m%d_%H%Mz.nc4')]) +``` + +Examples: +- `fvcore_internal_checkpoint.20210701_0900z.nc4` +- `moist_internal_checkpoint.20210701_0900z.nc4` + +Files are moved and renamed (strip timestamp): +``` +scratch/fvcore_internal_checkpoint.20210701_0900z.nc4 → fvcore_internal_checkpoint +``` + +#### Ocean and Ice Restarts + +```python +# CICE6 restart +move_files(self.logger, + self.forecast_dir('scratch/RESTART/iced.nc'), + self.forecast_dir('RESTART/iced.nc')) + +# Tile interface file +move_files(self.logger, + self.forecast_dir('scratch/tile.bin'), + self.forecast_dir('tile.bin')) +``` + +#### MOM6 Multiple Restart Files + +MOM6 can write multiple restart files for high-resolution simulations. The task handles both single and multiple restart scenarios: + +```python +# Without RECORD_FREQUENCY +src = self.forecast_dir(['scratch', 'RESTART', 'MOM.res*nc']) + +# Time-stamped +# With RECORD_FREQUENCY active in AGCM.rc and #override RESTART_CONTROL = 2 set in MOM_override +rst_pattern = rst_dto.strftime('MOM.res_Y%Y_D%j_S') + seconds_str + '*.nc' +``` + +Examples of time-stamped MOM6 restarts: +- `MOM.res_Y2021_D182_S32400.nc` (main restart) +- `MOM.res_Y2021_D182_S32400_1.nc` (additional PE domain) +- `MOM.res_Y2021_D182_S32400_2.nc` (additional PE domain) + +These are renamed to remove the timestamp: +``` +scratch/RESTART/MOM.res_Y2021_D182_S32400.nc → RESTART/MOM.res.nc +scratch/RESTART/MOM.res_Y2021_D182_S32400_1.nc → RESTART/MOM.res_1.nc +``` + +#### MOM6 IAU Increment + +If MOM6 IAU is enabled, the increment file is also moved: + +```python +if self.mom6_iau: + move_files(self.logger, + os.path.join(self.cycle_dir(), 'mom6_increment.nc'), + self.forecast_dir(['RESTART', 'mom6_increment.nc'])) +``` + +This allows the next forecast to apply the analysis increment gradually. + +#### History Restart Files + +History restart files (`.rcx`) are moved to maintain continuity in GEOS HISTORY outputs: + +```python +rcx_files = os.path.join(self.forecast_dir('scratch'), '*.rcx') +for filepath in list(glob.glob(rcx_files)): + filename = os.path.basename(filepath) + dst_path = os.path.join(self.forecast_dir(), filename) + move_files(self.logger, filepath, dst_path) +``` + +### Complete File Flow Example + +Here's a complete example of file movement through one DA cycle: + +**Initial State** (Cycle 1 - 2021-07-01T12:00:00Z): +``` +forecast/ +├── RESTART/ +│ ├── iced.nc # From GetCoupledGeosRestart +│ ├── MOM.res.nc # From GetCoupledGeosRestart +│ └── MOM.res_1.nc +├── fvcore_internal_rst # From GetCoupledGeosRestart +├── moist_internal_rst +└── ... +``` + +**After PrepCoupledGeosRunDir**: +``` +forecast/ +├── RESTART/ # Previous restarts ready for forecast +├── gcm_run.j # Modified job script +├── CAP.rc # Modified for forecast duration +├── AGCM.rc # Modified with RECORD_FREQUENCY settings +├── ice_in # Modified for background frequency +├── MOM_input # Potentially augmented with MOM_oda_incupd +└── cap_restart # Created with forecast start time +``` + +**After RunGeos**: +``` +forecast/ +├── RESTART/ # Old restarts (still present) +├── scratch/ +│ ├── his_2021_07_01_03.nc # History at window begin (for DA window 6hr) +│ ├── his_2021_07_01_06.nc # History at mid-window (for DA window 6hr) +│ ├── his_2021_07_01_09.nc # History at window end (for DA window 6hr) +│ ├── fvcore_internal_checkpoint.20210701_0900z.nc4 +│ ├── moist_internal_checkpoint.20210701_0900z.nc4 +│ ├── RESTART/ +│ │ ├── iced.nc +│ │ ├── MOM.res_Y2021_D182_S32400.nc +│ │ └── MOM.res_Y2021_D182_S32400_1.nc +│ └── tile.bin +└── ... +``` + +**After LinkCoupledGeosOutput**: +``` +cycle_dir/ +├── ocn.bkg.2021-07-01T03:00:00Z.nc → ../forecast/scratch/his_2021_07_01_03.nc +├── ocn.bkg.2021-07-01T06:00:00Z.nc → ../forecast/scratch/his_2021_07_01_06.nc +├── ocn.bkg.2021-07-01T09:00:00Z.nc → ../forecast/scratch/his_2021_07_01_09.nc +└── iced.res.2021-07-01T09:00:00Z.nc → ../forecast/scratch/RESTART/iced.nc +``` + +**After MoveDaRestart** (preparing for next cycle): +``` +forecast/ +├── RESTART/ +│ ├── iced.nc # Moved from scratch/RESTART/ +│ ├── MOM.res.nc # Moved and renamed from scratch/RESTART/ +│ ├── MOM.res_1.nc # Moved and renamed from scratch/RESTART/ +│ └── mom6_increment.nc # Moved from cycle_dir/ (if IAU enabled) +├── fvcore_internal_checkpoint # Moved and renamed from scratch/ +├── moist_internal_checkpoint # Moved and renamed from scratch/ +├── tile.bin # Moved from scratch/ +└── *.rcx # Moved from scratch/ +``` + +The cycle then repeats for the next analysis time. + +## Task Sequence in the Workflow + +The complete task sequence for a typical DA cycle is: + +```mermaid +graph TD + A[MoveDaRestart from previous cycle] --> B[PrepCoupledGeosRunDir] + B --> C[RunGeos] + C --> D[LinkCoupledGeosOutput] + D --> E[Run JEDI Analysis] + E --> F[PrepareAnalysis] + F --> G[SaveRestart] + G --> H[MoveDaRestart] + H --> I[CleanCycle] + I --> J[Next cycle PrepCoupledGeosRunDir] +``` + +**Cycle N-1 to Cycle N**: +1. `MoveDaRestart[-window_length]`: Move analysis restarts from previous cycle +2. `PrepCoupledGeosRunDir`: Configure forecast directory for current cycle +3. `RunGeos`: Execute `gcm_run.j` to run forecast +4. `LinkCoupledGeosOutput`: Link model outputs for JEDI +5. `GetObservations`: Retrieve observations for current cycle +6. `RunJediFgatExecutable`: Run JEDI analysis +7. `PrepareAnalysis`: Prepare analysis for next forecast +8. `SaveRestart`: Save analysis state to R2D2 (optional) +9. `MoveDaRestart`: Move analysis restarts to next cycle directory +10. `CleanCycle`: Remove large intermediate files + +## Configuration Tips + +### Configuring Window Length and Forecast Duration + +The relationship between window length and forecast duration is important: + +```yaml +forecast_duration: PT12H # Total forecast length + +models: + geos_marine: + window_length: PT6H # DA window length +``` + +For a 6-hour DA window with 12-hour forecast: +- Forecast runs from T-9h to T+3h (where T is cycle time) +- DA window is from T-3h to T+3h +- GEOS outputs backgrounds within the window + +### Configuring Background Frequency + +For 4D methods (3DFGAT): + +```yaml +models: + geos_marine: + background_frequency: PT1H # Output backgrounds every hour + window_type: 4D +``` + +This generates backgrounds at multiple time slots within the DA window. + +### Configuring MOM6 IAU + +For incremental analysis update: + +```yaml +models: + geos_marine: + mom6_iau: true + mom6_iau_nhours: PT6H # Apply increment over 6 hours +``` + +### Configuring AGCM Restart Record and MOM_override + +To enable time-stamped restarts for precise DA window alignment: + +In your GEOS HOMDIR, edit `AGCM.rc`: +``` +RECORD_FREQUENCY: 060000 # Write restarts every 6 hours +RECORD_REF_DATE: 20210701 # Reference date (updated by SWELL) +RECORD_REF_TIME: 090000 # Reference time (updated by SWELL) +``` + +SWELL will automatically update `RECORD_REF_DATE` and `RECORD_REF_TIME` to match the DA window boundaries. + +Add in your `MOM_override`: + +``` +#override RESTART_CONTROL = 2 +``` + +## Common Issues and Solutions + +### Issue: Restarts at Wrong Time + +**Symptom**: Restarts don't align with DA window boundaries + +**Solution**: Enable `RECORD_FREQUENCY` in AGCM.rc and ensure `forecast_duration` is longer than `window_length` + +### Issue: MOM6 Increment Not Applied + +**Symptom**: Analysis has minimal impact on subsequent forecasts + +**Solution**: +1. Check that `mom6_iau: true` in experiment.yaml +2. Verify `mom6_increment.nc` exists in cycle directory after JEDI analysis +3. Check that `MOM_oda_incupd` file is being augmented to `MOM_input` + +### Issue: Missing History Files + +**Symptom**: JEDI cannot find background files + +**Solution**: +1. Verify `diag_table` and `ice_in` includes ocean and sea-ice history collections +2. Check that history output frequency matches `background_frequency` +3. Ensure `LinkCoupledGeosOutput` completes successfully before JEDI + +### Issue: Scratch Directory Fills Up + +**Symptom**: Disk quota exceeded errors + +**Solution**: +1. Ensure `CleanCycle` task runs after analysis completes +2. Check that `MoveDaRestart` successfully moves files out of scratch +3. Consider reducing HISTORY output frequency if not needed for DA +4. Make sure R2D2 scrubber is active (this is work in progress!) + +## Summary + +The GEOS cycling workflow in SWELL is designed to seamlessly integrate coupled model forecasts with JEDI data assimilation: + +- **Initial Setup**: `GetCoupledGeosRestart` obtains restart files from various sources +- **Cycle Preparation**: `PrepCoupledGeosRunDir` configures the forecast directory with proper paths, timings, and model configurations +- **Forecast Execution**: `gcm_run.j` runs the coupled GEOS model, writing outputs to `scratch/` +- **Output Management**: `LinkCoupledGeosOutput` makes model backgrounds accessible to JEDI +- **Restart Management**: `MoveDaRestart` relocates analysis restarts to prepare for the next cycle +- **Cleanup**: `CleanCycle` removes large intermediate files to manage disk space + +This modular approach allows for flexible DA cycling with various window configurations, IAU options, and model resolutions. From 8244994028c052be4b64c5f0f18ad9153405f245 Mon Sep 17 00:00:00 2001 From: dooruk Date: Wed, 26 Aug 2026 16:20:24 -0400 Subject: [PATCH 13/13] comment --- src/swell/tasks/prep_coupled_geos_run_dir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index 31b3220a0..a4aa50237 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -24,8 +24,8 @@ class PrepCoupledGeosRunDir(taskBase): def execute(self) -> None: """ - Copies GEOS HOMDIR files to the GEOSfcm forecast directory and prepares it before executing - gcm_run.j. + Copies GEOS HOMDIR files to the GEOSgcm forecast directory and prepares it before executing + `gcm_run.j`. Modifies certain resource files using python's re package according to cycle_date such as: CAP.rc, AGCM.rc, input.nml, and gcm_run.j.