diff --git a/docs/adding_a_suite.md b/docs/adding_a_suite.md index 9a424f954..5471edc27 100644 --- a/docs/adding_a_suite.md +++ b/docs/adding_a_suite.md @@ -122,7 +122,7 @@ The `root` section defines actions and variables shared by all tasks. Note that ## How the experiment is created -When an experiment is created using `swell create `, a dictionary of questions is pieced together from questions associated with the suite and its member tasks. Answers for these questions are set either from default configurations, from user input on the command line, or overridden from a specified file. In a complex process, the answers provided are then used to generate the `experiment.yaml` and the experiment's `flow.cylc file. +When an experiment is created using `swell create `, a dictionary of questions is pieced together from questions associated with the suite and its member tasks. Answers for these questions are set either from default configurations, from user input on the command line, or overridden from a specified file. In a complex process, the answers provided are then used to generate the `experiment.yaml` and the experiment's `workflow.py` file. # Creating a Suite @@ -145,19 +145,20 @@ Creating visualizations such as flowcharts may help in designing workflows. In practice, there are three major steps towards creating a suite. Completing all of these steps is necessary to make the suite work, so these steps will likely be done iteratively/non-linearly: 1. Write the tasks. -2. Create the `flow.cylc` file. +2. Create the `workflow.py` file. 3. Add the appropriate suite and task question lists. More detailed instructions and examples for these steps follows in this section. ### Writing tasks -Swell has a variety of tasks, many of which are shared across suites. Tasks in Swell are defined as classes which extend the `taskBase` parent class, which has many helpful functions and attributes. When a task is run by swell, it calls the `execute` function. +Swell has a variety of tasks, many of which are shared across suites. Tasks in Swell are defined as classes which extend the `taskBase` parent class, which has many helpful functions and attributes. When a task is run by swell, it calls the `execute` function. Information on how to run the task and the parameters the task needs are specified in the `TaskSetup` class. Calls to parameters are made using either functions of `taskBase`, for more common parameters, or using `self.config.`. ### Example Swell Task ```python + class CloneGeosMksi(taskBase): def execute(self) -> None: @@ -179,32 +180,34 @@ class CloneGeosMksi(taskBase): ``` This example shows the basics of writing a task, including task definition and the execute function. The current model is accessed by the `self.get_model()` function, inherited from `taskBase`. The variables `path_to_geos_mksi`, and `tag`, are pulled from the experiment configuration, which is sourced from the `experiment.yaml`. -Tasks that have a slurm requirement need to be specified in `src/swell/utilities/slurm.py`. +The `TaskSetup` class informs swell how to construct the task's cylc parameters, as well as the questions that are used by the task. The `TaskSetup` class for `CloneGeosMksi` looks like the following: -For debugging purposes, it may be easier to first create and test some tasks outside of Swell, and then port them to Swell by changing relevant variables and path specifications. Alternatively, `experiment.yaml` can be populated manually and tested using `swell task experiment.yaml`. +```python -### Creating the flow.cylc template +task_name = 'CloneGeosMksi' +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_attributes(self): + self.base_name = task_name + self.model_dep = True + self.questions = [ + qd.observing_system_records_mksi_path(), + qd.observing_system_records_mksi_path_tag() + ] -For more detailed information on cylc workflows, see the [cylc documentation](https://cylc.github.io/cylc-doc/latest/html/index.html). Existing Swell suite workflows can also provide useful examples to consider. +``` -Suite workflows are stored in `src/swell/suites/`. +The `set_attributes` abstract method is used to set values for the class. The `self.questions` list sets needed question parameters for the config. Slurm requirements are also set in the the `TaskSetup`. For more information, see documentation in `src/swell/tasks/base/task_setup.py`. -The experiment `flow.cylc` file is generated from a suite template using a `jinja2` process. For example, here is part of a suite template, versus a filled-in experiment `flow.cylc`. During creation, specified questions are used to fill in the template: +For debugging purposes, it may be easier to first create and test some tasks outside of Swell, and then port them to Swell by changing relevant variables and path specifications. Alternatively, `experiment.yaml` can be populated manually and tested using `swell task experiment.yaml`. -``` -[scheduling] +### Creating the flow.cylc template - initial cycle point = {{start_cycle_point}} - final cycle point = {{final_cycle_point}} - runahead limit = {{runahead_limit}} -``` -``` -[scheduling] +For more detailed information on cylc workflows, see the [cylc documentation](https://cylc.github.io/cylc-doc/latest/html/index.html). Existing Swell suite workflows can also provide useful examples to consider. - initial cycle point = 2021-07-01T12:00:00Z - final cycle point = 2021-07-01T12:00:00Z - runahead limit = P4 -``` +Suite workflows are stored in `src/swell/suites//workflow.py`. + +The experiment `flow.cylc` file is generated from a suite template. This is handled through the `CylcWorkflow` task and generally consists of two steps, setting the graph and iterating through the tasks to generate. For more information, see the documentation for `CylcWorkflow` under `src/swell/utilities/cylc_workflow.py` and example suites. For initial development/testing purposes, it may be easier to create a `flow.cylc` using hard-coded values, then replace these with `jinja2` templated values as the suite nears completion. @@ -212,7 +215,7 @@ For initial development/testing purposes, it may be easier to create a `flow.cyl ### Question Objects -Questions for swell are stored as dataclass instances, in the file `src/swell/utilities/question_defaults.py`. Dataclasses allow for simple declaration of data fields, and powerful type checking capabilities. Each question is an extension of the `SuiteQuestion` or `TaskQuestion` class, which are extensions of the `SwellQuestion` parent: +Questions for swell are stored as dataclass instances, in the file `src/swell/configuration/question_defaults.py`. Dataclasses allow for simple declaration of data fields, and powerful type checking capabilities. Each question is an extension of the `SuiteQuestion` or `TaskQuestion` class, which are extensions of the `SwellQuestion` parent: ```python @dataclass @@ -274,121 +277,75 @@ question = existing_jedi_build_directory(options=['example1', 'example2']) Each individual suite and most tasks have an associated list of questions which are used to create the experiment. Suite question lists are stored in `src/swell/suites//suite_config.py` -Task question lists are stored in `src/swell/tasks/task_questions.py` - -`QuestionList` objects store and handle questions in an object-oriented manner. They can store questions directly, or store other lists to use their questions. Here is an example of a question list for a task: +Task question lists are stored in `src/swell/tasks/.py` -```python - BuildJediByLinking = QuestionList( - list_name="BuildJediByLinking", - questions=[ - qd.existing_jedi_build_directory(), - qd.existing_jedi_build_directory_pinned(), - qd.jedi_build_method() - ] - ) -``` +`QuestionList` objects store and handle questions in an object-oriented manner. They can store questions directly, or store other lists to use their questions. -During experiment creation, Swell scans the suite's `flow.cylc` file to find all of the tasks used in the workflow. It then finds the corresponding task lists in `src/swell/tasks/task_questions.py`, and fits together a list of uniquely named questions from all of the lists. Questions have a priority depending on order. In the case of duplicate questions, those further DOWN the list take priority. For this reason, it is NOT RECOMMENDED to set different default values for tasks in `task_questions.py`, since questions may be overridden by a questions in a different task. +During experiment creation, Swell consults the suite's `workflow.py` file to find all of the tasks used in the workflow. It then finds the corresponding task lists in the `TaskSetup` object located in `src/swell/tasks/.py`, and fits together a list of uniquely named questions from all of the lists. Questions have a priority depending on order. In the case of duplicate questions, those further DOWN the list take priority. For this reason, it is NOT RECOMMENDED to set different default values for questions listed in tasks, since questions may be overridden by a questions in a different task. In this question infrastructure, **suites take priority over tasks**. Any question specified in a suite configuration will override the default value for a question in one of its member tasks. This allows for easily setting different configurations for suites without having to specify redundant questions. For ease of use, model-dependent questions can be assigned directly in their respective lists. Consider the following example of suite questions for `3dvar_marine` (in python, variable names cannot begin with digits): -```python -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -class SuiteQuestions(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - # Shared groups of questions across suites - # -------------------------------------------------------------------------------------------------- - - all_suites = QuestionList( - list_name="all_suites", - questions=[ - qd.experiment_id(), - qd.experiment_root() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - common = QuestionList( - list_name="common", - questions=[ - all_suites, - qd.cycle_times(), - qd.start_cycle_point(), - qd.final_cycle_point(), - qd.model_components(), - qd.runahead_limit() - ] - ) - - # -------------------------------------------------------------------------------------------------- - marine = QuestionList( - list_name="marine", - questions=[ - common, - qd.marine_models() - ] - ) -``` +```python -```python -class SuiteConfig(QuestionContainer, Enum): +from swell.suites.base.all_suites import suite_configs +from swell.suites.base.suite_questions import marine + +_3dvar_marine_base = QuestionList( + list_name="3dvar_marine_base", + questions=[ + marine + ] +) + +suite_configs.register('3dvar_marine', '3dvar_marine_base', _3dvar_marine_base) + +# -------------------------------------------------------------------------------------------------- + +_3dvar_marine_tier1 = QuestionList( + list_name="3dvar_marine_tier1", + questions=[ + _3dvar_base, + qd.start_cycle_point("2021-07-01T12:00:00Z"), + qd.final_cycle_point("2021-07-01T12:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_marine']), + ], + geos_marine=[ + qd.cycle_times(['T12']), + qd.marine_models(['mom6']), + qd.window_length("P1D"), + qd.horizontal_resolution("72x36"), + qd.vertical_resolution("50"), + qd.total_processors(6), + qd.obs_experiment("s2s_v1"), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "insitu_profile_argo", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_gmi_l3u", + "sst_viirs_n20_l3u", + "temp_profile_xbt" + ]), + qd.obs_provider(['odas', 'gdas_marine']), + qd.background_time_offset("PT18H"), + qd.clean_patterns(['*.nc4', '*.txt']), + ] +) + +suite_configs.register('3dvar_marine', '3dvar_marine_tier1', _3dvar_marine_tier1) - _3dvar_marine_base = QuestionList( - list_name="3dvar_marine_base", - questions=[ - sq.marine - ] - ) - - # -------------------------------------------------------------------------------------------------- - - _3dvar_marine_tier1 = QuestionList( - list_name="3dvar_marine_tier1", - questions=[ - _3dvar_base, - qd.start_cycle_point("2021-07-01T12:00:00Z"), - qd.final_cycle_point("2021-07-01T12:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_marine']), - ], - geos_marine=[ - qd.cycle_times(['T12']), - qd.marine_models(['mom6']), - qd.window_length("P1D"), - qd.horizontal_resolution("72x36"), - qd.vertical_resolution("50"), - qd.total_processors(6), - qd.obs_experiment("s2s_v1"), - qd.observations([ - "adt_cryosat2n", - "adt_jason3", - "adt_saral", - "adt_sentinel3a", - "adt_sentinel3b", - "insitu_profile_argo", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_gmi_l3u", - "sst_viirs_n20_l3u", - "temp_profile_xbt" - ]), - qd.obs_provider(['odas', 'gdas_marine']), - qd.background_time_offset("PT18H"), - qd.clean_patterns(['*.nc4', '*.txt']), - ] - ) ``` -The class `SuiteQuestions` contains lists of questions which are common to many suites. This avoids the need for redundantly setting the same questions for every suite. +The file `suite_questions.py` contains lists of questions which are common to many suites. This avoids the need for redundantly setting the same questions for every suite. -`_3dvar_marine_base` is responsible for establishing the baseline for questions used by the suite. The 'base' list should be used to associate all questions used by the suite. This list will be populated with the questions that match the defaults in `QuestionDefaults` (`src/swell/utilities/question_defaults.py`). However, in many cases, those defaults will not be ideal defaults for the individual suite. Thus, `_3dvar_marine_tier1` sets different default values which override the question defaults. If desired, other configurations can then inherit question defaults from `_3dvar_marine_tier1`, and set their own defaults on top of the existing ones. +`_3dvar_marine_base` is responsible for establishing the baseline for questions used by the suite. The 'base' list should be used to associate all questions used by the suite. This list will be populated with the questions that match the defaults in `question_defaults.py` (`src/swell/configuration/question_defaults.py`). However, in many cases, those defaults will not be ideal defaults for the individual suite. Thus, `_3dvar_marine_tier1` sets different default values which override the question defaults. If desired, other configurations can then inherit question defaults from `_3dvar_marine_tier1`, and set their own defaults on top of the existing ones. diff --git a/docs/examples/templating_workflows.md b/docs/examples/templating_workflows.md new file mode 100644 index 000000000..0ccc29c18 --- /dev/null +++ b/docs/examples/templating_workflows.md @@ -0,0 +1,109 @@ +# Templating cylc workflows within swell +The `flow.cylc` file informs the `Cylc` workflow engine on how to run an experiment. This includes the order in which tasks should be run, and the scripts and environment variables necessary for each task. Templating a workflow within Swell previously used `jinja2` templating on a file named `flow.cylc` under each suite. This has been replaced with an approach that uses a python class to manipulate strings to generated the `flow.cylc` used in the experiment. This allows for more complex logic to be performed in generating the workflow, but also may be confusing to users. This documentation serves to explain the new method of templating workflows under these changes. + +## Cylc sections + +The `flow.cylc` that is generated under this method is not much different from the one generated before, and users shouldn't notice a difference when it comes to creating an experiment, using overrides, etc. When creating an experiment, `swell` consults a file `src/swell/suites//workflow.py` on how to construct the suite. This file should be an extension of the `CylcWorkflow` class (defined in `src/swell/suites/base/cylc_workflow.py`). The method `get_workflow_string` is called to return a string which fills the contents of the `flow.cylc` file. Overriding this method is used to manually specify the contents of the file. Typically, the graph section is templated in `jinja2`, and the runtime sections for each task are generated using swell's `TaskSetup` class. However, the entire `flow.cylc` file can be templated in `jinja`, if necessary. + + +## Tasks and the runtime section + +Swell will parse the graph section, which is constructed first, to obtain the tasks which are used by the experiment. It will then build the runtime section by consulting task setup objects. Since swell tasks broadly fall into only a few categories (model-dependent or independent, cycling or non-cycling) that do not differ much between suites, they are easily abstracted into a `TaskSetup` class. This class will dynamically set attributes such as messaging parameters and slurm settings. Each task has an associated `TaskSetup` class, which is defined in the main task file, and registered into the `TaskAttributes` container. For example, the following displays the `TaskSetup` class for `CloneJedi`, located in `src/swell/tasks/clone_jedi.py`: + +```python + +task_name = 'CloneJedi' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_attributes(self): + self.base_name = task_name + self.questions = [ + qd.bundles(), + qd.existing_jedi_source_directory(), + qd.existing_jedi_source_directory_pinned(), + qd.jedi_build_method() + ] +``` + +Other tasks have different requirements, such as `EvaObservations`: + +```python +task_name = 'EvaObservations' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_attributes(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {} + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.marine_models(), + qd.observing_system_records_path(), + qd.window_length(), + qd.marine_models(), + ] +``` + +Attributes are set by override the `set_attributes` method in `TaskSetup`. This has been combined with the previously-used `task_questions.py` for simplicity. + +The tags `is_cycling` and `model_dep` (both `False` by default) modify the script command (`swell task $config`): + +- `is_cycling = True` adds `-d $datetime` for cycling tasks +- `model_dep = True` adds `-m {model}` to indicate model-specific tasks. + +The `slurm` attribute determines where or not the task requires Slurm and provides a way to set task-specific overrides: + +- `slurm = None` means the task is not a Slurm task, so no `[[[directives]]]` section will be written. +- `slurm = {}` means the task *is* a Slurm task, so the `[[[directives]]]` will be populated according to the platform's default slurm settings (in `src/swell/deployment/platforms`) along with user-specific overrides. +- `slurm = {}` will optionally override the platform defaults with task-specific ones (but note that *user-configured overrides always have the highest priority*). + +For the task specification above for `EvaObservations`, the runtime section will be renderend as the following: + +``` +[[EvaObservations-geos_marine]] + script = "swell task EvaObservations $config -d $datetime -m geos_marine" + platform = nccs_discover_sles15 + execution time limit = PT30M + [[[directives]]] + --job-name = EvaObservations-geos_marine + --qos = allnccs + --nodes = 1 + --ntasks-per-node = 64 + --constraint = mil + --no-requeue = + --account = +``` + +This can be used to set task-specific defaults in `task_attributes.py`, rather than being set in `slurm.py`. For example, the task below defaults to slurm setting `--nodes=1`. + +```python +class RunJediConvertStateSoca2ciceExecutable(TaskSetup): + def set_attributes(self): + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {'nodes': 1} +``` + +This supports setting platform-specific overrides, for example: + +```python +class RunJediConvertStateSoca2ciceExecutable(TaskSetup): + def set_attributes(self): + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {'all': 1, + 'nccs_discover_cascade': 2} +``` + +On the `nccs_discover_cascade` platform, `nodes` will be set as 2, but on any other platform it will be 1. User overrides will still work as they did previously. diff --git a/pyproject.toml b/pyproject.toml index d088330f1..936d8a26f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ dependencies = [ "flake8==6.1.0", "netCDF4", "ruamel.yaml==0.17.16", - "xarray==2024.7.0" + "xarray==2024.7.0", + "requests>=2.23.0" ] [project.optional-dependencies] diff --git a/src/swell/configuration/jedi/interfaces/geos_atmosphere/model/stage_cycle.py b/src/swell/configuration/jedi/interfaces/geos_atmosphere/model/stage_cycle.py index 8f90e1a71..d95b3fb25 100644 --- a/src/swell/configuration/jedi/interfaces/geos_atmosphere/model/stage_cycle.py +++ b/src/swell/configuration/jedi/interfaces/geos_atmosphere/model/stage_cycle.py @@ -11,7 +11,7 @@ # -------------------------------------------------------------------------------------------------- -def stage_cycle(template_dict: Mapping) -> Mapping: +def stage_cycle(template_dict: Mapping) -> list: cycle_dir = template_dict['cycle_dir'] swell_static_files = template_dict['swell_static_files'] diff --git a/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml b/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml index a39e91c8c..80e7e5ee8 100644 --- a/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml @@ -2,6 +2,10 @@ cycle_times: default_value: ['T00', 'T06', 'T12', 'T18'] options: ['T00', 'T06', 'T12', 'T18'] +cycling_varbc: + default_value: false + options: [true, false] + ensemble_hofx_strategy: default_value: 'serial' diff --git a/src/swell/configuration/jedi/interfaces/geos_cf/model/getvalues.py b/src/swell/configuration/jedi/interfaces/geos_cf/model/getvalues.py index 671d3eee1..db8f1d1f2 100644 --- a/src/swell/configuration/jedi/interfaces/geos_cf/model/getvalues.py +++ b/src/swell/configuration/jedi/interfaces/geos_cf/model/getvalues.py @@ -6,13 +6,12 @@ # -------------------------------------------------------------------------------------------------- -from typing import Optional from collections.abc import Mapping # -------------------------------------------------------------------------------------------------- -def getvalues(template_dict: Mapping) -> Optional[Mapping]: +def getvalues(template_dict: Mapping) -> Mapping | None: getvalues = None return getvalues diff --git a/src/swell/configuration/jedi/interfaces/geos_cf/model/stage_cycle.py b/src/swell/configuration/jedi/interfaces/geos_cf/model/stage_cycle.py index c2a490247..78ff2c69e 100644 --- a/src/swell/configuration/jedi/interfaces/geos_cf/model/stage_cycle.py +++ b/src/swell/configuration/jedi/interfaces/geos_cf/model/stage_cycle.py @@ -11,7 +11,7 @@ # -------------------------------------------------------------------------------------------------- -def stage_cycle(template_dict: Mapping) -> Mapping: +def stage_cycle(template_dict: Mapping) -> Mapping | list: cycle_dir = template_dict['cycle_dir'] swell_static_files = template_dict['swell_static_files'] diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/model/getvalues.py b/src/swell/configuration/jedi/interfaces/geos_marine/model/getvalues.py index 671d3eee1..db8f1d1f2 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/model/getvalues.py +++ b/src/swell/configuration/jedi/interfaces/geos_marine/model/getvalues.py @@ -6,13 +6,12 @@ # -------------------------------------------------------------------------------------------------- -from typing import Optional from collections.abc import Mapping # -------------------------------------------------------------------------------------------------- -def getvalues(template_dict: Mapping) -> Optional[Mapping]: +def getvalues(template_dict: Mapping) -> Mapping | None: getvalues = None return getvalues diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/model/stage_cycle.py b/src/swell/configuration/jedi/interfaces/geos_marine/model/stage_cycle.py index d388cb046..fa4bbb3d1 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/model/stage_cycle.py +++ b/src/swell/configuration/jedi/interfaces/geos_marine/model/stage_cycle.py @@ -11,7 +11,7 @@ # -------------------------------------------------------------------------------------------------- -def stage_cycle(template_dict: Mapping) -> Mapping: +def stage_cycle(template_dict: Mapping) -> Mapping | list: swell_static_files = template_dict['swell_static_files'] cycle_dir = template_dict['cycle_dir'] horizontal_resolution = template_dict['horizontal_resolution'] diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/model/states.py b/src/swell/configuration/jedi/interfaces/geos_marine/model/states.py index 0fd02f537..e432315a4 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/model/states.py +++ b/src/swell/configuration/jedi/interfaces/geos_marine/model/states.py @@ -11,7 +11,7 @@ # -------------------------------------------------------------------------------------------------- -def states(template_dict: Mapping) -> Mapping: +def states(template_dict: Mapping) -> Mapping | list: experiment_id = template_dict['experiment_id'] analysis_time_iso = template_dict['analysis_time_iso'] diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml index 65361ae5a..dabd42400 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml @@ -2,6 +2,10 @@ cycle_times: default_value: ['T00', 'T12'] options: ['T00', 'T12'] +cycling_varbc: + default_value: false + options: [true, false] + ensemble_hofx_strategy: default_value: 'serial' @@ -16,3 +20,13 @@ saber_outer_block: skip_ensemble_hofx: default_value: true + +marine_models: + default_value: + - mom6 + - cice6 + options: + - mom6 + - cice6 + - bgc + - ww3 diff --git a/src/swell/configuration/jedi/interfaces/geos_marine/task_questions.yaml b/src/swell/configuration/jedi/interfaces/geos_marine/task_questions.yaml index 6f87658a1..a9f2f2c5d 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/task_questions.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/task_questions.yaml @@ -58,16 +58,6 @@ jedi_forecast_model: options: - NA -marine_models: - default_value: - - mom6 - - cice6 - options: - - mom6 - - cice6 - - bgc - - ww3 - minimizer: default_value: RPCG options: diff --git a/src/swell/configuration/question_defaults.py b/src/swell/configuration/question_defaults.py new file mode 100644 index 000000000..d890274e5 --- /dev/null +++ b/src/swell/configuration/question_defaults.py @@ -0,0 +1,1958 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + + +from dataclasses import dataclass +from typing import Literal + +from swell.utilities.swell_questions import SuiteQuestion, TaskQuestion +from swell.utilities.swell_questions import WidgetType as WType +from swell.utilities.dataclass_utils import mutable_field + +# -------------------------------------------------------------------------------------------------- +# Suite question defaults go here +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class comparison_experiment_paths(SuiteQuestion): + default_value: list = mutable_field([]) + question_name: str = "comparison_experiment_paths" + ask_question: bool = True + prompt: str = "Provide paths to two experiments to run comparison tests on." + widget_type: WType = WType.STRING_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class cycle_times(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "cycle_times" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Enter the cycle times for this model." + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class cycling_varbc(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "cycling_varbc" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Do you want to use cycling VarBC option?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class download_convert_pipeline(SuiteQuestion): + default_value: bool = False + question_name: str = "download_convert_pipeline" + ask_question: bool = False + prompt: str = ("Run the DownloadObs and ConvertObsToIoda tasks?" + "(DownloadObs -> ConvertObsToIoda) -> IngestObs to R2D2") + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class email_address(SuiteQuestion): + default_value: str = "defer_to_user" + question_name: str = "email_address" + prompt: str = "What email address should cylc messages be sent to?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensemble_hofx_packets(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "ensemble_hofx_packets" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Enter the number of ensemble packets." + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensemble_hofx_strategy(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "ensemble_hofx_strategy" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Enter the ensemble hofx strategy." + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class experiment_id(SuiteQuestion): + default_value: str = "defer_to_code" + question_name: str = "experiment_id" + ask_question: bool = True + prompt: str = "What is the experiment id?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class experiment_root(SuiteQuestion): + default_value: str = "defer_to_platform" + question_name: str = "experiment_root" + ask_question: bool = True + prompt: str = ("What is the experiment root (the directory where the " + "experiment will be stored)?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class final_cycle_point(SuiteQuestion): + default_value: str = "2023-10-10T06:00:00Z" + question_name: str = "final_cycle_point" + ask_question: bool = True + prompt: str = "What is the time of the final cycle (middle of the window)?" + widget_type: WType = WType.ISO_DATETIME + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class marine_models(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "marine_models" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_marine" + ]) + prompt: str = "Select the active SOCA models for this model." + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class mock_experiment(SuiteQuestion): + default_value: bool = False + question_name: str = "mock_experiment" + ask_question: bool = False + prompt: str = "Dry-run option for comparing configs." + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class model_components(SuiteQuestion): + default_value: str = "defer_to_code" + question_name: str = "model_components" + ask_question: bool = True + options: str = "defer_to_code" + prompt: str = "Enter the model components for this model." + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class parser_options(SuiteQuestion): + default_value: list = mutable_field(['fgrep_residual_norm']) + question_name: str = "parser_options" + ask_question: bool = True + options: list = mutable_field(['fgrep_residual_norm']) + prompt: str = "list the test types to run on the JEDI oops log." + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class r2d2_datastore(SuiteQuestion): + default_value: str | None = None + question_name: str = "r2d2_datastore" + ask_question: bool = False + prompt: str = ( + "Datastore name passed to R2D2 fetch and store operations " + "(e.g. a Discover directory store or an S3 bucket store). " + "Run scripts/discover_r2d2_datastores.py to list available datastores. " + "Leave empty to let R2D2 pick the highest-priority writable datastore " + "for your compute host." + ) + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class r2d2_experiment_id(SuiteQuestion): + default_value: str = "defer_to_code" + question_name: str = "r2d2_experiment_id" + prompt: str = "What experiment_id should r2d2 reference for experiment?" + widget_type: WType = WType.STRING + +# ------------------------------------------------------------------------------------------------- + + +@dataclass +class r2d2_server(SuiteQuestion): + default_value: str | None = None + question_name: str = "r2d2_server" + ask_question: bool = False + prompt: str = ( + "Server/profile name in ~/.swell/r2d2_credentials.yaml " + "(e.g. 'gmao_server'). Leave empty if credentials are at the root level." + ) + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class runahead_limit(SuiteQuestion): + default_value: str = "P4" + question_name: str = "runahead_limit" + ask_question: bool = True + prompt: str = ("Set the Cylc runahead limit: the maximum number of cycles " + "that may be active ahead of the current cycle " + "(e.g. P1: up to 1 cycle ahead, P3: up to 3 cycles ahead, default P4).") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class saber_central_block(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "saber_central_block" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which saber central block do you want to use?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class saber_outer_block(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "saber_outer_block" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which saber outer blocks do you want to use?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class skip_ensemble_hofx(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "skip_ensemble_hofx" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Enter if skip ensemble hofx." + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class skip_r2d2(SuiteQuestion): + default_value: bool = False + question_name: str = "skip_r2d2" + prompt: str = "Skip registering and storing results of this experiment in R2D2?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class start_cycle_point(SuiteQuestion): + default_value: str = "2023-10-10T00:00:00Z" + question_name: str = "start_cycle_point" + ask_question: bool = True + prompt: str = "What is the time of the first cycle (middle of the window)?" + widget_type: WType = WType.ISO_DATETIME + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class use_cycle_dir(SuiteQuestion): + default_value: bool = True + question_name: str = "use_cycle_dir" + ask_question: bool = False + prompt: str = ("For cycling tasks, send results to the experiment cycle directory?" + " If false, results will be stored in the current working directory.") + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class window_type(SuiteQuestion): + default_value: str = "defer_to_model" + question_name: str = "window_type" + options: list[str] = mutable_field([ + "3D", + "4D" + ]) + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Enter the window type for this model." + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- +# Task question defaults go here +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class analysis_variables(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "analysis_variables" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What are the analysis variables?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class background_error_model(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "background_error_model" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which background error model do you want to use?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class background_experiment(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "background_experiment" + ask_question: bool = True + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the name of the name of the experiment providing the backgrounds?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class background_frequency(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "background_frequency" + models: list[str] = mutable_field([ + "all_models" + ]) + depends: dict = mutable_field({ + "window_type": "4D" + }) + prompt: str = "What is the frequency of the background files?" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class background_time_offset(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "background_time_offset" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = ("How long before the middle of the analysis window did" + " the background providing forecast begin?") + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class bufr_obs_classes(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "bufr_obs_classes" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What BUFR observation classes will be used?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class bundles(TaskQuestion): + default_value: list[str] = mutable_field([ + "fv3-jedi", + "soca", + "iodaconv", + "ufo" + ]) + question_name: str = "bundles" + ask_question: bool = True + options: list[str] = mutable_field([ + "fv3-jedi", + "soca", + "iodaconv", + "ufo", + "ioda", + "oops", + "saber" + ]) + depends: dict = mutable_field({ + "jedi_build_method": "create" + }) + prompt: str = "Which JEDI bundles do you wish to build?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +class cache_fetch(TaskQuestion): + default_value: bool = True + question_name: str = "cache_fetch" + options: list[bool] = mutable_field([ + True, + False + ]) + prompt: str = "Use cached observation files if they already exist?" + widget_type: WType = WType.BOOLEAN + + # -------------------------------------------------------------------------------------------------- + + +@dataclass +class check_for_obs(TaskQuestion): + default_value: bool = True + question_name: str = "check_for_obs" + options: list[bool] = mutable_field([True, False]) + models: list[str] = mutable_field([ + 'all_models' + ]) + prompt: str = "Perform check for observations? Set to false for debugging purposes." + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class clean_patterns(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "clean_patterns" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Provide a list of patterns that you wish to remove from the cycle directory." + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class comparison_log_type(TaskQuestion): + default_value: str = "variational" + question_name: str = "comparison_log_type" + options: list[str] = mutable_field([ + 'variational', + 'fgat', + ]) + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Provide the log naming convention (e.g. 'variational', 'fgat')." + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class converter_path(TaskQuestion): + default_value: str = "" + question_name: str = "converter_path" + ask_question: bool = True + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = ("Path to directory containing ioda-converter scripts" + " (leave blank to use jedi_bin)") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class crtm_coeff_dir(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "crtm_coeff_dir" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the path to the CRTM coefficient files?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class dry_run(TaskQuestion): + default_value: bool = True + question_name: str = "dry_run" + ask_question: bool = False + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Dry-run mode: preview what would be ingested before storing to R2D2" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensemble_hofx_packets(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "ensemble_hofx_packets" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Enter number of packets in which ensemble observers should be computed." + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensemble_hofx_strategy(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "ensemble_hofx_strategy" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Enter hofx strategy." + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensemble_num_members(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "ensemble_num_members" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "How many members comprise the ensemble?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensmean_only(TaskQuestion): + default_value: bool = False + question_name: str = "ensmean_only" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Calculate ensemble mean only?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ensmeanvariance_only(TaskQuestion): + default_value: bool = False + question_name: str = "ensmeanvariance_only" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Calculate ensemble mean and variance only?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_geos_gcm_build_path(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_geos_gcm_build_path" + ask_question: bool = True + depends: dict = mutable_field({ + "geos_build_method": "use_existing" + }) + prompt: str = "What is the path to the existing GEOS build directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_geos_gcm_source_path(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_geos_gcm_source_path" + ask_question: bool = True + depends: dict = mutable_field({ + "geos_build_method": "use_existing" + }) + prompt: str = "What is the path to the existing GEOS source code directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_jedi_build_directory(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_jedi_build_directory" + ask_question: bool = True + depends: dict = mutable_field({ + "jedi_build_method": "use_existing" + }) + prompt: str = "What is the path to the existing JEDI build directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_jedi_build_directory_pinned(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_jedi_build_directory_pinned" + ask_question: bool = True + depends: dict = mutable_field({ + "jedi_build_method": "use_pinned_existing" + }) + prompt: str = "What is the path to the existing pinned JEDI build directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_jedi_source_directory(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_jedi_source_directory" + ask_question: bool = True + depends: dict = mutable_field({ + "jedi_build_method": "use_existing" + }) + prompt: str = "What is the path to the existing JEDI source code directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_jedi_source_directory_pinned(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "existing_jedi_source_directory_pinned" + ask_question: bool = True + depends: dict = mutable_field({ + "jedi_build_method": "use_pinned_existing" + }) + prompt: str = "What is the path to the existing pinned JEDI source code directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class existing_perllib_path(TaskQuestion): + default_value: str = 'defer_to_platform' + question_name: str = 'existing_perllib_path' + prompt: str = "Provide a path to an existing location for GMAO_perllib." + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class forecast_duration(TaskQuestion): + default_value: str = "PT12H" + question_name: str = "forecast_duration" + ask_question: bool = True + prompt: str = "GEOS forecast duration" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class forecast_length(TaskQuestion): + default_value: str = "PT12H" + question_name: str = "forecast_length" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "Duration of the GEOS-CF forecast (ISO 8601 duration, e.g. PT12H)" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class forecast_output_frequency(TaskQuestion): + default_value: str = "PT1H" + question_name: str = "forecast_output_frequency" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "Frequency of forecast output files (ISO 8601 duration, e.g. PT1H)" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class generate_yaml_and_exit(TaskQuestion): + default_value: bool = False + question_name: str = "generate_yaml_and_exit" + prompt: str = "Generate JEDI executable YAML and exit?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_build_method(TaskQuestion): + default_value: str = "create" + question_name: str = "geos_build_method" + ask_question: bool = True + options: list[str] = mutable_field([ + "use_existing", + "create" + ]) + prompt: str = "Do you want to use an existing GEOS build or create a new build?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_cf_install_dir(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geos_cf_install_dir" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the path to the GEOS-CF install directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_cf_run_dir(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geos_cf_run_dir" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the path to the GEOS-CF model run directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_expdir(TaskQuestion): + default_value: str = "/dev/null/" + question_name: str = "geos_expdir" + depends: dict = mutable_field({ + "geos_expdir_different": True + }) + prompt: str = ("What is the location for the EXPERIMENT Directory (to contain model " + "output and restart files), if it is different than your GEOS HOME " + "Directory?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_expdir_different(TaskQuestion): + default_value: str = False + question_name: str = "geos_expdir_different" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + prompt: str = ("Is your GEOS EXPERIMENT Directory, where restarts and scratch is located, " + "different than your GEOS HOME Directory?") + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_experiment_directory(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geos_experiment_directory" + ask_question: bool = True + prompt: str = "What is the path to the GEOS restarts directory?" + widget_type: WType = WType.STRING + + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_gcm_tag(TaskQuestion): + default_value: str = "v11.6.0" + question_name: str = "geos_gcm_tag" + depends: dict = mutable_field({ + "geos_build_method": "create" + }) + prompt: str = "Which GEOS tag do you wish to clone?" + widget_type: WType = WType.STRING + +# ------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_homdir(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geos_homdir" + ask_question: bool = True + prompt: str = ("What is the location for the HOME Directory (HOMDIR in gcm_run and " + "gcm_setup) that contains model settings and RC files?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_restarts_directory(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geos_restarts_directory" + ask_question: bool = True + prompt: str = "What is the path to the GEOS restarts directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_x_background_directory(TaskQuestion): + default_value: str = "/dev/null/" + question_name: str = "geos_x_background_directory" + ask_question: bool = True + options: list[str] = mutable_field([ + "/dev/null/", + "/discover/nobackup/projects/gmao/dadev/rtodling/archive/Restarts/JEDI/541x" + ]) + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the path to the GEOS X-backgrounds directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geos_x_ensemble_directory(TaskQuestion): + default_value: str = "/dev/null/" + question_name: str = "geos_x_ensemble_directory" + ask_question: bool = True + options: list[str] = mutable_field([ + "/dev/null/", + "/gpfsm/dnb05/projects/p139/rtodling/archive/" + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the path to the GEOS X-backgrounds directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geosfp_exp(TaskQuestion): + default_value: str = "f5295_fp" + question_name: str = "geosfp_exp" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the GEOS FP experiment ID used for IAU analysis files?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geosfp_path(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "geosfp_path" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the path to the GEOS FP archive?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geovals_experiment(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "geovals_experiment" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the name of the R2D2 experiment providing the GeoVaLs?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class geovals_provider(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "geovals_provider" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the name of the R2D2 database providing the GeoVaLs?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class gmao_perllib_tag(TaskQuestion): + default_value: str = 'g1.0.1' + question_name: str = 'gmao_perllib_tag' + prompt: str = "Specify the tag at which GMAO_perllib should be cloned." + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class gradient_norm_reduction(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "gradient_norm_reduction" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What value of gradient norm reduction for convergence?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class gsibec_configuration(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "gsibec_configuration" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which GSIBEC climatological or hybrid?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class gsibec_nlats(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "gsibec_nlats" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "How many number of latutides in GSIBEC grid?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class gsibec_nlons(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "gsibec_nlons" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "How many number of longitudes in GSIBEC grid?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class horizontal_localization_lengthscale(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "horizontal_localization_lengthscale" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the length scale for horizontal covariance localization?" + widget_type: WType = WType.FLOAT + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class horizontal_localization_max_nobs(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "horizontal_localization_max_nobs" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ("What is the maximum number of observations to consider" + " for horizontal covariance localization?") + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class horizontal_localization_method(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "horizontal_localization_method" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which localization scheme should be applied in the horizontal?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class horizontal_resolution(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "horizontal_resolution" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the horizontal resolution for the forecast model and backgrounds?" + widget_type: WType = WType.STRING_DROP_LIST + +# ------------------------------------------------------------------------------------------------ + + +@dataclass +class iau(TaskQuestion): + default_value: bool = True + question_name: str = "iau" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "Use Incremental Analysis Update (IAU) in the GEOS-CF forecast?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class inc_template(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "inc_template" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the path to the GEOS-CF increment template NetCDF file?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class initial_restarts_method(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "initial_restarts_method" + ask_question: bool = True + options: list[str] = mutable_field([ + "geos_expdir", + "r2d2", + "hotstart", + ]) + prompt: str = "How should initial GEOS restarts be obtained?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ioda_locations_not_in_r2d2(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "ioda_locations_not_in_r2d2" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ( + "Provide a path that contains observation files not in r2d2.") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class jedi_build_method(TaskQuestion): + default_value: str = "create" + question_name: str = "jedi_build_method" + ask_question: bool = True + options: list[str] = mutable_field([ + "use_existing", + "use_pinned_existing", + "create", + "pinned_create" + ]) + prompt: str = "Do you want to use an existing JEDI build or create a new build?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class jedi_forecast_model(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "jedi_forecast_model" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + depends: dict = mutable_field({ + "window_type": "4D" + }) + prompt: str = "What forecast model should be used within JEDI for 4D window propagation?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_inflation_mult(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "local_ensemble_inflation_mult" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Specify the multiplicative prior inflation coefficient (0 inf]." + widget_type: WType = WType.FLOAT + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_inflation_rtpp(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "local_ensemble_inflation_rtpp" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Specify the Relaxation To Prior Perturbation (RTPP) coefficient (0 1]." + widget_type: WType = WType.FLOAT + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_inflation_rtps(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "local_ensemble_inflation_rtps" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Specify the Relaxation To Prior Spread (RTPS) coefficient (0 1]." + widget_type: WType = WType.FLOAT + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_save_posterior_ensemble(TaskQuestion): + default_value: bool = False + question_name: str = "local_ensemble_save_posterior_ensemble" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Save the posterior ensemble members?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_save_posterior_ensemble_increments(TaskQuestion): + default_value: bool = False + question_name: str = "local_ensemble_save_posterior_ensemble_increments" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Save the posterior ensemble member increments?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_save_posterior_mean(TaskQuestion): + default_value: bool = False + question_name: str = "local_ensemble_save_posterior_mean" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Save the posterior ensemble mean?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_save_posterior_mean_increment(TaskQuestion): + default_value: bool = True + question_name: str = "local_ensemble_save_posterior_mean_increment" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Save the posterior ensemble mean increment?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_solver(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "local_ensemble_solver" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which local ensemble solver type should be implemented?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class local_ensemble_use_linear_observer(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "local_ensemble_use_linear_observer" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which local ensemble solver type should be implemented?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class minimizer(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "minimizer" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which data assimilation minimizer do you wish to use?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class mom6_iau(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "mom6_iau" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_marine", + ]) + prompt: str = "Do you wish to use IAU for MOM6?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class mom6_iau_nhours(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "mom6_iau_nhours" + options: list[str] = mutable_field([ + 'PT3H', + 'PT12H' + ]) + depends: dict = mutable_field({'mom6_iau': True}) + models: list[str] = mutable_field([ + "geos_marine", + ]) + prompt: str = "What is the IAU length (ODA_INCUPD_NHOURS) for MOM6?" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class ncdiag_experiments(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "ncdiag_experiments" + options: list[str] = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which previously run experiments do you wish to use for the NCdiag?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class npx(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "npx" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the number of grid points in the x-direction on each cube face?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class npx_proc(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "npx_proc" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere", + "geos_cf" + ]) + prompt: str = "What number of processors do you wish to use in the x-direction?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class npy(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "npy" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the number of grid points in the y-direction on each cube face?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class npy_proc(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "npy_proc" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere", + "geos_cf" + ]) + prompt: str = "What number of processors do you wish to use in the y-direction?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class number_of_iterations(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "number_of_iterations" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = ( + "What number of iterations do you wish to use for each outer loop?" + " Provide a list of integers the same length as the number of outer loops.") + widget_type: WType = WType.INTEGER_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class obs_experiment(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "obs_experiment" + ask_question: bool = True + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the database providing the observations?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class obs_thinning_rej_fraction(TaskQuestion): + default_value: float = 0.75 + question_name: str = "obs_thinning_rej_fraction" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the rejection fraction for obs thinning?" + widget_type: WType = WType.FLOAT + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class obs_to_download(TaskQuestion): + default_value: list = mutable_field([]) + question_name: str = "obs_to_download" + ask_question: bool = True + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which observations do you want to download from remote servers?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class obs_to_ingest(TaskQuestion): + default_value: list = mutable_field([]) + question_name: str = "obs_to_ingest" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which observations do you want to ingest to R2D2?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class observations(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "observations" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Which observations do you want to include?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class observing_system_records_mksi_path(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "observing_system_records_mksi_path" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the path to the GSI formatted observing system records?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class observing_system_records_mksi_path_tag(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "observing_system_records_mksi_path_tag" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the GSI formatted observing system records tag?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class observing_system_records_path(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "observing_system_records_path" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the path to the Swell formatted observing system records?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class path_to_ensemble(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "path_to_ensemble" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere", + "geos_marine" + ]) + prompt: str = "What is the path to where ensemble members are stored?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class path_to_geos_adas_background(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "path_to_geos_adas_background" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ( + "What is the path for the GEOSadas cubed sphere backgrounds?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class path_to_gsi_bc_coefficients(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "path_to_gsi_bc_coefficients" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the location where GSI bias correction files can be found?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class path_to_gsi_nc_diags(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "path_to_gsi_nc_diags" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the path to where the GSI ncdiags are stored?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class pause_on_tasks(TaskQuestion): + default_value: list = mutable_field([]) + question_name: str = "pause_on_tasks" + ask_question: bool = False + prompt: str = ("Specify any tasks that the workflow should pause on " + "(for development purposes).") + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class perhost(TaskQuestion): + default_value: str = None + question_name: str = "perhost" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the number of processors per host?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class produce_geovals(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "produce_geovals" + ask_question: bool = True + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ("When running the ncdiag to ioda converted do you " + "want to produce GeoVaLs files?") + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class rst_experiment(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "rst_experiment" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What is the name of the experiment providing the restart files in R2D2?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class rst_file_types(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "rst_file_types" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = "What are the restart file types to fetch/store from R2D2?" + widget_type: WType = WType.STRING_CHECK_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class rst_store_interval(TaskQuestion): + default_value: str = None + question_name: str = "rst_store_interval" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_cf" + ]) + prompt: str = ("After how many cycles should restart files be stored as real files" + "(not symlinks)? E.g. 28 means every 28th cycle (and multiples) stores " + "real files. Leave unset to always store as symlinks.") + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class save_geovals(TaskQuestion): + default_value: bool = False + question_name: str = "save_geovals" + options: list[bool] = mutable_field([ + True, + False + ]) + prompt: str = "When running hofx do you want to output the GeoVaLs?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class single_observations(TaskQuestion): + default_value: bool = False + question_name: str = "single_observations" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Is it a single-observation test?" + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class swell_static_files(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "swell_static_files" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the path to the Swell Static files directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class swell_static_files_user(TaskQuestion): + default_value: str = "None" + question_name: str = "swell_static_files_user" + prompt: str = "What is the path to the user provided Swell Static Files directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class task_email_parameters(TaskQuestion): + default_value: Literal["auto"] | dict = "auto" + question_name: str = "task_email_parameters" + prompt: str = ("Provide a dictionary mapping tasks to cylc event statuses, or 'auto' to " + "automatically configure these based on the graph.") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class total_processors(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "total_processors" + ask_question: bool = True + models: list[str] = mutable_field([ + "geos_marine", + ]) + prompt: str = "What is the number of processors for JEDI?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_apply_log_transform(TaskQuestion): + default_value: bool = True + question_name: str = "vertical_localization_apply_log_transform" + options: list[bool] = mutable_field([ + True, + False + ]) + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ("Should a log (base 10) transformation be applied " + "to vertical coordinate when " + "constructing vertical localization?") + widget_type: WType = WType.BOOLEAN + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_function(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_localization_function" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which localization scheme should be applied in the vertical?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_ioda_vertical_coord(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_localization_ioda_vertical_coord" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "Which coordinate should be used in constructing vertical localization?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_ioda_vertical_coord_group(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_localization_ioda_vertical_coord_group" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ("Which vertical coordinate group should be used " + "in constructing vertical localization?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_lengthscale(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_localization_lengthscale" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = "What is the length scale for vertical covariance localization?" + widget_type: WType = WType.INTEGER + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_localization_method(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_localization_method" + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "geos_atmosphere" + ]) + prompt: str = ("What localization scheme should be applied in " + "constructing a vertical localization?") + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class vertical_resolution(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "vertical_resolution" + ask_question: bool = True + options: str = "defer_to_model" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the vertical resolution for the forecast model and background?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class window_length(TaskQuestion): + default_value: str = "defer_to_model" + question_name: str = "window_length" + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "What is the duration for the data assimilation window?" + widget_type: WType = WType.ISO_DURATION + +# -------------------------------------------------------------------------------------------------- + + +@dataclass +class window_type(TaskQuestion): + question_name: str = "window_type" + default_value: str = "defer_to_model" + ask_question: bool = True + options: list[str] = mutable_field([ + "3D", + "4D" + ]) + models: list[str] = mutable_field([ + "all_models" + ]) + prompt: str = "Do you want to use a 3D or 4D (including FGAT) window?" + widget_type: WType = WType.STRING_DROP_LIST + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/cylc_swell.py b/src/swell/cylc_swell.py index 8fb79916f..3091bcfab 100644 --- a/src/swell/cylc_swell.py +++ b/src/swell/cylc_swell.py @@ -11,6 +11,7 @@ import subprocess import os import sys +from collections.abc import Mapping from swell.deployment.platforms.platforms import SwellPlatform from swell.utilities.logger import Logger @@ -18,7 +19,7 @@ # -------------------------------------------------------------------------------------------------- -def configure_cylc_environment(append_dict: dict = {}) -> dict: +def configure_cylc_environment(append_dict: dict = {}) -> Mapping: ''' Unset the path containing Swell's cylc entry point, and set the environment according to the specified dictionary ''' diff --git a/src/swell/deployment/create_experiment.py b/src/swell/deployment/create_experiment.py index f4114d485..87bbff961 100644 --- a/src/swell/deployment/create_experiment.py +++ b/src/swell/deployment/create_experiment.py @@ -9,22 +9,20 @@ import copy -import datetime import io import os import shutil import sys from ruamel.yaml import YAML -from typing import Optional -from swell.suites.all_suites import AllSuites from swell.deployment.prepare_config_and_suite.prepare_config_and_suite import \ PrepareExperimentConfigAndSuite from swell.swell_path import get_swell_path -from swell.utilities.dictionary import add_comments_to_dictionary, dict_get +from swell.utilities.dictionary import add_comments_to_dictionary, dict_get, update_dict from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.logger import Logger, get_logger -from swell.utilities.slurm import prepare_scheduling_dict +from swell.utilities.slurm import prepare_slurm_defaults_and_overrides +from swell.suites.base.suite_attributes import suite_configs, workflows from swell.utilities.check_da_params import check_da_params @@ -38,6 +36,7 @@ def clone_config( platform: str, advanced: bool ) -> str: + # Create a logger logger = get_logger('SwellCloneExperiment') @@ -50,6 +49,8 @@ def clone_config( yaml = YAML() override_dict = yaml.load(f) + suite_config = override_dict['suite_to_run'] + # Check that override_dict has a suite key and get the suite name if 'suite_to_run' not in override_dict: logger.abort('The provided configuration file does not have a \'suite_to_run\' key') @@ -63,7 +64,13 @@ def clone_config( override_dict['experiment_id'] = experiment_id # First create the configuration for the experiment. - return prepare_config(suite, method, override_dict['platform'], override_dict, advanced) + return prepare_config(suite, + suite_config=suite_config, + method=method, + platform=override_dict['platform'], + override=override_dict, + advanced=advanced, + slurm=None) # -------------------------------------------------------------------------------------------------- @@ -76,7 +83,7 @@ def prepare_config( platform: str, override: dict, advanced: bool, - slurm: str + slurm: str | None ) -> str: # Create a logger @@ -98,40 +105,22 @@ def prepare_config( prepare_config_and_suite = PrepareExperimentConfigAndSuite(logger, suite, suite_config, platform, method, override) - # Ask questions as the suite gets configured - # ------------------------------------------ - experiment_dict, comment_dict = prepare_config_and_suite.ask_questions_and_configure_suite() - # Add the datetime to the dictionary - # ---------------------------------- - experiment_dict['datetime_created'] = datetime.datetime.today().strftime("%Y%m%d_%H%M%SZ") - comment_dict['datetime_created'] = 'Datetime this file was created (auto added)' - - # Add the platform the dictionary - # ------------------------------- - experiment_dict['platform'] = platform - comment_dict['platform'] = 'Computing platform to run the experiment' - - # Add the suite_to_run to the dictionary + # Retrieved the answered suite questions # -------------------------------------- - experiment_dict['suite_to_run'] = suite - comment_dict['suite_to_run'] = 'Record of the suite being executed' - - # Add the model components to the dictionary - # ------------------------------------------ - if 'models' in experiment_dict: - experiment_dict['model_components'] = list(experiment_dict['models'].keys()) - comment_dict['model_components'] = 'List of models in this experiment' + suite_dict = prepare_config_and_suite.experiment_dict.copy() # Overrides for comparison suites - if 'start_cycle_point' in experiment_dict: - start_cycle_point = experiment_dict['start_cycle_point'] - final_cycle_point = experiment_dict['final_cycle_point'] - if experiment_dict['start_cycle_point'] is None: - config_list = experiment_dict['comparison_experiment_paths'] + if 'start_cycle_point' in suite_dict: + start_cycle_point = suite_dict['start_cycle_point'] + final_cycle_point = suite_dict['final_cycle_point'] + if 'comparison_experiment_paths' in suite_dict and \ + suite_dict['start_cycle_point'] is None: + config_list = suite_dict['comparison_experiment_paths'] if isinstance(config_list, dict): config_list = list(config_list.values()) - for model in experiment_dict['model_components']: - cycle_times = experiment_dict['models'][model]['cycle_times'] + for model in suite_dict['model_components']: + cycle_times = suite_dict['models'][model]['cycle_times'] + start_cycle_point, final_cycle_point, cycle_times = check_da_params( config_list, model, @@ -139,36 +128,85 @@ def prepare_config( final_cycle_point, cycle_times) - experiment_dict['start_cycle_point'] = start_cycle_point - experiment_dict['final_cycle_point'] = final_cycle_point - experiment_dict['models'][model]['cycle_times'] = cycle_times + suite_dict['start_cycle_point'] = start_cycle_point + suite_dict['final_cycle_point'] = final_cycle_point + suite_dict['models'][model]['cycle_times'] = cycle_times - # Expand experiment dict with SLURM overrides. - # NOTE: This is a bit of a hack. We should really either commit to using a - # separate file and pass it around everywhere, or commit fully to keeping - # everything in `experiment.yaml` and support it through the Questionary - # infrastructure. - # ---------------------------------- - if slurm is not None: - logger.info(f"Reading SLURM directives from {slurm}.") - assert os.path.exists(slurm) - with open(slurm, "r") as slurmfile: - slurm_dict = yaml.load(slurmfile) - # Ensure that SLURM dict is _only_ used for SLURM directives. - slurm_invalid_keys = set(slurm_dict.keys()).difference({ - "slurm_directives_global", - "slurm_directives_tasks" - }) - if slurm_invalid_keys: - logger.abort(f'SLURM file contains invalid keys: {slurm_invalid_keys}') - experiment_dict = {**experiment_dict, **slurm_dict} + # Resolve cycle times for models + # ------------------------------ + if 'models' in suite_dict and 'start_cycle_point' in suite_dict: + model_components = suite_dict['models'] + + # Since cycle times are used, the render_dictionary will need to include cycle_times + # If there are different model components then process each to gather cycle times + if len(model_components) > 0 and all('cycle_times' in suite_dict['models'][model] + for model in model_components): + cycle_times = [] + for model_component in model_components: + cycle_times_mc = suite_dict['models'][model_component]['cycle_times'] + cycle_times = list(set(cycle_times + cycle_times_mc)) + cycle_times.sort() + + cycle_times_dict_list = [] + for cycle_time in cycle_times: + cycle_time_dict = {} + cycle_time_dict['cycle_time'] = cycle_time + for model_component in model_components: + cycle_time_dict[model_component] = False + if cycle_time in suite_dict['models'][model_component]['cycle_times']: + cycle_time_dict[model_component] = True + cycle_times_dict_list.append(cycle_time_dict) + + suite_dict['cycle_times'] = cycle_times_dict_list + + # Otherwise check that suite_dict has cycle_times + elif 'cycle_times' in suite_dict: + + cycle_times = list(set(suite_dict['cycle_times'])) + cycle_times.sort() + suite_dict['cycle_times'] = cycle_times + + # Get the slurm defaults from the user and platform + # ------------------------------------------------- + slurm_dict = prepare_slurm_defaults_and_overrides(logger, platform, slurm) + + # Initialize the workflow + # ----------------------- + workflow_class = workflows.get(suite) + workflow = workflow_class(suite_dict, slurm_dict) + + # Get the list of tasks from the workflow's graph + # ----------------------------------------------- + model_ind_tasks, model_dep_tasks = workflow.get_independent_and_model_tasks() + + # Set the tasks to be used in preparing the suite + # ----------------------------------------------- + prepare_config_and_suite.model_independent_tasks = model_ind_tasks + prepare_config_and_suite.model_dependent_tasks = model_dep_tasks + + # Ask the task questions + # ---------------------- + experiment_dict, comment_dict = prepare_config_and_suite.configure_and_ask_task_questions() + if 'start_cycle_point' in suite_dict: + experiment_dict['start_cycle_point'] = suite_dict['start_cycle_point'] + experiment_dict['final_cycle_point'] = suite_dict['final_cycle_point'] + + # Update dict with cycle times + # ---------------------------- + workflow_dict = update_dict(experiment_dict, suite_dict) + workflow.experiment_dict = workflow_dict + + # Finalize the workflow by adding the runtime section, and get the contents + # ------------------------------------------------------------------------- + workflow_string = workflow.get_workflow_string() # Register the experiment in R2D2 # ------------------------------- if 'r2d2_experiment_id' in experiment_dict and 'skip_r2d2' in experiment_dict \ and not experiment_dict['skip_r2d2']: - from swell.utilities.r2d2 import load_r2d2_credentials, load_r2d2_module, unique_r2d2_id + from swell.utilities.r2d2_utils import load_r2d2_credentials, load_r2d2_module, \ + unique_r2d2_id load_r2d2_module(logger, platform) r2d2_server = experiment_dict.get('r2d2_server') @@ -213,7 +251,7 @@ def prepare_config( # Return path to dictionary file # ------------------------------ - return experiment_dict_string_comments + return experiment_dict_string_comments, workflow_string # -------------------------------------------------------------------------------------------------- @@ -231,7 +269,7 @@ def create_experiment_directory( # Get the base name of the suite # ------------------------------ - suite = AllSuites.base_suite(suite_config) + suite = suite_configs.base_suite(suite_config) # Create a logger # --------------- @@ -246,8 +284,8 @@ def create_experiment_directory( # Call the experiment config and suite generation # ------------------------------------------------ - experiment_dict_str = prepare_config(suite, suite_config, method, platform, - override, advanced, slurm) + experiment_dict_str, workflow_str = prepare_config(suite, suite_config, method, platform, + override, advanced, slurm) # Load the string using yaml # -------------------------- @@ -275,13 +313,8 @@ def create_experiment_directory( with open(os.path.join(exp_suite_path, 'experiment.yaml'), 'w') as file: file.write(experiment_dict_str) - # At this point we need to write the complete suite file with all templates resolved. Call the - # function to build the scheduling dictionary, combine with the experiment dictionary, - # resolve the templates and write the suite file to the experiment suite directory. - # -------------------------------------------------------------------------------------------- - swell_suite_path = os.path.join(get_swell_path(), 'suites', suite) - prepare_cylc_suite_jinja2(logger, swell_suite_path, exp_suite_path, experiment_dict, - platform, exp_path) + with open(os.path.join(exp_suite_path, 'flow.cylc'), 'w') as file: + file.write(workflow_str) # Copy suite and platform files to experiment suite directory # ----------------------------------------------------------- @@ -341,7 +374,7 @@ def copy_eva_files( def copy_platform_files( logger: Logger, exp_suite_path: str, - platform: Optional[str] = None + platform: str | None = None ) -> None: # Copy platform related files to the suite directory @@ -379,6 +412,8 @@ def template_modules_file( # Swell bin path # -------------- swell_bin_path = shutil.which("swell") + if swell_bin_path is None: + raise ModuleNotFoundError(f'Could not find swell executable') swell_bin_path = os.path.split(swell_bin_path)[0] # Swell lib path @@ -415,7 +450,6 @@ def template_modules_file( with open(modules_file, 'w') as modules_file_open: modules_file_open.write(modules_file_str) - # -------------------------------------------------------------------------------------------------- @@ -469,7 +503,6 @@ def create_modules_csh( # -------------------------------------------------------------------------------------------------- - def prepare_cylc_suite_jinja2( logger: Logger, swell_suite_path: str, @@ -504,7 +537,7 @@ def prepare_cylc_suite_jinja2( # Since cycle times are used, the render_dictionary will need to include cycle_times # If there are different model components then process each to gather cycle times if len(model_components) > 0: - cycle_times = [] + cycle_times: list = [] for model_component in model_components: cycle_times_mc = experiment_dict['models'][model_component]['cycle_times'] cycle_times = list(set(cycle_times + cycle_times_mc)) diff --git a/src/swell/deployment/create_task_config.py b/src/swell/deployment/create_task_config.py new file mode 100644 index 000000000..e3e679b69 --- /dev/null +++ b/src/swell/deployment/create_task_config.py @@ -0,0 +1,226 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +import io +import os +from typing import Optional +from ruamel.yaml import YAML +import isodate + +from swell.tasks.base.task_attributes import task_attributes +from swell.utilities.logger import get_logger +from swell.deployment.prepare_config_and_suite.prepare_config_and_suite import \ + PrepareExperimentConfigAndSuite +from swell.utilities.slurm import prepare_slurm_defaults_and_overrides +from swell.utilities.dictionary import add_comments_to_dictionary +from swell.deployment.create_experiment import template_modules_file, create_modules_csh +from swell.utilities.jinja2 import template_string_jinja2 +from swell.utilities.shell_commands import create_executable_file + +# -------------------------------------------------------------------------------------------------- + +script_template = '''#!{{shell}} +{% if task_slurm_dict != None %} +{%- for key, value in task_slurm_dict.items() %} +#SBATCH --{{key}} = {{value}} +{%- endfor %} +{% endif %} + +# ------------------- + +source {{modules_file}} + +# ------------------- + +{{script}} + +# ------------------- +''' + + +# -------------------------------------------------------------------------------------------------- + +def task_config_wrapper(task_name: str, + platform: str, + datetime: Optional[str], + model: Optional[str], + input_method: str, + override: dict, + slurm: str, + cwd: bool) -> None: + + # Create logger + logger = get_logger('SwellTaskConfig') + + logger.info(f'Generating config for task {task_name}') + + # Get the task attributes for the class + task_attr_class = getattr(task_attributes, task_name) + task = task_attr_class(model=model, platform=platform) + + # Check that model is specified for the task + if task.model_dep and model is None: + logger.abort('Task requires model (e.g. geos_marine, geos_atmsophere)' + ' but none was specified at the command line.') + + # Check that datetime is specified for the task + if task.is_cycling and datetime is None: + logger.abort('Task requires datetime (e.g. 20231010T000000Z)' + ' but none was specified at the command line.') + + if model is not None: + override['model_components'] = [model] + else: + override['model_components'] = [] + + # Build in current working directory + if cwd: + override['experiment_root'] = os.getcwd() + + # Construct task ID + task_id = f'swell-{task_name}' + if model is not None: + task_id = task_id + f'-{model}' + + if datetime is not None: + task_id = task_id + f'-{datetime}' + + if 'experiment_id' not in override: + override['experiment_id'] = task_id + + # Don't put results in cycle dir + if 'use_cycle_dir' not in override: + override['use_cycle_dir'] = False + + # Build the suite for task minimums + prepare_config_and_suite = PrepareExperimentConfigAndSuite(logger=logger, + suite='task_minimum', + suite_config='task_minimum', + platform=platform, + config_client=input_method, + override=override) + + # Set the tasks appropriately + model_independent_tasks = [] + model_dependent_tasks = {} + + if model is None: + model_independent_tasks.append(task) + else: + model_dependent_tasks[model] = [task] + + prepare_config_and_suite.model_independent_tasks = model_independent_tasks + prepare_config_and_suite.model_dependent_tasks = model_dependent_tasks + + # Configure and ask all questions + experiment_dict, comment_dict = prepare_config_and_suite.configure_and_ask_task_questions() + + yaml = YAML(typ='safe') + + # Expand all environment vars in the dictionary + output = io.StringIO() + yaml.dump(experiment_dict, output) + experiment_dict_string = output.getvalue() + experiment_dict_string = os.path.expandvars(experiment_dict_string) + experiment_dict = yaml.load(experiment_dict_string) + + # Add comments to dictionary + output = io.StringIO() + yaml.dump(experiment_dict, output) + experiment_dict_string = output.getvalue() + + experiment_dict_string_comments = add_comments_to_dictionary(logger, experiment_dict_string, + comment_dict) + + # Construct the slurm dict for the task + task_slurm_dict = None + if task.slurm is not None: + # Construct the slurm defaults + slurm_external_dict = prepare_slurm_defaults_and_overrides(logger, platform, slurm) + task_slurm_dict = task.generate_task_slurm_dict(slurm_external_dict) + + task_time_limit = task.task_time_limit + if task_time_limit is not None: + task_time_limit_dto = isodate.parse_duration(task_time_limit) + task_slurm_dict['time'] = isodate.strftime(task_time_limit_dto, '%H:%M:%S') + + # Determine the path for task results + experiment_root = experiment_dict['experiment_root'] + experiment_id = experiment_dict['experiment_id'] + + task_path = os.path.join(experiment_root, experiment_id) + + # If use_cycle_dir, construct the experiment directory the same way as a suite + if experiment_dict['use_cycle_dir']: + task_path = os.path.join(task_path, f'{experiment_id}-suite') + + # Create the task directory + os.makedirs(task_path, exist_ok=True) + + # Write the task config + config_file = os.path.join(task_path, 'task_config.yaml') + with open(config_file, 'w') as f: + f.write(experiment_dict_string_comments) + + logger.info(f'Writing task config under {config_file}') + + # Build the modules file depending on the shell type + shell = os.environ.get('SHELL') + if shell is None: + logger.abort('Could not ascertaine $SHELL') + + if shell is not None and 'bash' in shell: + template_modules_file(logger, experiment_dict, task_path) + modules_file = os.path.join(task_path, 'modules') + shell_type = 'bash' + elif shell is not None and 'csh' in shell: + create_modules_csh(logger, task_path) + modules_file = os.path.join(task_path, 'modules-csh') + shell_type = 'csh' + else: + logger.abort(f'Failed to deduce the target shell. $SHELL is currently set to {shell}') + + file_ext = shell_type + if task_slurm_dict is not None: + file_ext = 'slurm' + + # Build the swell task script + script = f'swell task {task_name} {config_file}' + if model is not None: + script += f' -m {model}' + + if datetime is not None: + script += f' -d {datetime}' + + # Build the template dict for the shell script file + script_dict = {} + script_dict['shell'] = shell + script_dict['task_slurm_dict'] = task_slurm_dict + script_dict['modules_file'] = modules_file + script_dict['script'] = script + + # Template the shell script + script_content = template_string_jinja2(logger, + templated_string=script_template, + dictionary_of_templates=script_dict) + + # Create the shell script file + script_file = os.path.join(task_path, f'{task_id}.{file_ext}') + create_executable_file(logger, script_file, script_content) + + logger.info('Task config successfully generated.') + logger.info('To run the task by itself, run: ') + print(f'\n {script}\n') + logger.info('Or, to use auto-generated script, run: ') + if task_slurm_dict is not None: + print(f'\n sbatch {script_file}\n') + else: + print(f'\n {script_file}\n') + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/deployment/launch_experiment.py b/src/swell/deployment/launch_experiment.py index ef9992e57..2c5e9bb5f 100644 --- a/src/swell/deployment/launch_experiment.py +++ b/src/swell/deployment/launch_experiment.py @@ -25,6 +25,8 @@ def __init__( experiment_name: str, no_detach: bool, log_path: str, + send_cylc_messages: bool, + allow_pause: bool, cylc_timeout: str ) -> None: @@ -94,6 +96,12 @@ def cylc_run_experiment(self) -> None: # NB: Could be a factory based on workfl self.logger.info(' \u001b[32mcylc stop --kill ' + self.experiment_name + '\033[0m') self.logger.info(' ') + send_messages = os.environ.get('SWELL_SEND_MESSAGES') + if send_messages == '1': + self.logger.info(' Workflow will pause on tasks configured to do so. To unpause:') + self.logger.info(' \u001b[32mcylc play ' + self.experiment_name + '\033[0m') + self.logger.info(' ') + # Launch the job monitor self.logger.critical('Press Enter to launch the TUI. To exit TUI, ' + 'press \'q\' at any time.') @@ -112,7 +120,9 @@ def launch_experiment( suite_path: str, no_detach: bool, log_path: str, - cylc_timeout: bool + send_cylc_messages: bool = False, + allow_pause: bool = False, + cylc_timeout: str | None = None ) -> None: # Get the path to where the suite files are located @@ -128,13 +138,28 @@ def launch_experiment( # Create the deployment object # ---------------------------- - deploy_workflow = DeployWorkflow(suite_path, experiment_name, no_detach, log_path, cylc_timeout) + deploy_workflow = DeployWorkflow(suite_path, experiment_name, no_detach, log_path, + send_cylc_messages, allow_pause, cylc_timeout) # Write some info for the user # ---------------------------- deploy_workflow.logger.info('Launching workflow defined by files in \'' + suite_path + '\'.') deploy_workflow.logger.info('Experiment name: ' + experiment_name) + # Set environment variable allowing for cylc email messaging + # ---------------------------------------------------------- + if send_cylc_messages: + os.environ['SWELL_SEND_MESSAGES'] = str(1) + else: + os.environ['SWELL_SEND_MESSAGES'] = str(0) + + # Set environment variable allowing for pausing on set tasks + # ---------------------------------------------------------- + if allow_pause: + os.environ['SWELL_PAUSE_WORKFLOW'] = str(1) + else: + os.environ['SWELL_PAUSE_WORKFLOW'] = str(0) + # Launch the workflow # ------------------- deploy_workflow.cylc_run_experiment() diff --git a/src/swell/deployment/prepare_config_and_suite/prepare_config_and_suite.py b/src/swell/deployment/prepare_config_and_suite/prepare_config_and_suite.py index 69987bf5f..3c3515463 100644 --- a/src/swell/deployment/prepare_config_and_suite/prepare_config_and_suite.py +++ b/src/swell/deployment/prepare_config_and_suite/prepare_config_and_suite.py @@ -8,21 +8,20 @@ # -------------------------------------------------------------------------------------------------- -import copy import os from ruamel.yaml import YAML from collections.abc import Mapping -from typing import Tuple, Optional +import datetime from swell.swell_path import get_swell_path +from swell.utilities.suite_utils import get_model_components from swell.deployment.prepare_config_and_suite.question_and_answer_cli import GetAnswerCli from swell.deployment.prepare_config_and_suite.question_and_answer_defaults import GetAnswerDefaults from swell.utilities.dictionary import dict_get from swell.utilities.logger import Logger -from swell.utilities.jinja2 import template_string_jinja2 -from swell.utilities.dictionary import update_dict -from swell.tasks.task_questions import TaskQuestions as task_questions -from swell.suites.all_suites import AllSuites +from swell.utilities.dictionary import add_dict +from swell.suites.base.suite_attributes import suite_configs +from swell.utilities.swell_questions import QuestionType # -------------------------------------------------------------------------------------------------- @@ -76,220 +75,199 @@ def __init__( # Big dictionary that contains all user responses as well a dictionary containing the # questions that were asked - self.experiment_dict = {} - self.questions_dict = {} + self.experiment_dict: dict = {} + self.comment_dict: dict = {} - # Get list of all possible models - self.possible_model_components = os.listdir(os.path.join(get_swell_path(), 'configuration', - 'jedi', 'interfaces')) + # Add the datetime to the dictionary + # ---------------------------------- + self.experiment_dict['datetime_created'] = datetime.datetime.today().strftime( + "%Y%m%d_%H%M%SZ") + self.comment_dict['datetime_created'] = 'Datetime this file was created (auto added)' - # Read suite file into a string - suite_file = os.path.join(get_swell_path(), 'suites', self.suite, 'flow.cylc') - with open(suite_file, 'r') as suite_file_open: - self.suite_str = suite_file_open.read() + # Add the platform the dictionary + # ------------------------------- + self.experiment_dict['platform'] = platform + self.comment_dict['platform'] = 'Computing platform to run the experiment' - # Get a list of model-independent and dependent questions - self.model_ind_tasks = self.get_suite_task_list_model_ind(self.suite_str) - self.all_model_dep_tasks = self.get_all_model_dep_tasks(self.suite_str) + # Add the suite_to_run to the dictionary + # -------------------------------------- + self.experiment_dict['suite_to_run'] = suite + self.comment_dict['suite_to_run'] = 'Record of the suite being executed' + + # Get list of all possible models + # ------------------------------- + self.possible_model_components = get_model_components() - # Perform the assembly of the dictionaries that contain all the questions that can possibly - # be asked. This + # Initialize task trackers + # ------------------------ + self.model_dependent_tasks = [] + self.model_independent_tasks = {} - self.prepare_question_dictionaries() - self.override_with_defaults() - self.override_with_external() + # Start initializing the suite questions first + # -------------------------------------------- + self.prepare_suite_question_dictionary() + self.override_with_defaults(QuestionType.SUITE) + self.override_with_external(QuestionType.SUITE) + self.ask_questions_and_configure(QuestionType.SUITE) # ---------------------------------------------------------------------------------------------- - def prepare_question_dictionaries(self) -> None: + def configure_and_ask_task_questions(self) -> tuple[dict, dict]: + # Finalize the experiment config with task questions - # Create a dictionary of all suite questions - question_dictionary = {} + self.prepare_task_question_dictionary() + self.override_with_defaults(QuestionType.TASK) + self.override_with_external(QuestionType.TASK) + self.ask_questions_and_configure(QuestionType.TASK) - # Create a dictionary of all task questions - question_dictionary_tasks = {} + return self.experiment_dict, self.comment_dict - # Create a dictionary associating each task with its list of questions - self.questions_per_task = {} + # ---------------------------------------------------------------------------------------------- - # Create an override dictionary for model-dependent questions - # This will later be used to set defaults - model_dep_questions_override = {} + def prepare_suite_question_dictionary(self) -> None: + # Get questions from the suite config - # Get a list of all questions associated with the suite, except for those specified - # seperately for models + question_dictionary_model_ind = {} + question_dictionary_model_dep = {} - suite_config_obj = AllSuites.get_config(self.suite_config) + suite_config_obj = suite_configs.get_config(self.suite_config) suite_question_list = suite_config_obj.expand_question_list() - # Allow for adding extra tasks manually from configuration - # For dynamic suite creation (e.g. comparison tests) + for model in self.possible_model_components: + question_dictionary_model_dep[model] = {} - dynamic_tasks = self.get_dynamic_tasks(suite_question_list) + for question in suite_config_obj.expand_question_list(model): + question_dictionary_model_dep[model][question['question_name']] = question - # Loop through all tasks and get their associated tasks - for task in self.model_ind_tasks + self.all_model_dep_tasks + dynamic_tasks: - if task in task_questions.get_all(): - question_list = task_questions[task].value.expand_question_list() + for question in suite_question_list: + if question['models'] is None: + question_dictionary_model_ind[question['question_name']] = question + else: + if 'all_models' in question['models']: + question_models = self.possible_model_components + else: + question_models = question['models'] - for question in question_list: - question_dictionary_tasks[question['question_name']] = question + for model in question_models: + question_dictionary_model_dep = add_dict(question_dictionary_model_dep, + {model: {question['question_name']: + question}}) - for model in self.possible_model_components: - for question in task_questions[task].value.expand_question_list(model): - model_dep_questions_override[model][question['question_name']] = question + self.suite_needs_model_components = True + if 'model_components' not in question_dictionary_model_ind.keys(): + self.suite_needs_model_components = False - self.questions_per_task[task] = [question['question_name'] - for question in question_list] - else: - self.questions_per_task[task] = [] + for question in suite_question_list: + if question['question_name'] == 'cycle_times': + question['models'] = None + question_dictionary_model_ind['cycle_times'] = question - # Convert the list of questions into a dictionary indexed by the question name - for question in suite_question_list: - question_dictionary[question['question_name']] = question + self.question_dictionary_model_ind = question_dictionary_model_ind + self.question_dictionary_model_dep = question_dictionary_model_dep - # Update model dependent overrides with suite questions - for model in self.possible_model_components: - model_dep_questions_override[model] = {} - for question in suite_config_obj.expand_question_list(model): - model_dep_questions_override[model][question['question_name']] = question + # ---------------------------------------------------------------------------------------------- - # Merge the dictionaries for task questions into the suite question - # list, but keep suite questions at the top of the order - # Override priority is given to questions defined in the suite_question_list + def prepare_task_question_dictionary(self): + # Fill in the question dictionaries with questions from the tasks - # Iterate through the task questions - # ---------------------------------- - for key, value in question_dictionary_tasks.items(): + # Track all possible tasks + task_options = [] - # If a question has no counterpart specified in suite questions, merge it - # ----------------------------------------------------------------------- - if key not in question_dictionary: - question_dictionary[key] = value - else: + # Model components used by the experiment + model_components = [] + if 'model_components' in self.experiment_dict.keys(): + model_components = self.experiment_dict['model_components'] - # Otherwise, we need to check the suite question to - # see if there are any model-dependent fields which are not specified - # ---------------------------------------------------------------------------------------------------------------------- - for sub_key, sub_val in question_dictionary[key].items(): - if isinstance(sub_val, Mapping) and 'depends_on_model' in sub_val.keys(): - for model in self.possible_model_components: - if model not in sub_val[ - 'depends_on_model'].keys() and sub_key in value.keys(): - - # If the value is a model-dependent specification, - # grab the value associated with each model, if present - # ------------------------------------------------------------------------------------------------------ - if isinstance(value[sub_key], Mapping) and ( - 'depends_on_model' in value[sub_key].keys()): - if model in value[sub_key]['depends_on_model'].keys(): - question_dictionary[key][sub_key][ - 'depends_on_model'][model] = \ - value[sub_key]['depends_on_model'][model] - else: - question_dictionary[key][sub_key][ - 'depends_on_model'][model] = 'defer_to_model' - - # If the value is not a model-dependent dictionary, - # set the missing model to its value - # ----------------------------------------------------------------------------------- - else: - question_dictionary[key][sub_key][ - 'depends_on_model'][model] = value[sub_key] - - # At this point we can check to see if this is a suite that requires model components - self.suite_needs_model_components = True - if 'model_components' not in question_dictionary.keys(): - self.suite_needs_model_components = False + # Iterate through model independent tasks and update with defaults if not already set + for task in self.model_independent_tasks: + task_options.append(task.base_name) + question_list = task.question_list.expand_question_list() - # Create copy of the question_dictionary for model independent questions - question_dictionary_model_ind = copy.deepcopy(question_dictionary) - - # Iterate through the model_ind dictionary and remove questions associated with models - # and questions not required by the suite - keys_to_remove = [] - for key, val in question_dictionary_model_ind.items(): - if dict_get(self.logger, val, 'models', None) is not None: - keys_to_remove.append(key) - - # Cycle times can be a special case that is needed even when models are not. Though if they - # are then the cycle times are needed for each model component. So we need to check if the - # suite needs cycle_times - - # If there are no models and the cycle_times is in the keys to remove then remove it - if not self.suite_needs_model_components and 'cycle_times' in keys_to_remove: - keys_to_remove.remove('cycle_times') - - # Now remove the keys - for key in keys_to_remove: - del question_dictionary_model_ind[key] - self.question_dictionary_model_ind = copy.deepcopy(question_dictionary_model_ind) - - # If there are no models and the cycle_times is in the keys then remove the models key from - # the cycle_times question dictionary - if 'cycle_times' in self.question_dictionary_model_ind.keys(): - if not self.suite_needs_model_components: - self.question_dictionary_model_ind['cycle_times'].pop('models') - if self.question_dictionary_model_ind['cycle_times']['default_value'] == \ - 'defer_to_model': - self.question_dictionary_model_ind['cycle_times']['default_value'] = 'T00' - - # At this point we can return if there are no model components - if not self.suite_needs_model_components: - return - - # Create copy of the question_dictionary for model dependent questions - question_dictionary_model_dep = copy.deepcopy(question_dictionary) - - # Iterate through the model_dep dictionary and remove questions not associated with models - # and questions not required by the suite - keys_to_remove = [] - for key, val in question_dictionary_model_dep.items(): - if dict_get(self.logger, val, 'models', None) is None: - keys_to_remove.append(key) - for key in keys_to_remove: - del question_dictionary_model_dep[key] - - # Create new questions dictionary for each model component - self.question_dictionary_model_dep = {} - for model in self.possible_model_components: - self.question_dictionary_model_dep[model] = update_dict( - copy.deepcopy(question_dictionary_model_dep), - model_dep_questions_override[model]) + for question in question_list: + question_dict = {question['question_name']: question} - # Remove any questions that are not associated with the model component - for model in self.possible_model_components: - keys_to_remove = [] - for key, val in self.question_dictionary_model_dep[model].items(): - if val['models'] != ['all_models'] and model not in val['models']: - keys_to_remove.append(key) # Remove if not needed by this model + if question['models'] is not None: + model_dict = {} + + for question_model in question['models']: + if question_model == 'all_models': + for model in model_components: + model_dict[model] = question_dict + elif question_model in model_components: + model_dict[question_model] = question_dict + + self.question_dictionary_model_dep = add_dict( + self.question_dictionary_model_dep, model_dict) + + else: + self.question_dictionary_model_ind = add_dict( + self.question_dictionary_model_ind, question_dict) + + # Iterate through model dependent tasks and update if not already set + for model, task_list in self.model_dependent_tasks.items(): + for task in task_list: + task_options.append(task.base_name) - for key in keys_to_remove: - del self.question_dictionary_model_dep[model][key] + question_list = task.question_list.expand_question_list() + + for question in question_list: + question_dict = {question['question_name']: question} + if question['models'] is None: + self.question_dictionary_model_ind = add_dict( + self.question_dictionary_model_ind, question_dict) + elif model in question['models'] or 'all_models' in question['models']: + self.question_dictionary_model_dep = add_dict( + self.question_dictionary_model_dep, {model: question_dict}) + + # Set options for task email parameters + if 'task_email_parameters' in self.question_dictionary_model_ind: + self.question_dictionary_model_ind['task_email_parameters']['options'] = task_options + + # Set options for workflow pause + if 'pause_on_tasks' in self.question_dictionary_model_ind: + self.question_dictionary_model_ind['pause_on_tasks']['options'] = task_options # ---------------------------------------------------------------------------------------------- - def override_with_defaults(self) -> None: + def override_with_defaults(self, suite_task: QuestionType) -> None: # Perform a platform override on the model_ind dictionary # ------------------------------------------------------- yaml = YAML(typ='safe') platform_defaults = {} - for suite_task in ['suite', 'task']: - platform_dict_file = os.path.join(get_swell_path(), 'deployment', 'platforms', - self.platform, f'{suite_task}_questions.yaml') - with open(platform_dict_file, 'r') as ymlfile: - platform_defaults.update(yaml.load(ymlfile)) + + platform_dict_file = os.path.join(get_swell_path(), 'deployment', 'platforms', + self.platform, f'{suite_task.value}_questions.yaml') + with open(platform_dict_file, 'r') as ymlfile: + platform_defaults.update(yaml.load(ymlfile)) # Loop over the keys in self.question_dictionary_model_ind and update with platform_defaults # if that dictionary shares the key for question_name, question in self.question_dictionary_model_ind.items(): - if question_name in platform_defaults.keys(): - for key, val in question.items(): - if val == 'defer_to_platform' and \ - key in platform_defaults[question_name]: - self.question_dictionary_model_ind[question_name][key] = platform_defaults[ - question_name][key] + if question['question_type'] == suite_task: + if question_name in platform_defaults.keys(): + for platform_key, platform_val in platform_defaults[question_name].items(): + if platform_key not in question.keys() or \ + question[platform_key] == 'defer_to_platform': + question[platform_key] = platform_val + + # Construct the dictionary for user defaults + # ------------------------------------------ + user_defaults = {} + settings_file = os.path.expanduser('~/.swell/swell-settings.yaml') + if os.path.exists(settings_file): + with open(settings_file, 'r') as f: + user_defaults = yaml.load(f) + + # See if any questions have user defaults + # --------------------------------------- + for question_name, question in self.question_dictionary_model_ind.items(): + if question['question_type'] == suite_task: + if question_name in user_defaults.keys(): + for user_key, user_val in user_defaults[question_name].items(): + if platform_key not in question.keys() or \ + question[platform_key] == 'defer_to_user': + question[user_key] = user_val # Perform a model override on the model_dep dictionary # ---------------------------------------------------- @@ -298,60 +276,64 @@ def override_with_defaults(self) -> None: # Open the suite and task default dictionaries model_defaults = {} - for suite_task in ['suite', 'task']: - model_dict_file = os.path.join(get_swell_path(), 'configuration', 'jedi', - 'interfaces', model, - f'{suite_task}_questions.yaml') - with open(model_dict_file, 'r') as ymlfile: - model_defaults.update(yaml.load(ymlfile)) + model_dict_file = os.path.join(get_swell_path(), 'configuration', 'jedi', + 'interfaces', model, + f'{suite_task.value}_questions.yaml') + + with open(model_dict_file, 'r') as ymlfile: + model_defaults.update(yaml.load(ymlfile)) # Loop over the keys in self.question_dictionary_model_ind and update with # model_defaults or platform_defaults if that dictionary shares the key for question_name, question in model_dict.items(): - if question_name in model_defaults.keys(): - for key, val in question.items(): - # If the value of the question is still set as model-dependent, - # set the value for that model - if isinstance(val, Mapping) and \ - 'depends_on_model' in val.keys() and \ - model in val['depends_on_model'].keys() and \ - val['depends_on_model'][model] != 'defer_to_model': - - model_dict[question_name][key] = val['depends_on_model'][model] - elif key in model_defaults[question_name].keys() and ( - val == 'defer_to_model' or val is None): - model_dict[question_name][key] = model_defaults[question_name][key] - - if question_name in platform_defaults.keys(): - for key, val in question.items(): - if val == 'defer_to_platform' and \ - key in platform_defaults[question_name]: - model_dict[question_name][key] = platform_defaults[ - question_name][key] + if question['question_type'] == suite_task: + if question_name in model_defaults.keys(): + for key, val in question.items(): + # If the value of the question is still set as model-dependent, + # set the value for that model + if isinstance(val, Mapping) and \ + 'depends_on_model' in val.keys() and \ + model in val['depends_on_model'].keys() and \ + val['depends_on_model'][model] != 'defer_to_model': + + model_dict[question_name][key] = val['depends_on_model'][model] + elif key in model_defaults[question_name].keys() and ( + val == 'defer_to_model' or val is None): + model_dict[question_name][key] = model_defaults[ + question_name][key] + + if question_name in platform_defaults.keys(): + for platform_key, platform_val in \ + platform_defaults[question_name].items(): + if question[platform_key] == 'defer_to_platform': + model_dict[question_name][platform_key] = platform_val # Look for defer_to_code in the model_ind dictionary # -------------------------------------------------- - for key, val in self.question_dictionary_model_ind.items(): - - if key == 'model_components': - if val['default_value'] == 'defer_to_code': - val['default_value'] = self.possible_model_components - if val['options'] == 'defer_to_code': - val['options'] = self.possible_model_components - - if key == 'experiment_id' and val['default_value'] == 'defer_to_code': - val['default_value'] = f'swell-{self.suite}' - - if key == 'r2d2_experiment_id' and val['default_value'] == 'defer_to_code': - swell_id = self.question_dictionary_model_ind['experiment_id']['default_value'] - if swell_id == 'defer_to_code': - swell_id = f'swell-{self.suite}' - self.question_dictionary_model_ind['experiment_id']['default_value'] = swell_id - val['default_value'] = swell_id + for question_name, question in self.question_dictionary_model_ind.items(): + if question['question_type'] == suite_task: + if question_name == 'model_components': + if question['default_value'] == 'defer_to_code': + question['default_value'] = self.possible_model_components + if question['options'] == 'defer_to_code': + question['options'] = self.possible_model_components + + if question_name == 'experiment_id' and question[ + 'default_value'] == 'defer_to_code': + question['default_value'] = f'swell-{self.suite}' + + if question_name == 'r2d2_experiment_id' and \ + question['default_value'] == 'defer_to_code': + swell_id = self.question_dictionary_model_ind['experiment_id']['default_value'] + if swell_id == 'defer_to_code': + swell_id = f'swell-{self.suite}' + self.question_dictionary_model_ind['experiment_id'][ + 'default_value'] = swell_id + question['default_value'] = swell_id # ---------------------------------------------------------------------------------------------- - def override_with_external(self) -> None: + def override_with_external(self, suite_task: QuestionType) -> None: # In this case the user is sending in a dictionary that looks like the experiment # dictionary that they will ultimately be looking at. This means the dictionary does @@ -359,178 +341,75 @@ def override_with_external(self) -> None: # Iterate over the model_ind dictionary and override # -------------------------------------------------- - for key, val in self.question_dictionary_model_ind.items(): - if key in self.override: - val['default_value'] = self.override[key] + for question_name, question in self.question_dictionary_model_ind.items(): + if question['question_type'] == suite_task: + if question_name in self.override: + question['default_value'] = self.override[question_name] # Iterate over the model_dep dictionary and override # -------------------------------------------------- if self.suite_needs_model_components and 'models' in self.override.keys(): for model, model_dict in self.question_dictionary_model_dep.items(): - for key, val in model_dict.items(): - if model in self.override['models']: - if key in self.override['models'][model]: - val['default_value'] = self.override['models'][model][key] + for question_name, question in model_dict.items(): + if question['question_type'] == suite_task: + if model in self.override['models']: + if question_name in self.override['models'][model]: + question['default_value'] = self.override[ + 'models'][model][question_name] # ---------------------------------------------------------------------------------------------- - def ask_questions_and_configure_suite(self) -> Tuple[dict, dict]: - - """ - This is where we ask all the questions and as we go configure the suite file. The process - is rather complex and proceeds as described below. The order is determined by what makes - sense to a user that is going through answering questions. For example we want them to be - able to answer all the questions associated with a certain model together. While there is - work going on behind the scenes to configure the suite file the user should not see a break - in the questioning or a back and forth that causes confusion. + def get_questions_of_type(self, + suite_task: QuestionType, + question_dictionary: Mapping + ) -> Mapping: - 1. Ask the model independent suite questions. + # Get all questions of a certain type + out_dict = {} - 2. Perform a non-exhaustive resolving of suite file templates. Non-exhaustive because at - this point we have not asked the model dependent suite questions so there may be more - templates to resolve. + if 'models' in question_dictionary.keys(): + for model in self.possible_model_components: + if model in question_dictionary['models'].keys(): + out_dict[model] = self.get_questions_of_type( + suite_task, question_dictionary[model]) - 3. Get a list of tasks that do not depend on the model component. + else: + for question_name, question in question_dictionary.items(): + if question['question_type'] == suite_task: + out_dict[question['question_name']] = question - 4. Ask the model independent task questions. + return out_dict - 5. Check that the suite in question has model_components - - 6. Ask the model dependent suite questions. - - 7. Perform an exhaustive resolving of suite file templates. Now it is exhaustive because at - this point we should have all the required information to resolve all the templates. - - 8. Ask the new task questions that do not actually depend on the model.. - - 9.1 Build a list of tasks for each model component. + # ---------------------------------------------------------------------------------------------- - 9.2 Iterate over the model_dep dictionary and ask task questions. - """ + def ask_questions_and_configure(self, suite_task: QuestionType) -> None: + # Handle asking questions for either suites or tasks - # If the client is CLI put out some information about what is due to happen next - if self.config_client.__class__.__name__ == 'GetAnswerCli': + if self.config_client.__class__.__name__ == 'GetAnswerCli' and ( + suite_task == QuestionType.SUITE): self.logger.info("Please answer the following questions to configure your experiment ") - # 1. Iterate over the model_ind dictionary and ask questions - # ---------------------------------------------------------- - for question_key in self.question_dictionary_model_ind: - - # Ask only the suite questions first - # ---------------------------------- - if self.question_dictionary_model_ind[question_key]['question_type'] == 'suite': - - # Ask the question - self.ask_a_question(self.question_dictionary_model_ind, question_key) - - # 2. Perform a non-exhaustive resolving of suite file templates - # ------------------------------------------------------------- - suite_str = template_string_jinja2(self.logger, self.suite_str, self.experiment_dict, True) - - # 3. Get a list of tasks that do not depend on the model component - # ---------------------------------------------------------------- - model_ind_tasks = self.get_suite_task_list_model_ind(suite_str) - - # 4.1 Iterate over the model_ind dictionary and ask task questions - # ---------------------------------------------------------------- - for question_key in self.question_dictionary_model_ind: - - # Ask the task questions - # ---------------------- - if self.question_dictionary_model_ind[question_key]['question_type'] != 'suite': - - # Check if the question is associated with any model independent tasks - if any(question_key in self.questions_per_task[task] for task in model_ind_tasks - if task in self.questions_per_task.keys()): - - # Ask the question - self.ask_a_question(self.question_dictionary_model_ind, question_key) + for question_name, question in self.get_questions_of_type( + suite_task, self.question_dictionary_model_ind).items(): + self.ask_a_question(self.question_dictionary_model_ind, question_name) - # 5. Check that the suite in question has model_components - # -------------------------------------------------------- - if not self.suite_needs_model_components: - return self.experiment_dict, self.questions_dict - - # 6. Iterate over the model_dep dictionary and ask suite questions - # ---------------------------------------------------------------- - - # At this point the user should have provided the model components answer. Check that it is - # in the experiment dictionary and retrieve the response - - if 'model_components' not in self.experiment_dict: - self.logger.abort('The model components question has not been answered.') - - for model in self.experiment_dict['model_components']: - - model_dict = self.question_dictionary_model_dep[model] - - # Loop over keys of each model - for question_key in model_dict: - - # Ask only the suite questions first - if model_dict[question_key]['question_type'] == 'suite': - - # Ask the question - self.ask_a_question(model_dict, question_key, model) - - # 7. Perform a more exhaustive resolving of suite file templates - # -------------------------------------------------------------- - # Note that we reset the suite file to avoid templates having been left unresolved - # (removed) from the previous attempt. We still do not ask for an exhaustive resolving - # of templates because there are things related to scheduling that are not yet able to be - # resolved. In the future it might be good to bring some of that information into the - # sphere of suite questions but that requires some careful thought so as not to overload - # the user with questions. - suite_str = template_string_jinja2(self.logger, self.suite_str, self.experiment_dict, - True) - - # 8. Ask the new task questions that do not actually depend on the model - # ----------------------------------------------------------------------- - for question_key in self.question_dictionary_model_ind: - - if self.question_dictionary_model_ind[question_key]['question_type'] == 'task': - - # Check whether the question is associated with any model dependent tasks - if any(question_key in self.questions_per_task[task] - for task in self.all_model_dep_tasks): - - # Ask the question - self.ask_a_question(self.question_dictionary_model_ind, question_key) - - # 9.1 Build a list of tasks for each model component - # ------------------------------------------------- - model_dep_tasks = self.get_suite_task_list_model_dep(suite_str) - - # 9.2 Iterate over the model_dep dictionary and ask task questions - # ---------------------------------------------------------------- - for model in self.experiment_dict['model_components']: - - # Iterate over the model_dep dictionary and ask questions - # ------------------------------------------------------- - for question_key in self.question_dictionary_model_dep[model]: - - # Ask only the task questions first - # ---------------------------------- - if self.question_dictionary_model_dep[model][ - question_key]['question_type'] == 'task': - - # Check whether any of tasks_dep_per_model are in question_tasks - if any(question_key in self.questions_per_task[task] - for task in model_dep_tasks[model] + model_ind_tasks): - - # Ask the question - self.ask_a_question(self.question_dictionary_model_dep[model], question_key, - model) + if self.suite_needs_model_components: + if 'model_components' not in self.experiment_dict: + self.logger.abort('The model components question has not been answered.') - # Return the main experiment dictionary - return self.experiment_dict, self.questions_dict + for model in self.experiment_dict['model_components']: + model_dict = self.question_dictionary_model_dep[model] + for question_name, question in self.get_questions_of_type( + suite_task, model_dict).items(): + self.ask_a_question(model_dict, question_name, model) # ---------------------------------------------------------------------------------------------- + def ask_a_question( self, full_question_dictionary: dict, question_key: str, - model: Optional[str] = None + model: str | None = None ) -> None: # Set flag for whether the question should be asked @@ -542,10 +421,10 @@ def ask_a_question( if model is not None: if 'models' not in self.experiment_dict: self.experiment_dict['models'] = {} - self.questions_dict['models'] = f"Configurations for the model components." + self.comment_dict['models'] = f"Configurations for the model components." if model not in self.experiment_dict['models']: self.experiment_dict['models'][model] = {} - self.questions_dict[f'models.{model}'] = \ + self.comment_dict[f'models.{model}'] = \ f"Configuration for the {model} model component." # Check the dependency chain for the question @@ -572,15 +451,15 @@ def ask_a_question( if model is None: self.experiment_dict[question_key] = self.config_client.get_answer( self.logger, question_key, qd) - self.questions_dict[question_key] = qd['prompt'] + self.comment_dict[question_key] = qd['prompt'] else: self.experiment_dict['models'][model][question_key] = \ self.config_client.get_answer(self.logger, question_key, qd, model) - self.questions_dict[f'models.{model}.{question_key}'] = qd['prompt'] + self.comment_dict[f'models.{model}.{question_key}'] = qd['prompt'] # ---------------------------------------------------------------------------------------------- - def question_not_been_asked(self, question_key: str, model: str) -> bool: + def question_not_been_asked(self, question_key: str, model: str | None) -> bool: # See if a question has been answered in the experiment dict # Check model independent keys @@ -593,98 +472,4 @@ def question_not_been_asked(self, question_key: str, model: str) -> bool: return True - # ---------------------------------------------------------------------------------------------- - - def get_suite_task_list_model_ind(self, suite_str: str) -> list: - - # Search the suite string for lines containing 'swell task' and not '-m' - swell_task_lines = [line for line in suite_str.split('\n') if 'swell task' in line and - '-m' not in line] - - # Now get the task part - tasks = [] - for line in swell_task_lines: - # Split by 'swell task' - # Remove any leading spaces - # Split by space - tasks.append(line.split('swell task')[1].strip().split(' ')[0]) - - # Ensure there are no duplicate tasks - tasks = list(set(tasks)) - - # Return tasks - return tasks - - # ---------------------------------------------------------------------------------------------- - - def get_all_model_dep_tasks(self, suite_str: str) -> list: - - # Search the suite string for lines containing 'swell task' and '-m' - swell_task_lines = [line for line in suite_str.split('\n') if 'swell task' in line and - '-m' in line] - - # Strip " and spaces from all lines - swell_task_lines = [line.replace('"', '') for line in swell_task_lines] - swell_task_lines = [line.strip() for line in swell_task_lines] - - # All tasks - all_tasks = [] - - for line in swell_task_lines: - all_tasks.append(line.split('swell task ')[1].split(' ')[0]) - - # Ensure all_tasks are unique - all_tasks = list(set(all_tasks)) - - return all_tasks - - # ---------------------------------------------------------------------------------------------- - - def get_suite_task_list_model_dep(self, suite_str: str) -> dict: - - # Search the suite string for lines containing 'swell task' and '-m' - swell_task_lines = [line for line in suite_str.split('\n') if 'swell task' in line and - '-m' in line] - - # Strip " and spaces from all lines - swell_task_lines = [line.replace('"', '') for line in swell_task_lines] - swell_task_lines = [line.strip() for line in swell_task_lines] - - # Now get the model part - models = [] - for line in swell_task_lines: - models.append(line.split('-m')[1].split('0')[0].strip()) - - # Unique models - models = list(set(models)) - - # Assemble dictionary where key is model and val is the tasks that model is associated with - model_tasks = {} - for model in models: - - # Get all elements of swell_task_lines that contains "-m {model}" - model_tasks_this_model = [line for line in swell_task_lines if f'-m {model}' in line] - - # Get task name - tasks = [] - for line in model_tasks_this_model: - tasks.append(line.split('swell task ')[1].split(' ')[0]) - - # Unique model tasks - model_tasks[model] = list(set(tasks)) - - # Return the dictionary - return model_tasks - - # ---------------------------------------------------------------------------------------------- - - def get_dynamic_tasks(self, question_list: list) -> list: - tasks = [] - - for question in question_list: - if question['question_name'] == 'dynamic_task_list': - tasks.extend(question['default_value']) - - return tasks - # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/deployment/prepare_config_and_suite/question_and_answer_cli.py b/src/swell/deployment/prepare_config_and_suite/question_and_answer_cli.py index bc6075020..3f52c0054 100644 --- a/src/swell/deployment/prepare_config_and_suite/question_and_answer_cli.py +++ b/src/swell/deployment/prepare_config_and_suite/question_and_answer_cli.py @@ -10,7 +10,6 @@ import questionary from questionary import Choice -from typing import Optional from swell.utilities.logger import Logger from swell.utilities.swell_questions import WidgetType @@ -21,12 +20,15 @@ class GetAnswerCli: - def get_answer(self, logger: Logger, key: str, val: dict, model: Optional[str] = None): + def get_answer(self, logger: Logger, key: str, val: dict, model: str | None = None): prompt = val['prompt'] default = val['default_value'] widget_type = val['widget_type'] options = val['options'] + if options is None: + options = [] + if model is not None: prompt = f'[{model}] {prompt}' diff --git a/src/swell/deployment/prepare_config_and_suite/question_and_answer_defaults.py b/src/swell/deployment/prepare_config_and_suite/question_and_answer_defaults.py index f600c4198..9bacbe8c8 100644 --- a/src/swell/deployment/prepare_config_and_suite/question_and_answer_defaults.py +++ b/src/swell/deployment/prepare_config_and_suite/question_and_answer_defaults.py @@ -7,16 +7,13 @@ # -------------------------------------------------------------------------------------------------- - -from typing import Union, Optional - from swell.utilities.logger import Logger class GetAnswerDefaults: def get_answer(self, logger: Logger, key: str, val: dict, - model: Optional[str] = None) -> Union[int, float, str]: + model: str | None = None) -> int | float | str: default = val['default_value'] widget_type = val['widget_type'] diff --git a/src/swell/suites/3dfgat_atmos/__init__.py b/src/swell/suites/3dfgat_atmos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dfgat_atmos/suite_config.py b/src/swell/suites/3dfgat_atmos/suite_config.py index c35915186..9245cb06f 100644 --- a/src/swell/suites/3dfgat_atmos/suite_config.py +++ b/src/swell/suites/3dfgat_atmos/suite_config.py @@ -8,101 +8,101 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs +suite_name = '3dfgat_atmos' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +_3dfgat_atmos_tier1 = QuestionList( + questions=[ + common, + qd.start_cycle_point("2023-10-10T00:00:00Z"), + qd.final_cycle_point("2023-10-10T06:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_atmosphere']), + qd.runahead_limit("P2"), + qd.cycling_varbc() + ], + geos_atmosphere=[ + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18" + ]), + qd.horizontal_resolution("91"), + qd.geos_x_background_directory("/discover/nobackup/projects/gmao/" + "dadev/rtodling/archive/Restarts/JEDI/541x"), + qd.window_type("4D"), + qd.observations([ + "aircraft_temperature", + "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "pibal", + "satwind", + "scatwind", + "sfcship", + "sfc", + "sondes", + "ssmis_f17" + ]), + qd.gradient_norm_reduction("1e-3"), + qd.number_of_iterations([10]), + qd.clean_patterns(['*.txt', '*.csv']), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dfgat_atmos_tier1', _3dfgat_atmos_tier1) - _3dfgat_atmos_tier1 = QuestionList( - list_name="3dfgat_atmos", - questions=[ - sq.common, - qd.start_cycle_point("2023-10-10T00:00:00Z"), - qd.final_cycle_point("2023-10-10T06:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_atmosphere']), - qd.runahead_limit("P2"), - ], - geos_atmosphere=[ - qd.cycle_times([ - "T00", - "T06", - "T12", - "T18" - ]), - qd.horizontal_resolution("91"), - qd.geos_x_background_directory("/discover/nobackup/projects/gmao/" - "dadev/rtodling/archive/Restarts/JEDI/541x"), - qd.window_type("4D"), - qd.observations([ - "aircraft_temperature", - "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "pibal", - "satwind", - "scatwind", - "sfcship", - "sfc", - "sondes", - "ssmis_f17" - ]), - qd.gradient_norm_reduction("1e-3"), - qd.number_of_iterations([10]), - qd.clean_patterns(['*.txt', '*.csv']), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +_3dfgat_atmos = QuestionList( + questions=[ + _3dfgat_atmos_tier1 + ] +) - _3dfgat_atmos = QuestionList( - list_name="3dfgat_atmos", - questions=[ - _3dfgat_atmos_tier1 - ] - ) +suite_configs.register(suite_name, '3dfgat_atmos', _3dfgat_atmos) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dfgat_atmos_tier2 = QuestionList( - list_name="3dfgat_atmos_tier2", - questions=[ - _3dfgat_atmos_tier1, - ], - geos_atmosphere=[ - qd.number_of_iterations([100]), - ] - ) +_3dfgat_atmos_tier2 = QuestionList( + questions=[ + _3dfgat_atmos_tier1, + ], + geos_atmosphere=[ + qd.number_of_iterations([100]), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dfgat_atmos_tier2', _3dfgat_atmos_tier2) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dfgat_atmos/flow.cylc b/src/swell/suites/3dfgat_atmos/workflow.py similarity index 54% rename from src/swell/suites/3dfgat_atmos/flow.cylc rename to src/swell/suites/3dfgat_atmos/workflow.py index e46158b88..8fc5ad03b 100644 --- a/src/swell/suites/3dfgat_atmos/flow.cylc +++ b/src/swell/suites/3dfgat_atmos/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based non-cycling variational data assimilation @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -75,14 +86,17 @@ BuildJediByLinking[^]? | BuildJedi[^] => RunJediVariationalExecutable-{{model_component}} CloneJedi[^] => StageJediCycle-{{model_component}} StageJediCycle-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - GetBackgroundGeosExperiment-{{model_component}}? | GetBackground-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + GetBackgroundGeosExperiment-{{model_component}}? | GetBackground-{{model_component}} => + RunJediVariationalExecutable-{{model_component}} + GetObsNotInR2d2-{{model_component}}: fail? => GetObservations-{{model_component}} - GetObsNotInR2d2-{{model_component}}? | GetObservations-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - GenerateObservingSystemRecords-{{model_component}} => RenderJediObservations-{{model_component}} - GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} + + GetObsNotInR2d2-{{model_component}}? | GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} + GenerateObservingSystemRecords-{{model_component}} => RunJediVariationalExecutable-{{model_component}} RenderJediObservations-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + # EvaObservations RunJediVariationalExecutable-{{model_component}} => EvaObservations-{{model_component}} @@ -114,88 +128,59 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - {% for model_component in model_components %} +''' # noqa - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" - - [[GetObsNotInR2d2-{{model_component}}]] - script = "swell task GetObsNotInR2d2 $config -d $datetime -m {{model_component}}" - - [[GenerateObservingSystemRecords-{{model_component}}]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}} ]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetBackgroundGeosExperiment-{{model_component}} ]] - script = "swell task GetBackgroundGeosExperiment $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} +# -------------------------------------------------------------------------------------------------- - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} +@workflows.register('3dfgat_atmos') +class Workflow_3dfgat_atmos(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> None: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.GenerateBClimatologyByLinking(model=model)) + self.tasks.append(ta.GenerateBClimatology(model=model)) + self.tasks.append(ta.GetObsNotInR2d2(model=model)) + self.tasks.append(ta.GetBackgroundGeosExperiment(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dfgat_cycle/__init__.py b/src/swell/suites/3dfgat_cycle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dfgat_marine_cycle/__init__.py b/src/swell/suites/3dfgat_marine_cycle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dfgat_marine_cycle/suite_config.py b/src/swell/suites/3dfgat_marine_cycle/suite_config.py index da954da5c..f0d40b87c 100644 --- a/src/swell/suites/3dfgat_marine_cycle/suite_config.py +++ b/src/swell/suites/3dfgat_marine_cycle/suite_config.py @@ -8,154 +8,153 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +from swell.configuration import question_defaults as qd +from swell.suites.base.suite_questions import marine +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = '3dfgat_marine_cycle' - # -------------------------------------------------------------------------------------------------- +_3dfgat_marine_cycle = QuestionList( + questions=[ + marine, + qd.start_cycle_point("2021-07-02T06:00:00Z"), + qd.final_cycle_point("2021-07-02T12:00:00Z"), + qd.runahead_limit("P2"), + qd.jedi_build_method("use_existing"), + qd.geos_build_method("use_existing"), + qd.model_components(['geos_marine']), + qd.comparison_log_type('fgat'), + ], + geos_marine=[ + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18" + ]), + qd.analysis_variables([ + "sea_water_salinity", + "sea_water_potential_temperature", + "sea_surface_height_above_geoid", + "sea_water_cell_thickness", + "sea_ice_area_fraction", + "sea_ice_thickness", + "sea_ice_snow_thickness" + ]), + qd.window_length("PT6H"), + qd.window_type("4D"), + qd.horizontal_resolution("72x36"), + qd.vertical_resolution("50"), + qd.total_processors(6), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "insitu_profile_argo", + "icec_amsr2_north", + "icec_amsr2_south", + "icec_nsidc_nh", + "icec_nsidc_sh", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_gmi_l3u", + "sst_viirs_n20_l3u", + "temp_profile_xbt" + ]), + qd.number_of_iterations([10]), + qd.mom6_iau(True), + qd.background_time_offset("PT9H"), + qd.clean_patterns([ + "*.nc4", + "*.txt", + "*.rc", + "*.bin" + ]), + ] +) - _3dfgat_marine_cycle = QuestionList( - list_name="3dfgat_marine_cycle", - questions=[ - sq.marine, - qd.start_cycle_point("2021-07-02T06:00:00Z"), - qd.final_cycle_point("2021-07-02T12:00:00Z"), - qd.runahead_limit("P2"), - qd.jedi_build_method("use_existing"), - qd.geos_build_method("use_existing"), - qd.model_components(['geos_marine']), - qd.comparison_log_type('fgat'), - ], - geos_marine=[ - qd.cycle_times([ - "T00", - "T06", - "T12", - "T18" - ]), - qd.analysis_variables([ - "sea_water_salinity", - "sea_water_potential_temperature", - "sea_surface_height_above_geoid", - "sea_water_cell_thickness", - "sea_ice_area_fraction", - "sea_ice_thickness", - "sea_ice_snow_thickness" - ]), - qd.window_length("PT6H"), - qd.window_type("4D"), - qd.horizontal_resolution("72x36"), - qd.vertical_resolution("50"), - qd.total_processors(6), - qd.observations([ - "adt_cryosat2n", - "adt_jason3", - "adt_saral", - "adt_sentinel3a", - "adt_sentinel3b", - "insitu_profile_argo", - "icec_amsr2_north", - "icec_amsr2_south", - "icec_nsidc_nh", - "icec_nsidc_sh", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_gmi_l3u", - "sst_viirs_n20_l3u", - "temp_profile_xbt" - ]), - qd.number_of_iterations([10]), - qd.mom6_iau(True), - qd.background_time_offset("PT9H"), - qd.clean_patterns([ - "*.nc4", - "*.txt", - "*.rc", - "*.bin" - ]), - ] - ) +suite_configs.register(suite_name, '3dfgat_marine_cycle', _3dfgat_marine_cycle) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dfgat_marine_cycle_tier1 = QuestionList( - list_name="3dfgat_marine_cycle_tier1", - questions=[ - _3dfgat_marine_cycle - ] - ) +_3dfgat_marine_cycle_tier1 = QuestionList( + questions=[ + _3dfgat_marine_cycle + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dfgat_marine_cycle_tier1', _3dfgat_marine_cycle_tier1) - _3dfgat_marine_cycle_tier2 = QuestionList( - list_name="3dfgat_marine_cycle_tier2", +# -------------------------------------------------------------------------------------------------- - questions=[ - _3dfgat_marine_cycle, - qd.start_cycle_point("2023-07-02T12:00:00Z"), - qd.final_cycle_point("2023-07-03T12:00:00Z"), - qd.forecast_duration("P2D"), - qd.geos_homdir("/discover/nobackup/projects/gmao/advda/SwellStaticFiles/geos/homdirs/" - "dataatm_025") - ], - geos_marine=[ - qd.cycle_times([ - "T12", - ]), - qd.analysis_variables([ - "sea_water_salinity", - "sea_water_potential_temperature", - "sea_surface_height_above_geoid", - "sea_water_cell_thickness", - "sea_ice_area_fraction", - "sea_ice_thickness", - "sea_ice_snow_thickness" - ]), - qd.window_length("P1D"), - qd.mom6_iau_nhours("PT18H"), - qd.horizontal_resolution("1440x1080"), - qd.vertical_resolution("75"), - qd.total_processors(720), - 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", - "icec_amsr2_north", - "icec_amsr2_south", - "icec_nsidc_nh", - "icec_nsidc_sh", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_avhrrf_mb_l3u", - "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"), - ] - ) +_3dfgat_marine_cycle_tier2 = QuestionList( + questions=[ + _3dfgat_marine_cycle, + qd.start_cycle_point("2023-07-02T12:00:00Z"), + qd.final_cycle_point("2023-07-03T12:00:00Z"), + qd.forecast_duration("P2D"), + qd.geos_homdir("/discover/nobackup/projects/gmao/advda/SwellStaticFiles/geos/homdirs/" + "dataatm_025") + ], + geos_marine=[ + qd.cycle_times([ + "T12", + ]), + qd.analysis_variables([ + "sea_water_salinity", + "sea_water_potential_temperature", + "sea_surface_height_above_geoid", + "sea_water_cell_thickness", + "sea_ice_area_fraction", + "sea_ice_thickness", + "sea_ice_snow_thickness" + ]), + qd.window_length("P1D"), + qd.mom6_iau_nhours("PT18H"), + qd.horizontal_resolution("1440x1080"), + qd.vertical_resolution("75"), + qd.total_processors(720), + 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", + "icec_amsr2_north", + "icec_amsr2_south", + "icec_nsidc_nh", + "icec_nsidc_sh", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_avhrrf_mb_l3u", + "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"), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dfgat_marine_cycle_tier2', _3dfgat_marine_cycle_tier2) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dfgat_marine_cycle/flow.cylc b/src/swell/suites/3dfgat_marine_cycle/workflow.py similarity index 50% rename from src/swell/suites/3dfgat_marine_cycle/flow.cylc rename to src/swell/suites/3dfgat_marine_cycle/workflow.py index ee00e36d0..b496150a2 100644 --- a/src/swell/suites/3dfgat_marine_cycle/flow.cylc +++ b/src/swell/suites/3dfgat_marine_cycle/workflow.py @@ -5,6 +5,18 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows +from swell.suites.forecast_coupled_geos.workflow import RunGeos + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing Geos forecast @@ -15,7 +27,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -120,143 +132,70 @@ {% endfor %} """ {% endfor %} + # -------------------------------------------------------------------------------------------------- [runtime] # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneGeos]] - script = "swell task CloneGeos $config" - - [[BuildGeosByLinking]] - script = "swell task BuildGeosByLinking $config" - - [[BuildGeos]] - script = "swell task BuildGeos $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildGeos"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[RunGeos]] - script = "{{experiment_path}}/GEOSgcm/forecast/gcm_run.j" - platform = {{platform}} - [[[directives]]] - {%- for key, value in scheduling["RunGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PrepCoupledGeosRunDir]] - script = "swell task PrepCoupledGeosRunDir $config -d $datetime" - - [[GetCoupledGeosRestart]] - script = "swell task GetCoupledGeosRestart $config -d $datetime" - - {% for model_component in model_components %} - - [[LinkCoupledGeosOutput-{{model_component}}]] - script = "swell task LinkCoupledGeosOutput $config -d $datetime -m {{model_component}}" - - [[MoveDaRestart-{{model_component}}]] - script = "swell task MoveDaRestart $config -d $datetime -m {{model_component}}" - - [[StageJedi-{{model_component}}]] - script = "swell task StageJedi $config -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[GenerateBClimatology-{{model_component}}]] - script = "swell task GenerateBClimatology $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["GenerateBClimatology"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["GenerateBClimatology"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% if 'cice6' in models["geos_marine"]["marine_models"] %} - - [[RunJediConvertStateSoca2ciceExecutable-{{model_component}}]] - script = "swell task RunJediConvertStateSoca2ciceExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediConvertStateSoca2ciceExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediConvertStateSoca2ciceExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% endif %} - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediFgatExecutable-{{model_component}}]] - script = "swell task RunJediFgatExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediFgatExecutable"]["execution_time_limit"]}} - execution retry delays = 1*PT10M - [[[directives]]] - {%- for key, value in scheduling["RunJediFgatExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveRestart-{{model_component}}]] - script = "swell task SaveRestart $config -d $datetime -m {{model_component}}" - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[PrepareAnalysis-{{model_component}}]] - script = "swell task PrepareAnalysis $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dfgat_marine_cycle') +class Workflow_3dfgat_marine_cycle(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + workflow_str += template_str + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str = template_string_jinja2(self.logger, workflow_str, self.experiment_dict, + allow_unresolved=True) + + return workflow_str + + def set_tasks(self) -> None: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.CloneGeos()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildGeos()) + self.tasks.append(ta.BuildGeosByLinking()) + self.tasks.append(ta.GetCoupledGeosRestart()) + self.tasks.append(ta.PrepCoupledGeosRunDir()) + self.tasks.append(RunGeos()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.RunJediFgatExecutable(model=model)) + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.MoveDaRestart(model=model)) + self.tasks.append(ta.LinkCoupledGeosOutput(model=model)) + self.tasks.append(ta.GenerateBClimatology(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.PrepareAnalysis(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediConvertStateSoca2ciceExecutable(model=model)) + self.tasks.append(ta.SaveRestart(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) + self.tasks.append(ta.PrepareAnalysis(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar/__init__.py b/src/swell/suites/3dvar/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_atmos/__init__.py b/src/swell/suites/3dvar_atmos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_atmos/suite_config.py b/src/swell/suites/3dvar_atmos/suite_config.py index 2bf5ab418..9cbb3df91 100644 --- a/src/swell/suites/3dvar_atmos/suite_config.py +++ b/src/swell/suites/3dvar_atmos/suite_config.py @@ -8,91 +8,90 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_attributes import suite_configs +from swell.suites.base.suite_questions import common +suite_name = '3dvar_atmos' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +_3dvar_atmos_tier1 = QuestionList( + questions=[ + common, + qd.start_cycle_point("2023-10-10T00:00:00Z"), + qd.final_cycle_point("2023-10-10T06:00:00Z"), + qd.runahead_limit("P2"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_atmosphere']), + qd.cycling_varbc(), + ], + geos_atmosphere=[ + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18" + ]), + qd.geos_x_background_directory("/discover/nobackup/projects/gmao/" + "dadev/rtodling/archive/Restarts/JEDI/541x"), + qd.window_length("PT6H"), + qd.window_type("3D"), + qd.horizontal_resolution("91"), + qd.gsibec_nlats("91"), + qd.gsibec_nlons("144"), + qd.vertical_resolution("72"), + qd.observations([ + "aircraft_temperature", + "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "pibal", + "satwind", + "scatwind", + "sfcship", + "sfc", + "sondes", + "ssmis_f17" + ]), + qd.clean_patterns(['*.txt', '*.csv']), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_atmos_tier1', _3dvar_atmos_tier1) - _3dvar_atmos_tier1 = QuestionList( - list_name="3dvar_atmos", - questions=[ - sq.common, - qd.start_cycle_point("2023-10-10T00:00:00Z"), - qd.final_cycle_point("2023-10-10T06:00:00Z"), - qd.runahead_limit("P2"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.cycle_times([ - "T00", - "T06", - "T12", - "T18" - ]), - qd.geos_x_background_directory("/discover/nobackup/projects/gmao/" - "dadev/rtodling/archive/Restarts/JEDI/541x"), - qd.window_length("PT6H"), - qd.window_type("3D"), - qd.horizontal_resolution("91"), - qd.gsibec_nlats("91"), - qd.gsibec_nlons("144"), - qd.vertical_resolution("72"), - qd.observations([ - "aircraft_temperature", - "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "pibal", - "satwind", - "scatwind", - "sfcship", - "sfc", - "sondes", - "ssmis_f17" - ]), - qd.clean_patterns(['*.txt', '*.csv']), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +_3dvar_atmos = QuestionList( + questions=[ + _3dvar_atmos_tier1 + ] +) - _3dvar_atmos = QuestionList( - list_name="3dvar_atmos", - questions=[ - _3dvar_atmos_tier1 - ] - ) +suite_configs.register(suite_name, '3dvar_atmos', _3dvar_atmos) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_atmos/flow.cylc b/src/swell/suites/3dvar_atmos/workflow.py similarity index 58% rename from src/swell/suites/3dvar_atmos/flow.cylc rename to src/swell/suites/3dvar_atmos/workflow.py index 01e042005..015b91faf 100644 --- a/src/swell/suites/3dvar_atmos/flow.cylc +++ b/src/swell/suites/3dvar_atmos/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based non-cycling variational data assimilation @@ -15,6 +26,8 @@ UTC mode = True allow implicit tasks = False +{{stall_timeout}} + # -------------------------------------------------------------------------------------------------- [scheduling] @@ -108,85 +121,59 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" - - [[GenerateObservingSystemRecords-{{model_component}}]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m {{model_component}}" - - [[GetObsNotInR2d2-{{model_component}}]] - script = "swell task GetObsNotInR2d2 $config -d $datetime -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}} ]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetBackgroundGeosExperiment-{{model_component}} ]] - script = "swell task GetBackgroundGeosExperiment $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dvar_atmos') +class Workflow_3dvar_atmos(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetBackgroundGeosExperiment(model=model)) + self.tasks.append(ta.GenerateBClimatologyByLinking(model=model)) + self.tasks.append(ta.GenerateBClimatology(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.GetObsNotInR2d2(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cf/__init__.py b/src/swell/suites/3dvar_cf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_cf/suite_config.py b/src/swell/suites/3dvar_cf/suite_config.py index 93dd6118c..536470314 100644 --- a/src/swell/suites/3dvar_cf/suite_config.py +++ b/src/swell/suites/3dvar_cf/suite_config.py @@ -8,58 +8,57 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +from swell.configuration import question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = '3dvar_cf' + +_3dvar_cf_tier1 = QuestionList( + questions=[ + common, + qd.swell_static_files("/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles"), + qd.start_cycle_point("2023-08-05T18:00:00Z"), + qd.final_cycle_point("2023-08-05T18:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_cf']), + qd.check_for_obs(False) + ], + geos_cf=[ + qd.window_length("PT6H"), + qd.window_type("3D"), + qd.horizontal_resolution("c90"), + qd.npx(91), + qd.npy(91), + qd.npx_proc(2), + qd.npy_proc(2), + qd.vertical_resolution(72), + qd.saber_central_block('bump_nicas'), + qd.saber_outer_block('stddev_bkg_scaled'), + qd.analysis_variables(["volume_mixing_ratio_of_no2"]), + qd.background_experiment("swell_test"), + qd.observations([ + "tempo_no2_tropo", + "tropomi_s5p_no2_tropo", + ]), + qd.clean_patterns(['*.txt', 'logfile.*.out']), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_cf_tier1', _3dvar_cf_tier1) - _3dvar_cf_tier1 = QuestionList( - list_name="3dvar_cf", - questions=[ - sq.common, - qd.swell_static_files("/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles"), - qd.start_cycle_point("2023-08-05T18:00:00Z"), - qd.final_cycle_point("2023-08-05T18:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_cf']), - qd.check_for_obs(False) - ], - geos_cf=[ - qd.window_length("PT6H"), - qd.window_type("3D"), - qd.horizontal_resolution("c90"), - qd.npx(91), - qd.npy(91), - qd.npx_proc(2), - qd.npy_proc(2), - qd.vertical_resolution(72), - qd.saber_central_block('bump_nicas'), - qd.saber_outer_block('stddev_bkg_scaled'), - qd.analysis_variables(["volume_mixing_ratio_of_no2"]), - qd.background_experiment("swell_test"), - qd.observations([ - "tempo_no2_tropo", - "tropomi_s5p_no2_tropo", - ]), - qd.clean_patterns(['*.txt', 'logfile.*.out']), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +_3dvar_cf = QuestionList( + questions=[ + _3dvar_cf_tier1 + ] +) - _3dvar_cf = QuestionList( - list_name="3dvar_cf", - questions=[ - _3dvar_cf_tier1 - ] - ) +suite_configs.register(suite_name, '3dvar_cf', _3dvar_cf) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cf/flow.cylc b/src/swell/suites/3dvar_cf/workflow.py similarity index 56% rename from src/swell/suites/3dvar_cf/flow.cylc rename to src/swell/suites/3dvar_cf/workflow.py index 14a94c94e..4d2afa92b 100644 --- a/src/swell/suites/3dvar_cf/flow.cylc +++ b/src/swell/suites/3dvar_cf/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based non-cycling variational data assimilation @@ -15,6 +26,8 @@ UTC mode = True allow implicit tasks = False +{{stall_timeout}} + # -------------------------------------------------------------------------------------------------- [scheduling] @@ -88,72 +101,52 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}}]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dvar_cf') +class Workflow_hofx_cf(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cf_cycle/__init__.py b/src/swell/suites/3dvar_cf_cycle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_cf_cycle/suite_config.py b/src/swell/suites/3dvar_cf_cycle/suite_config.py index 7cc828346..3fa32ad5a 100644 --- a/src/swell/suites/3dvar_cf_cycle/suite_config.py +++ b/src/swell/suites/3dvar_cf_cycle/suite_config.py @@ -8,104 +8,101 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum - +import swell.configuration.question_defaults as qd +from swell.utilities.swell_questions import QuestionList +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = '3dvar_cf_cycle' - # -------------------------------------------------------------------------------------------------- +_3dvar_cf_cycle_tier1 = QuestionList( + questions=[ + common, + qd.start_cycle_point("2023-08-10T00:00:00Z"), + qd.final_cycle_point("2023-08-10T06:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_cf']), + qd.check_for_obs(False) + ], + geos_cf=[ + qd.window_length("PT6H"), + qd.window_type("3D"), + qd.horizontal_resolution("c90"), + qd.npx(91), + qd.npy(91), + qd.npx_proc(2), + qd.npy_proc(2), + qd.vertical_resolution(72), + qd.saber_central_block('bump_nicas'), + qd.saber_outer_block('stddev_bkg_scaled'), + qd.analysis_variables(["volume_mixing_ratio_of_no2"]), + qd.background_experiment("swell_test"), + qd.rst_experiment("swell_test"), + qd.rst_file_types(['achem_internal', + 'aiau_import', + 'cabc_internal', + 'cabr_internal', + 'caoc_internal', + 'catch_internal', + 'du_internal', + 'fvcore_internal', + 'geoschemchem_import', + 'geoschemchem_internal', + 'gocart_import', + 'gocart_internal', + 'gwd_import', + 'hemco_import', + 'hemco_internal', + 'irrad_internal', + 'lake_internal', + 'landice_internal', + 'moist_import', + 'moist_internal', + 'ni_internal', + 'openwater_internal', + 'pchem_internal', + 'saltwater_import', + 'seaicethermo_internal', + 'solar_internal', + 'ss_internal', + 'su_internal', + 'surf_import', + 'tr_import', + 'tr_internal', + 'turb_import', + 'turb_internal']), + qd.observations([ + "tempo_no2_tropo", + "tropomi_s5p_no2_tropo", + ]), + # forecast settings + # mom6_iau is available. I don't know if we want run 3dvar without iau + qd.iau(True), + qd.forecast_length('PT12H'), + qd.forecast_output_frequency('PT3H'), + qd.clean_patterns(['*.nc4', '*.txt', 'logfile.*.out']), + qd.inc_template( + '/discover/nobackup/mabdiosk/rundir/handle_inc/' + 'GCC_c90_FPens.geoscf_jedi.20210805_0600z.nc4' + ), + qd.geos_cf_install_dir('/discover/nobackup/mabdiosk/GEOS-mil/GEOSgcm/install'), + qd.geos_cf_run_dir('/discover/nobackup/mabdiosk/rundir/GCv14.0_GCMv1.17_c90_Skylab'), + qd.swell_static_files('/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles'), + qd.geosfp_path('/discover/nobackup/projects/gmao/geos_cf_dev/jbarre') + ] +) - _3dvar_cf_cycle_tier1 = QuestionList( - list_name="3dvar_cf_cycle", - questions=[ - sq.common, - qd.start_cycle_point("2023-08-10T00:00:00Z"), - qd.final_cycle_point("2023-08-10T06:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_cf']), - qd.check_for_obs(False) - ], - geos_cf=[ - qd.window_length("PT6H"), - qd.window_type("3D"), - qd.horizontal_resolution("c90"), - qd.npx(91), - qd.npy(91), - qd.npx_proc(2), - qd.npy_proc(2), - qd.vertical_resolution(72), - qd.saber_central_block('bump_nicas'), - qd.saber_outer_block('stddev_bkg_scaled'), - qd.analysis_variables(["volume_mixing_ratio_of_no2"]), - qd.background_experiment("swell_test"), - qd.rst_experiment("swell_test"), - qd.rst_file_types(['achem_internal', - 'aiau_import', - 'cabc_internal', - 'cabr_internal', - 'caoc_internal', - 'catch_internal', - 'du_internal', - 'fvcore_internal', - 'geoschemchem_import', - 'geoschemchem_internal', - 'gocart_import', - 'gocart_internal', - 'gwd_import', - 'hemco_import', - 'hemco_internal', - 'irrad_internal', - 'lake_internal', - 'landice_internal', - 'moist_import', - 'moist_internal', - 'ni_internal', - 'openwater_internal', - 'pchem_internal', - 'saltwater_import', - 'seaicethermo_internal', - 'solar_internal', - 'ss_internal', - 'su_internal', - 'surf_import', - 'tr_import', - 'tr_internal', - 'turb_import', - 'turb_internal']), - qd.observations([ - "tempo_no2_tropo", - "tropomi_s5p_no2_tropo", - ]), - # forecast settings - # mom6_iau is available. I don't know if we want run 3dvar without iau - qd.iau(True), - qd.forecast_length('PT12H'), - qd.forecast_output_frequency('PT3H'), - qd.clean_patterns(['*.nc4', '*.txt', 'logfile.*.out']), - qd.inc_template( - '/discover/nobackup/mabdiosk/rundir/handle_inc/' - 'GCC_c90_FPens.geoscf_jedi.20210805_0600z.nc4' - ), - qd.geos_cf_install_dir('/discover/nobackup/mabdiosk/GEOS-mil/GEOSgcm/install'), - qd.geos_cf_run_dir('/discover/nobackup/mabdiosk/rundir/GCv14.0_GCMv1.17_c90_Skylab'), - qd.swell_static_files('/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles'), - qd.geosfp_path('/discover/nobackup/projects/gmao/geos_cf_dev/jbarre') - ] - ) +suite_configs.register(suite_name, '3dvar_cf_cycle_tier1', _3dvar_cf_cycle_tier1) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dvar_cf_cycle = QuestionList( - list_name="3dvar_cf_cycle", - questions=[ - _3dvar_cf_cycle_tier1 - ] - ) +_3dvar_cf_cycle = QuestionList( + questions=[ + _3dvar_cf_cycle_tier1 + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_cf_cycle', _3dvar_cf_cycle) +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cf_cycle/flow.cylc b/src/swell/suites/3dvar_cf_cycle/workflow.py similarity index 53% rename from src/swell/suites/3dvar_cf_cycle/flow.cylc rename to src/swell/suites/3dvar_cf_cycle/workflow.py index 2ba4fceb9..68bee1dc0 100644 --- a/src/swell/suites/3dvar_cf_cycle/flow.cylc +++ b/src/swell/suites/3dvar_cf_cycle/workflow.py @@ -4,6 +4,18 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.tasks.base.task_setup import TaskSetup +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based non-cycling variational data assimilation @@ -20,7 +32,7 @@ initial cycle point = {{start_cycle_point}} final cycle point = {{final_cycle_point}} - runahead limit = P0 + runahead limit = {{runahead_limit}} [[graph]] R1 = """ @@ -87,7 +99,7 @@ RunJediVariationalExecutable-{{model_component}} => SaveObsDiags-{{model_component}} # Clean up large files - EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & EvaIncrement-{{model_component}} & + EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & EvaIncrement-{{model_component}} & SaveObsDiags-{{model_component}} & SaveForecastCf-{{model_component}} & SaveRestartCf-{{model_component}} => CleanCycle-{{model_component}} {% endif %} @@ -101,99 +113,73 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}}]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[GetRestartCf-{{model_component}}]] - script = "swell task GetRestartCf $config -d $datetime -m {{model_component}}" - - [[PrepForecastCf-{{model_component}}]] - script = "swell task PrepForecastCf $config -d $datetime -m {{model_component}}" - - [[RunForecast-{{model_component}}]] - script = """ - cycle_dir=$(python3 -c 'from swell.utilities.datetime_util import Datetime; import sys; print(Datetime(sys.argv[1]).string_directory())' "$CYLC_TASK_CYCLE_POINT") - scratch_dir="{{experiment_path}}/run/${cycle_dir}/{{model_component}}/scratch" + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +class RunForecast(TaskSetup): + def set_defaults(self): + self.base_name = 'RunForecast' + self.script = """cycle_dir=$(python3 -c 'from swell.utilities.datetime_util import Datetime; import sys; print(Datetime(sys.argv[1]).string_directory())' "$CYLC_TASK_CYCLE_POINT") + scratch_dir="{{experiment_root}}/{{experiment_id}}/run/${cycle_dir}/{{model_component}}/scratch" forecast_log="${scratch_dir}/CFv2_gcm_${SLURM_JOB_ID:-$$}" "${scratch_dir}/gcm_run_geoscf.j" > "${forecast_log}" 2>&1 - """ - platform = {{platform}} - [[[directives]]] - --time = 00:40:00 - --nodes = 8 - --ntasks-per-node = 108 - --job-name = CFv2rc1t13 - --output = /dev/null - - [[SaveForecastCf-{{model_component}}]] - script = "swell task SaveForecastCf $config -d $datetime -m {{model_component}}" - - [[SaveRestartCf-{{model_component}}]] - script = "swell task SaveRestartCf $config -d $datetime -m {{model_component}}" - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + """ # noqa + self.is_cycling = True + self.model_dep = True + self.slurm = {'ntasks-per-node': 108, 'nodes': 8, 'job-name': 'CFv2rc1t13', + 'output': '/dev/null', 'time': '00:40:00'} + + +@workflows.register('3dvar_cf_cycle') +class Workflow_3dvar_cf_cycle(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + workflow_str = template_string_jinja2(self.logger, workflow_str, self.experiment_dict, True) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.GetRestartCf(model=model)) + self.tasks.append(ta.PrepForecastCf(model=model)) + self.tasks.append(RunForecast(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.SaveRestartCf(model=model)) + self.tasks.append(ta.SaveForecastCf(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cycle/__init__.py b/src/swell/suites/3dvar_cycle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_marine/__init__.py b/src/swell/suites/3dvar_marine/__init__.py new file mode 100644 index 000000000..d02359e0e --- /dev/null +++ b/src/swell/suites/3dvar_marine/__init__.py @@ -0,0 +1,12 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +import os + +repo_directory = os.path.dirname(__file__) + +# Set the version for swell +__version__ = '1.20.0' diff --git a/src/swell/suites/3dvar_marine/suite_config.py b/src/swell/suites/3dvar_marine/suite_config.py index c9db952fb..28f20731f 100644 --- a/src/swell/suites/3dvar_marine/suite_config.py +++ b/src/swell/suites/3dvar_marine/suite_config.py @@ -8,63 +8,63 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum - +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_attributes import suite_configs +from swell.suites.base.suite_questions import marine # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = '3dvar_marine' - # -------------------------------------------------------------------------------------------------- +_3dvar_tier1 = QuestionList( + questions=[ + marine, + qd.cycling_varbc(), + qd.start_cycle_point("2021-07-01T12:00:00Z"), + qd.final_cycle_point("2021-07-01T12:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_marine']), + qd.parser_options(), + ], + geos_marine=[ + qd.cycle_times(['T12']), + qd.marine_models(['mom6']), + qd.window_length("P1D"), + qd.horizontal_resolution("72x36"), + qd.vertical_resolution("50"), + qd.total_processors(6), + qd.obs_experiment("s2s_v1"), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "insitu_profile_argo", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_gmi_l3u", + "sst_viirs_n20_l3u", + "temp_profile_xbt" + ]), + qd.background_time_offset("PT18H"), + qd.clean_patterns(['*.nc4', '*.txt']), + ] +) - _3dvar_marine = QuestionList( - list_name="3dvar_marine", - questions=[ - sq.marine, - qd.start_cycle_point("2021-07-01T12:00:00Z"), - qd.final_cycle_point("2021-07-01T12:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_marine']), - ], - geos_marine=[ - qd.cycle_times(['T12']), - qd.marine_models(['mom6']), - qd.window_length("P1D"), - qd.horizontal_resolution("72x36"), - qd.vertical_resolution("50"), - qd.total_processors(6), - qd.obs_experiment("s2s_v1"), - qd.observations([ - "adt_cryosat2n", - "adt_jason3", - "adt_saral", - "adt_sentinel3a", - "adt_sentinel3b", - "insitu_profile_argo", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_gmi_l3u", - "sst_viirs_n20_l3u", - "temp_profile_xbt" - ]), - qd.background_time_offset("PT18H"), - qd.clean_patterns(['*.nc4', '*.txt']), - ] - ) +suite_configs.register(suite_name, '3dvar_marine_tier1', _3dvar_tier1) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dvar_marine_tier1 = QuestionList( - list_name="3dvar_marine_tier1", - questions=[ - _3dvar_marine - ] - ) +_3dvar = QuestionList( + questions=[ + _3dvar_tier1 + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_marine', _3dvar) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_marine/flow.cylc b/src/swell/suites/3dvar_marine/workflow.py similarity index 54% rename from src/swell/suites/3dvar_marine/flow.cylc rename to src/swell/suites/3dvar_marine/workflow.py index 95ab19c8f..e916ec1fc 100644 --- a/src/swell/suites/3dvar_marine/flow.cylc +++ b/src/swell/suites/3dvar_marine/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based non-cycling variational data assimilation @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -48,6 +59,7 @@ {{cycle_time.cycle_time}} = """ {% for model_component in model_components %} {% if cycle_time[model_component] %} + # Task triggers for: {{model_component}} # ------------------ # GenerateBClimatology, for ocean it is cycle dependent @@ -97,85 +109,54 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - [[StageJedi-{{model_component}}]] - script = "swell task StageJedi $config -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[ GetBackground-{{model_component}} ]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[GenerateBClimatology-{{model_component}}]] - script = "swell task GenerateBClimatology $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["GenerateBClimatology"]["execution_time_limit"]}} - execution retry delays = 2*PT1M - [[[directives]]] - {%- for key, value in scheduling["GenerateBClimatology"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dvar_marine') +class Workflow_3dvar_marine(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.GenerateBClimatology(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_marine_cycle/__init__.py b/src/swell/suites/3dvar_marine_cycle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/3dvar_marine_cycle/suite_config.py b/src/swell/suites/3dvar_marine_cycle/suite_config.py index 0a6967502..7022dac3d 100644 --- a/src/swell/suites/3dvar_marine_cycle/suite_config.py +++ b/src/swell/suites/3dvar_marine_cycle/suite_config.py @@ -8,135 +8,134 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import marine +from swell.suites.base.suite_attributes import suite_configs +suite_name = '3dvar_marine_cycle' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +_3dvar_marine_cycle = QuestionList( + questions=[ + marine, + qd.start_cycle_point("2021-07-02T06:00:00Z"), + qd.final_cycle_point("2021-07-02T12:00:00Z"), + qd.runahead_limit("P2"), + qd.jedi_build_method("use_existing"), + qd.geos_build_method("use_existing"), + qd.model_components(['geos_marine']), + qd.comparison_log_type('variational'), + ], + geos_marine=[ + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18", + ]), + qd.window_length("PT6H"), + qd.horizontal_resolution("72x36"), + qd.vertical_resolution("50"), + qd.total_processors(6), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "insitu_profile_argo", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_gmi_l3u", + "sst_viirs_n20_l3u", + "temp_profile_xbt" + ]), + qd.number_of_iterations([10]), + qd.mom6_iau(True), + qd.marine_models(['mom6']), + qd.background_time_offset("PT9H"), + qd.clean_patterns([ + "*.nc4", + "*.txt", + "*.rc", + "*.bin" + ]), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_marine_cycle', _3dvar_marine_cycle) - _3dvar_marine_cycle = QuestionList( - list_name="3dvar_marine_cycle", - questions=[ - sq.marine, - qd.start_cycle_point("2021-07-02T06:00:00Z"), - qd.final_cycle_point("2021-07-02T12:00:00Z"), - qd.runahead_limit("P2"), - qd.jedi_build_method("use_existing"), - qd.geos_build_method("use_existing"), - qd.model_components(['geos_marine']), - qd.comparison_log_type('variational'), - ], - geos_marine=[ - qd.cycle_times([ - "T00", - "T06", - "T12", - "T18", - ]), - qd.window_length("PT6H"), - qd.horizontal_resolution("72x36"), - qd.vertical_resolution("50"), - qd.total_processors(6), - qd.observations([ - "adt_cryosat2n", - "adt_jason3", - "adt_saral", - "adt_sentinel3a", - "adt_sentinel3b", - "insitu_profile_argo", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_gmi_l3u", - "sst_viirs_n20_l3u", - "temp_profile_xbt" - ]), - qd.number_of_iterations([10]), - qd.mom6_iau(True), - qd.marine_models(['mom6']), - qd.background_time_offset("PT9H"), - qd.clean_patterns([ - "*.nc4", - "*.txt", - "*.rc", - "*.bin" - ]), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +_3dvar_marine_cycle_tier1 = QuestionList( + questions=[ + _3dvar_marine_cycle + ] +) - _3dvar_marine_cycle_tier1 = QuestionList( - list_name="3dvar_marine_cycle_tier1", - questions=[ - _3dvar_marine_cycle - ] - ) +suite_configs.register(suite_name, '3dvar_marine_cycle_tier1', _3dvar_marine_cycle_tier1) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dvar_marine_cycle_tier2 = QuestionList( - list_name="3dvar_marine_cycle_tier2", - questions=[ - _3dvar_marine_cycle, - qd.start_cycle_point("2023-07-02T12:00:00Z"), - qd.final_cycle_point("2023-07-02T18:00:00Z"), - qd.forecast_duration("PT12H"), - qd.geos_homdir("/discover/nobackup/projects/gmao/advda/SwellStaticFiles/geos/homdirs/" - "dataatm_025") - ], - geos_marine=[ - qd.cycle_times([ - "T00", - "T06", - "T12", - "T18", - ]), - qd.analysis_variables([ - "sea_water_salinity", - "sea_water_potential_temperature", - "sea_surface_height_above_geoid", - "sea_water_cell_thickness", - ]), - qd.window_length("PT6H"), - qd.horizontal_resolution("1440x1080"), - qd.vertical_resolution("75"), - qd.total_processors(720), - 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", - "sst_ostia", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_avhrrf_mb_l3u", - "sst_avhrrf_mc_l3u", - "sst_viirs_n20_l3u", - "sst_viirs_npp_l3u", - "temp_profile_xbt" - ]), - qd.number_of_iterations([50]), - qd.background_time_offset("PT9H"), - ] - ) +_3dvar_marine_cycle_tier2 = QuestionList( + questions=[ + _3dvar_marine_cycle, + qd.start_cycle_point("2023-07-02T12:00:00Z"), + qd.final_cycle_point("2023-07-02T18:00:00Z"), + qd.forecast_duration("PT12H"), + qd.geos_homdir("/discover/nobackup/projects/gmao/advda/SwellStaticFiles/geos/homdirs/" + "dataatm_025") + ], + geos_marine=[ + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18", + ]), + qd.analysis_variables([ + "sea_water_salinity", + "sea_water_potential_temperature", + "sea_surface_height_above_geoid", + "sea_water_cell_thickness", + ]), + qd.window_length("PT6H"), + qd.horizontal_resolution("1440x1080"), + qd.vertical_resolution("75"), + qd.total_processors(720), + 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", + "sst_ostia", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_avhrrf_mb_l3u", + "sst_avhrrf_mc_l3u", + "sst_viirs_n20_l3u", + "sst_viirs_npp_l3u", + "temp_profile_xbt" + ]), + qd.number_of_iterations([50]), + qd.background_time_offset("PT9H"), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar_marine_cycle_tier2', _3dvar_marine_cycle_tier2) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_marine_cycle/flow.cylc b/src/swell/suites/3dvar_marine_cycle/workflow.py similarity index 50% rename from src/swell/suites/3dvar_marine_cycle/flow.cylc rename to src/swell/suites/3dvar_marine_cycle/workflow.py index 4c7e10aab..02d093285 100644 --- a/src/swell/suites/3dvar_marine_cycle/flow.cylc +++ b/src/swell/suites/3dvar_marine_cycle/workflow.py @@ -5,6 +5,18 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows +from swell.suites.forecast_coupled_geos.workflow import RunGeos + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing Geos forecast @@ -15,7 +27,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -119,143 +131,69 @@ {% endfor %} """ {% endfor %} + # -------------------------------------------------------------------------------------------------- [runtime] # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneGeos]] - script = "swell task CloneGeos $config" - - [[BuildGeosByLinking]] - script = "swell task BuildGeosByLinking $config" - - [[BuildGeos]] - script = "swell task BuildGeos $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildGeos"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[RunGeos]] - script = "{{experiment_path}}/GEOSgcm/forecast/gcm_run.j" - platform = {{platform}} - [[[directives]]] - {%- for key, value in scheduling["RunGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PrepCoupledGeosRunDir]] - script = "swell task PrepCoupledGeosRunDir $config -d $datetime" - - [[GetCoupledGeosRestart]] - script = "swell task GetCoupledGeosRestart $config -d $datetime" - - {% for model_component in model_components %} - - [[LinkCoupledGeosOutput-{{model_component}}]] - script = "swell task LinkCoupledGeosOutput $config -d $datetime -m {{model_component}}" - - [[MoveDaRestart-{{model_component}}]] - script = "swell task MoveDaRestart $config -d $datetime -m {{model_component}}" - - [[StageJedi-{{model_component}}]] - script = "swell task StageJedi $config -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[GenerateBClimatology-{{model_component}}]] - script = "swell task GenerateBClimatology $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["GenerateBClimatology"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["GenerateBClimatology"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% if 'cice6' in models["geos_marine"]["marine_models"] %} - - [[RunJediConvertStateSoca2ciceExecutable-{{model_component}}]] - script = "swell task RunJediConvertStateSoca2ciceExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediConvertStateSoca2ciceExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediConvertStateSoca2ciceExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% endif %} - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediVariationalExecutable-{{model_component}}]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution retry delays = 1*PT5M - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaJediLog-{{model_component}}]] - script = "swell task EvaJediLog $config -d $datetime -m {{model_component}}" - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveRestart-{{model_component}}]] - script = "swell task SaveRestart $config -d $datetime -m {{model_component}}" - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[PrepareAnalysis-{{model_component}}]] - script = "swell task PrepareAnalysis $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dvar_marine_cycle') +class Workflow_3dvar_marine_cycle(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + workflow_str += template_str + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str = template_string_jinja2(self.logger, workflow_str, self.experiment_dict, + allow_unresolved=True) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.CloneGeos()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildGeosByLinking()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildGeos()) + self.tasks.append(ta.GetCoupledGeosRestart()) + self.tasks.append(ta.PrepCoupledGeosRunDir()) + self.tasks.append(RunGeos()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.LinkCoupledGeosOutput(model=model)) + self.tasks.append(ta.GenerateBClimatology(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.PrepareAnalysis(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediConvertStateSoca2ciceExecutable(model=model)) + self.tasks.append(ta.MoveDaRestart(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.EvaJediLog(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) + self.tasks.append(ta.SaveRestart(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/all_suites.py b/src/swell/suites/all_suites.py deleted file mode 100644 index a5bd9983b..000000000 --- a/src/swell/suites/all_suites.py +++ /dev/null @@ -1,93 +0,0 @@ -# -------------------------------------------------------------------------------------------------- -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - -import os -from enum import Enum -from importlib import import_module - -from swell.swell_path import get_swell_path -from swell.utilities.suite_utils import get_suites - -# -------------------------------------------------------------------------------------------------- - - -# Class methods for AllSuites enum - -@classmethod -def get_config(cls, config_name): - return getattr(cls, config_name).value.value - - -@classmethod -def config_names(cls): - return cls._member_names_ - - -@classmethod -def base_suite(cls, config: str) -> str: - return cls.__config_suite_map__[config] - -# -------------------------------------------------------------------------------------------------- - - -def construct_suite_enum(): - # Automatically construct enum of all suite configs - - def format_config_name(config_name): - return config_name[1:] if config_name[0] == '_' else config_name - - def wrapper(suite_config_enum): - # Dictionary used to create the enum - enum_dict = {} - # Map of config names to their parent suites - config_suite_map = {} - - # Find all of the suite configs - for suite in get_suites(): - config_path = os.path.join(get_swell_path(), 'suites', suite, 'suite_config.py') - if os.path.exists(config_path): - suite_container = getattr( - import_module(f'swell.suites.{suite}.suite_config'), 'SuiteConfig') - suite_configs = suite_container.get_all() - - for config in suite_configs: - enum_dict[format_config_name(config)] = getattr(suite_container, config) - config_suite_map[format_config_name(config)] = suite - else: - enum_dict[suite] = SuiteQuestions.all_suites - config_suite_map[suite] = suite - - # Set the map dictionary to a hidden attribute - enum_dict['__config_suite_map__'] = config_suite_map - - # Override with manually specified keys in enum - for item in suite_config_enum: - enum_dict[item.name] = item.value - - # Build the enum - enum_cls = Enum(suite_config_enum.__name__, enum_dict) - - # Set classmethods for the enum - setattr(enum_cls, 'get_config', get_config) - setattr(enum_cls, 'config_names', config_names) - setattr(enum_cls, 'base_suite', base_suite) - - return enum_cls - return wrapper - -# -------------------------------------------------------------------------------------------------- - - -@construct_suite_enum() -class AllSuites(Enum): - pass - - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/base/__init__.py b/src/swell/suites/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/base/cylc_workflow.py b/src/swell/suites/base/cylc_workflow.py new file mode 100644 index 000000000..eeb9273f6 --- /dev/null +++ b/src/swell/suites/base/cylc_workflow.py @@ -0,0 +1,99 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from typing import Tuple +from abc import abstractmethod, ABC + +from swell.utilities.logger import get_logger + +# -------------------------------------------------------------------------------------------------- + + +header_str = '''#!jinja2 +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +''' + + +class CylcWorkflow(ABC): + + """Abstract class setting tasks to be run by the workflow, + as well as specifying the contents of flow.cylc. + + Attributes: + experiment_dict: Mapping of suite config to use in configuring the graph + slurm_external: Mapping of user and global slurm settings + tasks: list of TaskSetup objects which specify questions used by the + suite and cylc runtime attributes + """ + + def __init__(self, experiment_dict, slurm_external) -> None: + self.experiment_dict = experiment_dict + self.slurm_external = slurm_external + + self.logger = get_logger(self.__class__.__name__) + + self.tasks = [] + self.set_tasks() + + # -------------------------------------------------------------------------------------------------- + + def default_header(self) -> str: + """Set the default header, contains copyright information for Swell.""" + return header_str + + # -------------------------------------------------------------------------------------------------- + + @abstractmethod + def set_tasks(self) -> None: + """Abstract method to be overridden by child workflows, sets a list of TaskSetup objects.""" + pass + + # -------------------------------------------------------------------------------------------------- + + def get_independent_and_model_tasks(self) -> Tuple[list, dict]: + """Iterates through tasks and separate questions into model-independent and dependent. + + Returns: + List of model-independent questions. + Mapping of model to list of questions associated with that model. + """ + + ind_tasks = [] + model_tasks = {} + + models = [] + if 'model_components' in self.experiment_dict: + models = self.experiment_dict['model_components'] + else: + models = [] + + for model in models: + model_tasks[model] = [] + + for task in self.tasks: + if task.model is not None: + model_tasks[task.model].append(task) + else: + ind_tasks.append(task) + + return ind_tasks, model_tasks + + # -------------------------------------------------------------------------------------------------- + + @abstractmethod + def get_workflow_string(self) -> str: + """Abstract method containing instructions for constructing flow.cylc contents.""" + return '' + + # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/base/suite_attributes.py b/src/swell/suites/base/suite_attributes.py new file mode 100644 index 000000000..24a21b27d --- /dev/null +++ b/src/swell/suites/base/suite_attributes.py @@ -0,0 +1,106 @@ +# -------------------------------------------------------------------------------------------------- +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.utilities.swell_questions import QuestionList +import swell.suites +from swell.utilities.plugins import discover_plugins + +# -------------------------------------------------------------------------------------------------- + + +def format_suite_name(suite_name): + # Format suite names starting with a digit + return suite_name[1:] if suite_name[0] == '_' else suite_name + +# -------------------------------------------------------------------------------------------------- + + +class Workflows(): + + def __init__(self) -> None: + self.__workflow_names__ = [] + + def register(self, name: str) -> None: + self.__workflow_names__.append(name) + + def wrapper(cls): + setattr(self, name, cls) + return cls + return wrapper + + def get(self, name: str) -> type[CylcWorkflow]: + return getattr(self, name) + + def all(self) -> list: + return self.__workflow_names__ + +# -------------------------------------------------------------------------------------------------- + + +class SuiteConfigs(): + + def __init__(self) -> None: + + # Dictionary tracking the suite for each config + self.__config_map__ = {} + + # -------------------------------------------------------------------------------------------------- + + def register(self, + base_suite: str, + config_name: str, + question_list: QuestionList) -> None: + + self.__config_map__[config_name] = sub_dict = {} + + sub_dict['suite'] = base_suite + sub_dict['list'] = question_list + + # -------------------------------------------------------------------------------------------------- + + def get_config(self, config_name: str) -> QuestionList: + return self.__config_map__[config_name]['list'] + + # -------------------------------------------------------------------------------------------------- + + def base_suite(self, config_name: str) -> str: + return self.__config_map__[config_name]['suite'] + + # -------------------------------------------------------------------------------------------------- + + def all_configs(self) -> str: + return list(self.__config_map__.keys()) + + # -------------------------------------------------------------------------------------------------- + + def configs_under_suites(self) -> dict: + suite_map = {} + + for config_name, config_dict in self.__config_map__.items(): + suite_name = config_dict['suite'] + + if suite_name not in suite_map: + suite_map[suite_name] = [] + + suite_map[suite_name].append(config_name) + + return suite_map + +# -------------------------------------------------------------------------------------------------- + + +# Objects to reference in imports +suite_configs = SuiteConfigs() +workflows = Workflows() + +discover_plugins(swell.suites) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/base/suite_questions.py b/src/swell/suites/base/suite_questions.py new file mode 100644 index 000000000..e3f9a2158 --- /dev/null +++ b/src/swell/suites/base/suite_questions.py @@ -0,0 +1,82 @@ +# -------------------------------------------------------------------------------------------------- +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_attributes import suite_configs + +# -------------------------------------------------------------------------------------------------- +# Shared groups of questions across suites +# -------------------------------------------------------------------------------------------------- + +all_suites = QuestionList( + questions=[ + qd.experiment_id(), + qd.experiment_root(), + qd.pause_on_tasks(), + qd.task_email_parameters(), + qd.email_address() + ] +) + +suite_configs.register('AllSuites', 'AllSuites', all_suites) + +# -------------------------------------------------------------------------------------------------- + +common = QuestionList( + questions=[ + all_suites, + qd.cycle_times(), + qd.start_cycle_point(), + qd.final_cycle_point(), + qd.model_components(), + qd.runahead_limit(), + qd.r2d2_experiment_id(), + qd.mock_experiment(), + qd.skip_r2d2(), + qd.r2d2_server(), + qd.r2d2_datastore() + ] +) + +# -------------------------------------------------------------------------------------------------- + +marine = QuestionList( + questions=[ + common, + qd.marine_models() + ] +) + +# -------------------------------------------------------------------------------------------------- + +compare = QuestionList( + questions=[ + all_suites, + qd.comparison_experiment_paths() + ] +) + +# -------------------------------------------------------------------------------------------------- + +task_minimum = QuestionList( + questions=[ + qd.experiment_id(), + qd.experiment_root(), + qd.comparison_experiment_paths(), + qd.model_components(), + qd.marine_models(), + qd.use_cycle_dir(), + ] +) + +suite_configs.register('task_minimum', 'task_minimum', task_minimum) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_geos/__init__.py b/src/swell/suites/build_geos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/build_geos/flow.cylc b/src/swell/suites/build_geos/flow.cylc deleted file mode 100644 index e72bf7b11..000000000 --- a/src/swell/suites/build_geos/flow.cylc +++ /dev/null @@ -1,59 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for building the GEOS model - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - allow implicit tasks = False - -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - - [[graph]] - R1 = """ - CloneGeos => BuildGeosByLinking? - - BuildGeosByLinking:fail? => BuildGeos - """ - -# -------------------------------------------------------------------------------------------------- - -[runtime] - - # Task defaults - # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneGeos]] - script = "swell task CloneGeos $config" - - [[BuildGeosByLinking]] - script = "swell task BuildGeosByLinking $config" - - [[BuildGeos]] - script = "swell task BuildGeos $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildGeos"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_geos/suite_config.py b/src/swell/suites/build_geos/suite_config.py index a61136668..d3d3d0382 100644 --- a/src/swell/suites/build_geos/suite_config.py +++ b/src/swell/suites/build_geos/suite_config.py @@ -8,23 +8,20 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +from swell.suites.base.suite_questions import all_suites +from swell.suites.base.suite_attributes import suite_configs +suite_name = 'build_geos' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- +build_geos = QuestionList( + questions=[ + all_suites + ] +) - build_geos = QuestionList( - list_name="build_geos", - questions=[ - sq.all_suites - ] - ) +suite_configs.register(suite_name, 'build_geos', build_geos) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_geos/workflow.py b/src/swell/suites/build_geos/workflow.py new file mode 100644 index 000000000..91750ebcd --- /dev/null +++ b/src/swell/suites/build_geos/workflow.py @@ -0,0 +1,82 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' +# -------------------------------------------------------------------------------------------------- + +# Cylc suite for building the GEOS model + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + allow implicit tasks = False + +{{stall_timeout}} + +# -------------------------------------------------------------------------------------------------- + +[scheduling] + + [[graph]] + R1 = """ + CloneGeos => BuildGeosByLinking? + + BuildGeosByLinking:fail? => BuildGeos + """ + +# -------------------------------------------------------------------------------------------------- + +[runtime] + + # Task defaults + # ------------- + +''' + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('build_geos') +class Workflow_build_geos(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneGeos()) + self.tasks.append(ta.BuildGeos()) + self.tasks.append(ta.BuildGeosByLinking()) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_jedi/__init__.py b/src/swell/suites/build_jedi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/build_jedi/flow.cylc b/src/swell/suites/build_jedi/flow.cylc deleted file mode 100644 index c9cb17d1c..000000000 --- a/src/swell/suites/build_jedi/flow.cylc +++ /dev/null @@ -1,59 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for building the JEDI code - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - allow implicit tasks = False - -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - - [[graph]] - R1 = """ - CloneJedi => BuildJediByLinking? - - BuildJediByLinking:fail? => BuildJedi - """ - -# -------------------------------------------------------------------------------------------------- - -[runtime] - - # Task defaults - # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_jedi/suite_config.py b/src/swell/suites/build_jedi/suite_config.py index 4da92ab6b..12897233c 100644 --- a/src/swell/suites/build_jedi/suite_config.py +++ b/src/swell/suites/build_jedi/suite_config.py @@ -8,23 +8,20 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +from swell.suites.base.suite_questions import all_suites +from swell.suites.base.suite_attributes import suite_configs +suite_name = 'build_jedi' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- +build_jedi = QuestionList( + questions=[ + all_suites + ] +) - build_jedi = QuestionList( - list_name="build_jedi", - questions=[ - sq.all_suites - ] - ) +suite_configs.register(suite_name, 'build_jedi', build_jedi) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/build_jedi/workflow.py b/src/swell/suites/build_jedi/workflow.py new file mode 100644 index 000000000..28b3fb1c4 --- /dev/null +++ b/src/swell/suites/build_jedi/workflow.py @@ -0,0 +1,82 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' +# -------------------------------------------------------------------------------------------------- + +# Cylc suite for building the JEDI code + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + allow implicit tasks = False + +{{stall_timeout}} + +# -------------------------------------------------------------------------------------------------- + +[scheduling] + + [[graph]] + R1 = """ + CloneJedi => BuildJediByLinking? + + BuildJediByLinking:fail? => BuildJedi + """ + +# -------------------------------------------------------------------------------------------------- + +[runtime] + + # Task defaults + # ------------- + +''' + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('build_jedi') +class Workflow_build_jedi(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildJediByLinking()) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/compare/__init__.py b/src/swell/suites/compare/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/compare/flow.cylc b/src/swell/suites/compare/flow.cylc deleted file mode 100644 index 8e604e0b1..000000000 --- a/src/swell/suites/compare/flow.cylc +++ /dev/null @@ -1,106 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for running comparison tests on completed experiments - - - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - - initial cycle point = {{start_cycle_point}} - final cycle point = {{final_cycle_point}} - runahead limit = {{runahead_limit}} - - [[graph]] - R1 = """ - {% for model_component in model_components %} - JediLogComparison-{{model_component}}? - {% endfor %} - """ - {% for cycle_time in cycle_times %} - {{cycle_time.cycle_time}} = """ - {% for model_component in model_components %} - {% if cycle_time[model_component] %} - {% for path in comparison_experiment_paths %} - JediOopsLogParser-{{model_component}}-{{ loop.index0 }} - {% endfor %} - JediLogComparison-{{model_component}}[^]:fail? => EvaComparisonIncrement-{{model_component}} => PublishComparisons => comparison_fail - JediLogComparison-{{model_component}}[^]:fail? => EvaComparisonJediLog-{{model_component}} => PublishComparisons => comparison_fail - JediLogComparison-{{model_component}}[^]:fail? => EvaComparisonObservations-{{model_component}} => PublishComparisons => comparison_fail - {% endif %} - {% endfor %} - """ - {% endfor %} - -# -------------------------------------------------------------------------------------------------- - -[runtime] - - # Task defaults - # ------------- - - [[root]] - pre-script = """ - source $CYLC_SUITE_DEF_PATH/modules - """ - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - [[comparison_fail]] - script = "exit 1" - - {% for model_component in model_components %} - [[EvaComparisonIncrement-{{model_component}}]] - script = "swell task EvaComparisonIncrement $config -d $datetime -m {{model_component}}" - - [[EvaComparisonJediLog-{{model_component}}]] - script = "swell task EvaComparisonJediLog $config -d $datetime -m {{model_component}}" - - [[EvaComparisonObservations-{{model_component}}]] - script = "swell task EvaComparisonObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaComparisonObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaComparisonObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PublishComparisons]] - script = "swell task PublishComparisons $config -d $datetime -m {{model_component}}" - - {% if comparison_experiment_paths is mapping %} - {% for path in comparison_experiment_paths.values() %} - [[JediOopsLogParser-{{model_component}}-{{ loop.index0 }}]] - script = "swell task JediOopsLogParser {{path}} -d $datetime -m {{model_component}}" - {% endfor %} - {% else %} - {% for path in comparison_experiment_paths %} - [[JediOopsLogParser-{{model_component}}-{{ loop.index0 }}]] - script = "swell task JediOopsLogParser {{path}} -d $datetime -m {{model_component}}" - {% endfor %} - {% endif %} - - [[JediLogComparison-{{model_component}}]] - script = "swell task JediLogComparison $config -m {{model_component}}" - {% endfor %} - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/compare/suite_config.py b/src/swell/suites/compare/suite_config.py index 7847c366a..474ae64a0 100644 --- a/src/swell/suites/compare/suite_config.py +++ b/src/swell/suites/compare/suite_config.py @@ -7,74 +7,75 @@ # # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList, WidgetType -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq +from swell.utilities.swell_questions import QuestionList, WidgetType +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_attributes import suite_configs +from swell.suites.base.suite_questions import all_suites -from enum import Enum +# -------------------------------------------------------------------------------------------------- + +suite_name = 'compare' + +compare = QuestionList( + questions=[ + all_suites, + qd.comparison_experiment_paths(), + qd.start_cycle_point(default_value=None, widget_type=WidgetType.STRING), + qd.final_cycle_point(default_value=None, widget_type=WidgetType.STRING), + qd.cycle_times(default_value=[None], widget_type=WidgetType.STRING_CHECK_LIST), + qd.model_components(), + qd.runahead_limit(), + ] +) + +suite_configs.register(suite_name, 'compare', compare) + +# -------------------------------------------------------------------------------------------------- + +compare_variational_marine = QuestionList( + questions=[ + compare, + qd.comparison_log_type('variational'), + qd.model_components(['geos_marine']), + ] +) + +suite_configs.register(suite_name, 'compare_variational_marine', compare_variational_marine) + +# -------------------------------------------------------------------------------------------------- + +compare_variational_atmosphere = QuestionList( + questions=[ + compare, + qd.comparison_log_type('variational'), + qd.model_components(['geos_atmosphere']), + ] +) + +suite_configs.register(suite_name, 'compare_variational_atmosphere', compare_variational_atmosphere) # -------------------------------------------------------------------------------------------------- +compare_variational_cf = QuestionList( + questions=[ + compare, + qd.comparison_log_type('variational'), + qd.model_components(['geos_cf']), + ] +) -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - - compare = QuestionList( - list_name="compare", - questions=[ - sq.all_suites, - qd.comparison_experiment_paths(), - qd.start_cycle_point(default_value=None, widget_type=WidgetType.STRING), - qd.final_cycle_point(default_value=None, widget_type=WidgetType.STRING), - qd.cycle_times(default_value=[None], widget_type=WidgetType.STRING_CHECK_LIST), - qd.model_components(), - qd.runahead_limit(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - compare_variational_marine = QuestionList( - list_name="compare_variational_marine", - questions=[ - compare, - qd.comparison_log_type('variational'), - qd.model_components(['geos_marine']), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - compare_variational_atmosphere = QuestionList( - list_name="compare_variational_atmosphere", - questions=[ - compare, - qd.comparison_log_type('variational'), - qd.model_components(['geos_atmosphere']), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - compare_variational_cf = QuestionList( - list_name="compare_variational_cf", - questions=[ - compare, - qd.comparison_log_type('variational'), - qd.model_components(['geos_cf']), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - compare_fgat_marine = QuestionList( - list_name="compare_fgat_marine", - questions=[ - compare, - qd.comparison_log_type('fgat'), - qd.model_components(['geos_marine']), - ] - ) - - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, 'compare_variational_cf', compare_variational_cf) + +# -------------------------------------------------------------------------------------------------- + +compare_fgat_marine = QuestionList( + questions=[ + compare, + qd.comparison_log_type('fgat'), + qd.model_components(['geos_marine']), + ] +) + +suite_configs.register(suite_name, 'compare_fgat_marine', compare_fgat_marine) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/compare/workflow.py b/src/swell/suites/compare/workflow.py new file mode 100644 index 000000000..9c764b94b --- /dev/null +++ b/src/swell/suites/compare/workflow.py @@ -0,0 +1,123 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +import yaml + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' +# -------------------------------------------------------------------------------------------------- + +# Cylc suite for running comparison tests on completed experiments + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + UTC mode = True + allow implicit tasks = False + +{{stall_timeout}} + +# -------------------------------------------------------------------------------------------------- + +[scheduling] + + initial cycle point = {{start_cycle_point}} + final cycle point = {{final_cycle_point}} + runahead limit = {{runahead_limit}} + + [[graph]] + {% for cycle_time in cycle_times %} + {{cycle_time.cycle_time}} = """ + {% for model_component in model_components %} + {% if cycle_time[model_component] %} + {% for path in comparison_experiment_paths %} + JediOopsLogParser-{{model_component}}-{{ loop.index0 }} + {% endfor %} + JediLogComparison-{{model_component}}? + JediLogComparison-{{model_component}}:fail? => EvaComparisonIncrement-{{model_component}} => PublishComparisons => comparison_fail + JediLogComparison-{{model_component}}:fail? => EvaComparisonJediLog-{{model_component}} => PublishComparisons => comparison_fail + JediLogComparison-{{model_component}}:fail? => EvaComparisonObservations-{{model_component}} => PublishComparisons => comparison_fail + {% endif %} + {% endfor %} + """ + {% endfor %} + +# -------------------------------------------------------------------------------------------------- + +[runtime] + + # Task defaults + # ------------- + + [[comparison_fail]] + script = "exit 1" + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('compare') +class Workflow_compare(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + return workflow_str + + def set_tasks(self) -> list: + + paths = self.experiment_dict['comparison_experiment_paths'] + + for path in paths: + with open(path, 'r') as f: + config_dict = yaml.safe_load(f) + for model in self.experiment_dict['model_components']: + num_of_iterations = config_dict['models'][model]['number_of_iterations'] + + self.experiment_dict['models'][model]['number_of_iterations'] = num_of_iterations + + self.tasks.append(ta.root()) + self.tasks.append(ta.PublishComparisons()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.EvaComparisonObservations(model=model)) + self.tasks.append(ta.EvaComparisonIncrement(model=model)) + self.tasks.append(ta.EvaComparisonJediLog(model=model)) + self.tasks.append(ta.JediLogComparison(model=model)) + self.tasks.append(ta.PublishComparisons(model=model)) + + for i, path in enumerate(paths): + log_parser = ta.JediOopsLogParser(model=model) + log_parser.scheduling_name = f'JediOopsLogParser-{model}-{i}' + log_parser.script = (f'swell task JediOopsLogParser {paths[i]}' + f' -d $datetime -m {model}') + self.tasks.append(log_parser) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/convert_bufr/__init__.py b/src/swell/suites/convert_bufr/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/convert_bufr/suite_config.py b/src/swell/suites/convert_bufr/suite_config.py index 27e9d3e79..bcb28c95b 100644 --- a/src/swell/suites/convert_bufr/suite_config.py +++ b/src/swell/suites/convert_bufr/suite_config.py @@ -8,64 +8,62 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = 'convert_bufr' - # -------------------------------------------------------------------------------------------------- +convert_bufr = QuestionList( + questions=[ + common, + qd.start_cycle_point("2023-10-10T00:00:00Z"), + qd.final_cycle_point("2023-10-10T06:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.cycle_times(['T00', 'T06', 'T12', 'T18']), + qd.clean_patterns([ + "gsi_bcs/*.nc4", + "gsi_bcs/*.txt", + "ioda/*/temporary*.nc", + ]), + qd.bufr_obs_classes([ + "ncep_1bamua_bufr", + "ncep_atms_bufr", + "ncep_avcsam_bufr", + "ncep_avcspm_bufr", + "ncep_mhs_bufr", + "ncep_mtiasi_bufr", + "ncep_gpsro_bufr", + # "ncep_ssmis_bufr", + # "ncep_crisfsr_bufr", DNE in 2023 + # "ncep_acftpfl_bufr", + # "disc_airs_bufr", + # "disc_amsua_bufr", + # "gmao_amsr2_bufr", + # "gmao_gmi_bufr", + # "gmao_mlst_bufr", + # "m2scr_n21_ompslp_nc", + # "mls_nrt_nc", + # "ncep_acftpfl_bufr", + # "ncep_aura_omi_bufr", + # "ncep_goesfv_bufr", + # "ncep_gpsro_bufr", + # "ncep_prep_bufr", + # "ncep_satwnd_bufr", + # "ncep_tcvitals", + # "npp_ompsnm_bufr", + # "r21c_npp_ompslp_nc", + ]), + ] +) - convert_bufr = QuestionList( - list_name="convert_bufr", - questions=[ - sq.common, - qd.start_cycle_point("2023-10-10T00:00:00Z"), - qd.final_cycle_point("2023-10-10T06:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.cycle_times(['T00', 'T06', 'T12', 'T18']), - qd.clean_patterns([ - "gsi_bcs/*.nc4", - "gsi_bcs/*.txt", - "ioda/*/temporary*.nc", - ]), - qd.bufr_obs_classes([ - "ncep_1bamua_bufr", - "ncep_atms_bufr", - "ncep_avcsam_bufr", - "ncep_avcspm_bufr", - "ncep_mhs_bufr", - "ncep_mtiasi_bufr", - "ncep_gpsro_bufr", - # "ncep_ssmis_bufr", - # "ncep_crisfsr_bufr", DNE in 2023 - # "ncep_acftpfl_bufr", - # "disc_airs_bufr", - # "disc_amsua_bufr", - # "gmao_amsr2_bufr", - # "gmao_gmi_bufr", - # "gmao_mlst_bufr", - # "m2scr_n21_ompslp_nc", - # "mls_nrt_nc", - # "ncep_acftpfl_bufr", - # "ncep_aura_omi_bufr", - # "ncep_goesfv_bufr", - # "ncep_gpsro_bufr", - # "ncep_prep_bufr", - # "ncep_satwnd_bufr", - # "ncep_tcvitals", - # "npp_ompsnm_bufr", - # "r21c_npp_ompslp_nc", - ]), - ] +suite_configs.register(suite_name, 'convert_bufr', convert_bufr) - ) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/convert_bufr/flow.cylc b/src/swell/suites/convert_bufr/workflow.py similarity index 57% rename from src/swell/suites/convert_bufr/flow.cylc rename to src/swell/suites/convert_bufr/workflow.py index 9f22a442a..fdf916475 100644 --- a/src/swell/suites/convert_bufr/flow.cylc +++ b/src/swell/suites/convert_bufr/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing geos_atmosphere ObsFilters tests @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -74,47 +85,47 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml +''' # noqa + +# -------------------------------------------------------------------------------------------------- - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - [[CloneGmaoPerllib]] - script = "swell task CloneGmaoPerllib $config" +@workflows.register('convert_bufr') +class Workflow_convert_bufr(CylcWorkflow): - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" + def get_workflow_string(self): + workflow_str = self.default_header() - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" - {% for model_component in model_components %} + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) - [[GetBufr-{{model_component}}]] - script = "swell task GetBufr $config -d $datetime -m {{model_component}}" + return workflow_str - [[BufrToIoda-{{model_component}}]] - script = "swell task BufrToIoda $config -d $datetime -m {{model_component}}" + def set_tasks(self) -> list: - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.CloneGmaoPerllib()) - {% endfor %} + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.GetBufr(model=model)) + self.tasks.append(ta.BufrToIoda(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/convert_ncdiags/__init__.py b/src/swell/suites/convert_ncdiags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/convert_ncdiags/suite_config.py b/src/swell/suites/convert_ncdiags/suite_config.py index c8669546c..15a7f8971 100644 --- a/src/swell/suites/convert_ncdiags/suite_config.py +++ b/src/swell/suites/convert_ncdiags/suite_config.py @@ -8,88 +8,87 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = 'convert_ncdiags' + +convert_ncdiags_tier1 = QuestionList( + questions=[ + common, + qd.start_cycle_point("2021-12-12T00:00:00Z"), + qd.final_cycle_point("2021-12-12T06:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.bundles("REMOVE"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.cycle_times(['T00', 'T06']), + qd.clean_patterns([ + "gsi_bcs/*.nc4", + "gsi_bcs/*.txt", + "gsi_bcs/*.yaml", + "gsi_bcs", + "gsi_ncdiags/*.nc4", + "gsi_ncdiags/aircraft/*.nc4", + "gsi_ncdiags/aircraft", + "gsi_ncdiags" + ]), + qd.observations([ + "aircraft", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "pibal", + "satwind", + "scatwind", + "sfcship", + "sfc", + "sondes", + "ssmis_f17" + ]), + qd.path_to_gsi_nc_diags("/discover/nobackup/projects/gmao/advda/SwellTestData/" + "ufo_testing/ncdiagv2/%Y%m%d%H"), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, 'convert_ncdiags_tier1', convert_ncdiags_tier1) - convert_ncdiags_tier1 = QuestionList( - list_name="convert_ncdiags", - questions=[ - sq.common, - qd.start_cycle_point("2021-12-12T00:00:00Z"), - qd.final_cycle_point("2021-12-12T06:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.bundles("REMOVE"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.cycle_times(['T00', 'T06']), - qd.clean_patterns([ - "gsi_bcs/*.nc4", - "gsi_bcs/*.txt", - "gsi_bcs/*.yaml", - "gsi_bcs", - "gsi_ncdiags/*.nc4", - "gsi_ncdiags/aircraft/*.nc4", - "gsi_ncdiags/aircraft", - "gsi_ncdiags" - ]), - qd.observations([ - "aircraft", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "pibal", - "satwind", - "scatwind", - "sfcship", - "sfc", - "sondes", - "ssmis_f17" - ]), - qd.path_to_gsi_nc_diags("/discover/nobackup/projects/gmao/advda/SwellTestData/" - "ufo_testing/ncdiagv2/%Y%m%d%H"), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +convert_ncdiags = QuestionList( + questions=[ + convert_ncdiags_tier1 + ] +) - convert_ncdiags = QuestionList( - list_name="convert_ncdiags", - questions=[ - convert_ncdiags_tier1 - ] - ) +suite_configs.register(suite_name, 'convert_ncdiags', convert_ncdiags) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/convert_ncdiags/flow.cylc b/src/swell/suites/convert_ncdiags/workflow.py similarity index 52% rename from src/swell/suites/convert_ncdiags/flow.cylc rename to src/swell/suites/convert_ncdiags/workflow.py index a6e016f5d..b0bd75ac2 100644 --- a/src/swell/suites/convert_ncdiags/flow.cylc +++ b/src/swell/suites/convert_ncdiags/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing geos_atmosphere ObsFilters tests @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -63,43 +74,45 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml +''' + +# -------------------------------------------------------------------------------------------------- + - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" +@workflows.register('convert_ncdiags') +class Workflow_convert_ncdiags(CylcWorkflow): - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" + def get_workflow_string(self): + workflow_str = self.default_header() - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" - [[ GetGsiBc ]] - script = "swell task GetGsiBc $config -d $datetime -m geos_atmosphere" + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) - [[ GsiBcToIoda ]] - script = "swell task GsiBcToIoda $config -d $datetime -m geos_atmosphere" + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) - [[ GetGsiNcdiag ]] - script = "swell task GetGsiNcdiag $config -d $datetime -m geos_atmosphere" + return workflow_str - [[ GsiNcdiagToIoda ]] - script = "swell task GsiNcdiagToIoda $config -d $datetime -m geos_atmosphere" + def set_tasks(self) -> list: - [[CleanCycle]] - script = "swell task CleanCycle $config -d $datetime -m geos_atmosphere" + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.GetGsiBc()) + self.tasks.append(ta.GsiBcToIoda()) + self.tasks.append(ta.GetGsiNcdiag()) + self.tasks.append(ta.GsiNcdiagToIoda()) + self.tasks.append(ta.CleanCycle()) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/eva_capabilities/__init__.py b/src/swell/suites/eva_capabilities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/eva_capabilities/suite_config.py b/src/swell/suites/eva_capabilities/suite_config.py index c48749be9..3a9c266a1 100644 --- a/src/swell/suites/eva_capabilities/suite_config.py +++ b/src/swell/suites/eva_capabilities/suite_config.py @@ -8,100 +8,100 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import marine +from swell.suites.base.suite_attributes import suite_configs -from enum import Enum +# -------------------------------------------------------------------------------------------------- +suite_name = 'eva_capabilities' -# -------------------------------------------------------------------------------------------------- +eva_capabilities = QuestionList( + questions=[ + marine, + qd.start_cycle_point("2021-07-02T06:00:00Z"), + qd.final_cycle_point("2021-07-03T06:00:00Z"), + qd.model_components(['geos_marine']), + ], + geos_marine=[ + qd.cycle_times(['T00', 'T06', 'T12', 'T18']), + qd.window_length("PT6H"), + qd.observations([ + "adt_cryosat2n", + "adt_jason3", + "adt_saral", + "adt_sentinel3a", + "adt_sentinel3b", + "insitu_profile_argo", + "sss_smos", + "sss_smapv5", + "sst_abi_g16_l3c", + "sst_gmi_l3u", + "sst_viirs_n20_l3u", + "temp_profile_xbt" + ]), + qd.ncdiag_experiments(['fgat_jra55_01']), + qd.clean_patterns(['*.nc4', '*.txt']), + ] +) -class SuiteConfig(QuestionContainer, Enum): +suite_configs.register(suite_name, 'eva_capabilities', eva_capabilities) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - eva_capabilities = QuestionList( - list_name="eva_capabilities", - questions=[ - sq.marine, - qd.start_cycle_point("2021-07-02T06:00:00Z"), - qd.final_cycle_point("2021-07-03T06:00:00Z"), - qd.model_components(['geos_marine']), - ], - geos_marine=[ - qd.cycle_times(['T00', 'T06', 'T12', 'T18']), - qd.window_length("PT6H"), - qd.observations([ - "adt_cryosat2n", - "adt_jason3", - "adt_saral", - "adt_sentinel3a", - "adt_sentinel3b", - "insitu_profile_argo", - "sss_smos", - "sss_smapv5", - "sst_abi_g16_l3c", - "sst_gmi_l3u", - "sst_viirs_n20_l3u", - "temp_profile_xbt" - ]), - qd.ncdiag_experiments(['fgat_jra55_01']), - qd.clean_patterns(['*.nc4', '*.txt']), - ] - ) +eva_capabilities_atmosphere = QuestionList( + questions=[ + eva_capabilities, + qd.start_cycle_point("2023-10-10T00:00:00Z"), + qd.final_cycle_point("2023-10-10T06:00:00Z"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.cycle_times(['T00', 'T06', 'T12', 'T18']), + qd.observations([ + "abi_g16", + "abi_g18", + # "aircraft_temperature", + # "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + # "amsua_n18", + # "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + # "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + # "mls55_aura", + # "omi_aura", + # "ompsnm_npp", + # "pibal", + "satwind", + "scatwind", + "sfcship", + "sfc", + "sondes", + "ssmis_f17" + ]), + qd.ncdiag_experiments(['x0050_fgat']), + qd.clean_patterns(['*.txt', '*.csv']), + ] +) - eva_capabilities_atmosphere = QuestionList( - list_name="eva_capabilities_atmosphere", - questions=[ - eva_capabilities, - qd.start_cycle_point("2023-10-10T00:00:00Z"), - qd.final_cycle_point("2023-10-10T06:00:00Z"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.cycle_times(['T00', 'T06', 'T12', 'T18']), - qd.observations([ - "abi_g16", - "abi_g18", - # "aircraft_temperature", - # "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - # "amsua_n18", - # "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - # "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - # "mls55_aura", - # "omi_aura", - # "ompsnm_npp", - # "pibal", - "satwind", - "scatwind", - "sfcship", - "sfc", - "sondes", - "ssmis_f17" - ]), - qd.ncdiag_experiments(['x0050_fgat']), - qd.clean_patterns(['*.txt', '*.csv']), - ] - ) +suite_configs.register(suite_name, 'eva_capabilities_atmosphere', eva_capabilities_atmosphere) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/eva_capabilities/flow.cylc b/src/swell/suites/eva_capabilities/workflow.py similarity index 53% rename from src/swell/suites/eva_capabilities/flow.cylc rename to src/swell/suites/eva_capabilities/workflow.py index 89516bf66..2b0e4dac4 100644 --- a/src/swell/suites/eva_capabilities/flow.cylc +++ b/src/swell/suites/eva_capabilities/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing geos_atmosphere ObsFilters tests @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -59,37 +70,42 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - {% for model_component in model_components %} - - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" - - [[GenerateObservingSystemRecords-{{model_component}}]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m {{model_component}}" - - [[EvaTimeseries-{{model_component}}]] - script = "swell task EvaTimeseries $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaTimeseries"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaTimeseries"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[GetNcdiags-{{model_component}}]] - script = "swell task GetNcdiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('eva_capabilities') +class Workflow_eva_capabilities(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.GetNcdiags(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.EvaTimeseries(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/forecast_coupled_geos/__init__.py b/src/swell/suites/forecast_coupled_geos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/forecast_coupled_geos/flow.cylc b/src/swell/suites/forecast_coupled_geos/flow.cylc deleted file mode 100644 index b2f9cf2b7..000000000 --- a/src/swell/suites/forecast_coupled_geos/flow.cylc +++ /dev/null @@ -1,112 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for Geos forecast without DA - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - - initial cycle point = {{start_cycle_point}} - final cycle point = {{final_cycle_point}} - - [[graph]] - R1 = """ - # Triggers for non cycle time dependent tasks - # ------------------------------------------- - # Clone Geos source code - CloneGeos - - # Build Geos source code by linking - CloneGeos => BuildGeosByLinking? - - # If not able to link to build create the build - BuildGeosByLinking:fail? => BuildGeos - - # Need first set of restarts to run model - GetCoupledGeosRestart => PrepCoupledGeosRunDir - - # Get first set of restarts - BuildGeosByLinking? | BuildGeos => RunGeos - """ - - {% for cycle_time in cycle_times %} - {{cycle_time}} = """ - - # Run Geos Executable - PrepCoupledGeosRunDir => RunGeos - MoveForecastRestart[-PT6H] => PrepCoupledGeosRunDir - - # Move restart to next cycle - RunGeos => MoveForecastRestart - - """ - {% endfor %} - -# -------------------------------------------------------------------------------------------------- - -[runtime] - - # Task defaults - # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneGeos]] - script = "swell task CloneGeos $config" - - [[BuildGeosByLinking]] - script = "swell task BuildGeosByLinking $config" - - [[BuildGeos]] - script = "swell task BuildGeos $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildGeos"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PrepCoupledGeosRunDir]] - script = "swell task PrepCoupledGeosRunDir $config -d $datetime" - - [[GetCoupledGeosRestart]] - script = "swell task GetCoupledGeosRestart $config -d $datetime" - - [[MoveForecastRestart]] - script = "swell task MoveForecastRestart $config -d $datetime" - - [[SaveRestart]] - script = "swell task SaveRestart $config -d $datetime" - - [[RunGeos]] - script = "{{experiment_path}}/GEOSgcm/forecast/gcm_run.j" - platform = {{platform}} - [[[job]]] - shell = /bin/csh - [[[directives]]] - {%- for key, value in scheduling["RunGeos"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/forecast_coupled_geos/suite_config.py b/src/swell/suites/forecast_coupled_geos/suite_config.py index 8d2ad5e48..3dcf4742a 100644 --- a/src/swell/suites/forecast_coupled_geos/suite_config.py +++ b/src/swell/suites/forecast_coupled_geos/suite_config.py @@ -8,40 +8,44 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import all_suites +from swell.suites.base.suite_attributes import suite_configs -from enum import Enum +# -------------------------------------------------------------------------------------------------- +suite_name = 'forecast_coupled_geos' + +forecast_geos_tier1 = QuestionList( + questions=[ + all_suites, + qd.cycle_times(), + qd.final_cycle_point(), + qd.start_cycle_point(), + qd.start_cycle_point("2021-06-20T00:00:00Z"), + qd.final_cycle_point("2021-06-21T00:00:00Z"), + qd.cycle_times([ + "T00", + "T06", + "T12", + "T18" + ]), + qd.geos_build_method("use_existing"), + qd.forecast_duration("PT6H"), + ], +) + +suite_configs.register(suite_name, 'forecast_coupled_geos_tier1', forecast_geos_tier1) # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - - forecast_coupled_geos = QuestionList( - list_name="forecast_coupled_geos", - questions=[ - sq.all_suites, - qd.start_cycle_point("2021-07-02T12:00:00Z"), - qd.final_cycle_point("2021-07-03T12:00:00Z"), - qd.cycle_times([ - "T12", - ]), - qd.geos_build_method("use_existing"), - qd.forecast_duration("P1D"), - ], - ) - - # -------------------------------------------------------------------------------------------------- - - forecast_coupled_geos_tier1 = QuestionList( - list_name="forecast_coupled_geos_tier1", - questions=[ - forecast_coupled_geos - ] - ) - - # -------------------------------------------------------------------------------------------------- +forecast_geos = QuestionList( + questions=[ + forecast_geos_tier1 + ] +) + +suite_configs.register(suite_name, 'forecast_coupled_geos', forecast_geos) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/forecast_coupled_geos/workflow.py b/src/swell/suites/forecast_coupled_geos/workflow.py new file mode 100644 index 000000000..b1fbff6f3 --- /dev/null +++ b/src/swell/suites/forecast_coupled_geos/workflow.py @@ -0,0 +1,127 @@ +#!jinja2 +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.tasks.base.task_setup import TaskSetup +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' +# -------------------------------------------------------------------------------------------------- + +# Cylc suite for Geos forecast without DA + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + UTC mode = True + allow implicit tasks = False + +{{stall_timeout}} + +# -------------------------------------------------------------------------------------------------- + +[scheduling] + + initial cycle point = {{start_cycle_point}} + final cycle point = {{final_cycle_point}} + + [[graph]] + R1 = """ + # Triggers for non cycle time dependent tasks + # ------------------------------------------- + # Clone Geos source code + CloneGeos + + # Build Geos source code by linking + CloneGeos => BuildGeosByLinking? + + # If not able to link to build create the build + BuildGeosByLinking:fail? => BuildGeos + + # Need first set of restarts to run model + GetCoupledGeosRestart => PrepCoupledGeosRunDir + + # Get first set of restarts + BuildGeosByLinking? | BuildGeos => RunGeos + """ + + {% for cycle_time in cycle_times %} + {{cycle_time}} = """ + + # Run Geos Executable + PrepCoupledGeosRunDir => RunGeos + MoveForecastRestart[-PT6H] => PrepCoupledGeosRunDir + + # Move restart to next cycle + RunGeos => MoveForecastRestart + + """ + {% endfor %} + +# -------------------------------------------------------------------------------------------------- + +[runtime] + + # Task defaults + # ------------- + +''' + + +class RunGeos(TaskSetup): + def set_defaults(self): + self.base_name = 'RunGeos' + self.is_cycling = True + self.script = '{{experiment_root}}/{{experiment_id}}/GEOSgcm/forecast/gcm_run.j' + self.slurm = {} + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('forecast_coupled_geos') +class Workflow_forecast_coupled_geos(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneGeos()) + self.tasks.append(ta.BuildGeosByLinking()) + self.tasks.append(ta.BuildGeos()) + self.tasks.append(ta.PrepCoupledGeosRunDir()) + self.tasks.append(ta.GetCoupledGeosRestart()) + self.tasks.append(ta.MoveForecastRestart()) + self.tasks.append(ta.SaveRestart()) + self.tasks.append(RunGeos()) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/forecast_geos/__init__.py b/src/swell/suites/forecast_geos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/geosadas/__init__.py b/src/swell/suites/geosadas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/geosadas/suite_config.py b/src/swell/suites/geosadas/suite_config.py index 32c1e75b8..b8f3f8045 100644 --- a/src/swell/suites/geosadas/suite_config.py +++ b/src/swell/suites/geosadas/suite_config.py @@ -8,76 +8,75 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import all_suites +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = 'geosadas' + +geosadas_tier1 = QuestionList( + questions=[ + all_suites, + qd.jedi_build_method("use_existing"), + qd.bundles("REMOVE"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.horizontal_resolution("13"), + qd.observations([ + "abi_g16", + "abi_g18", + "aircraft_temperature", + "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "satwind", + "scatwind", + "ssmis_f17" + ]), + qd.produce_geovals(False), + qd.window_type("3D"), + qd.gradient_norm_reduction("1e-6"), + qd.number_of_iterations([5]), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, 'geosadas_tier1', geosadas_tier1) - geosadas_tier1 = QuestionList( - list_name="geosadas", - questions=[ - sq.all_suites, - qd.jedi_build_method("use_existing"), - qd.bundles("REMOVE"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.horizontal_resolution("13"), - qd.observations([ - "abi_g16", - "abi_g18", - "aircraft_temperature", - "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "satwind", - "scatwind", - "ssmis_f17" - ]), - qd.produce_geovals(False), - qd.window_type("3D"), - qd.gradient_norm_reduction("1e-6"), - qd.number_of_iterations([5]), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +geosadas = QuestionList( + questions=[ + geosadas_tier1 + ] +) - geosadas = QuestionList( - list_name="geosadas", - questions=[ - geosadas_tier1 - ] - ) +suite_configs.register(suite_name, 'geosadas', geosadas) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/geosadas/flow.cylc b/src/swell/suites/geosadas/workflow.py similarity index 51% rename from src/swell/suites/geosadas/flow.cylc rename to src/swell/suites/geosadas/workflow.py index 6ce3204df..fa21a4d9a 100644 --- a/src/swell/suites/geosadas/flow.cylc +++ b/src/swell/suites/geosadas/workflow.py @@ -5,17 +5,24 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + # -------------------------------------------------------------------------------------------------- -# Cylc suite for executing JEDI-based non-cycling variational data assimilation +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- +template_str = ''' # -------------------------------------------------------------------------------------------------- [scheduler] UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -71,58 +78,52 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml +''' - # Tasks - # ----- - [[CloneGeosMksi]] - script = "swell task CloneGeosMksi $config" - - [[GenerateObservingSystemRecords]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m geos_atmosphere" - - [[CloneJedi]] - script = "swell task CloneJedi $config" +# -------------------------------------------------------------------------------------------------- - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - [[StageJedi]] - script = "swell task StageJedi $config -m geos_atmosphere" +@workflows.register('geosadas') +class Workflow_geosadas(CylcWorkflow): - [[ GetGsiBc ]] - script = "swell task GetGsiBc $config -d $datetime -m geos_atmosphere" + def get_workflow_string(self): + workflow_str = self.default_header() - [[ GsiBcToIoda ]] - script = "swell task GsiBcToIoda $config -d $datetime -m geos_atmosphere" + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" - [[ GetGsiNcdiag ]] - script = "swell task GetGsiNcdiag $config -d $datetime -m geos_atmosphere" + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) - [[ GsiNcdiagToIoda ]] - script = "swell task GsiNcdiagToIoda $config -d $datetime -m geos_atmosphere" + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) - [[ GetGeosAdasBackground ]] - script = "swell task GetGeosAdasBackground $config -d $datetime -m geos_atmosphere" + return workflow_str - [[RenderJediObservations-geos_atmosphere]] - script = "swell task RenderJediObservations $config -d $datetime -m geos_atmosphere" + def set_tasks(self) -> list: - [[RunJediVariationalExecutable]] - script = "swell task RunJediVariationalExecutable $config -d $datetime -m geos_atmosphere" - platform = {{platform}} - execution time limit = {{scheduling["RunJediVariationalExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediVariationalExecutable"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.CloneGeosMksi()) - [[CleanCycle]] - script = "swell task CleanCycle $config -d $datetime -m geos_atmosphere" + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.StageJedi(model=model)) + self.tasks.append(ta.GetGsiBc(model=model)) + self.tasks.append(ta.GsiBcToIoda(model=model)) + self.tasks.append(ta.GetGsiNcdiag(model=model)) + self.tasks.append(ta.GsiNcdiagToIoda(model=model)) + self.tasks.append(ta.GetGeosAdasBackground(model=model)) + self.tasks.append(ta.RunJediVariationalExecutable(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/hofx/__init__.py b/src/swell/suites/hofx/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/hofx/suite_config.py b/src/swell/suites/hofx/suite_config.py index 90723ef84..01883f04a 100644 --- a/src/swell/suites/hofx/suite_config.py +++ b/src/swell/suites/hofx/suite_config.py @@ -8,81 +8,80 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import marine +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = 'hofx' + +hofx_tier1 = QuestionList( + questions=[ + marine, + qd.window_type('4D'), + qd.jedi_build_method("use_existing"), + qd.save_geovals(True), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.horizontal_resolution("91"), + qd.geos_x_background_directory("/discover/nobackup/projects/gmao/dadev/" + "rtodling/archive/Restarts/JEDI/541x"), + qd.npx_proc(2), + qd.npy_proc(2), + qd.observations([ + "aircraft_temperature", + "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "pibal", + "satwind", + "scatwind", + "sfcship", + "sfc", + "sondes", + "ssmis_f17" + ]), + qd.clean_patterns([]), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, 'hofx_tier1', hofx_tier1) - hofx_tier1 = QuestionList( - list_name="hofx", - questions=[ - sq.marine, - qd.window_type(), - qd.jedi_build_method("use_existing"), - qd.save_geovals(True), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.horizontal_resolution("91"), - qd.geos_x_background_directory("/discover/nobackup/projects/gmao/dadev/" - "rtodling/archive/Restarts/JEDI/541x"), - qd.npx_proc(2), - qd.npy_proc(2), - qd.observations([ - "aircraft_temperature", - "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "pibal", - "satwind", - "scatwind", - "sfcship", - "sfc", - "sondes", - "ssmis_f17" - ]), - qd.clean_patterns([]), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +hofx = QuestionList( + questions=[ + hofx_tier1 + ] +) - hofx = QuestionList( - list_name="hofx", - questions=[ - hofx_tier1 - ] - ) +suite_configs.register(suite_name, 'hofx', hofx) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/hofx/flow.cylc b/src/swell/suites/hofx/workflow.py similarity index 56% rename from src/swell/suites/hofx/flow.cylc rename to src/swell/suites/hofx/workflow.py index 5272428c0..3de2898cc 100644 --- a/src/swell/suites/hofx/flow.cylc +++ b/src/swell/suites/hofx/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based h(x) @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -94,79 +105,55 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" - - [[GenerateObservingSystemRecords-{{model_component}}]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}}]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetBackgroundGeosExperiment-{{model_component}} ]] - script = "swell task GetBackgroundGeosExperiment $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[GetObsNotInR2d2-{{model_component}}]] - script = "swell task GetObsNotInR2d2 $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediHofxExecutable-{{model_component}}]] - script = "swell task RunJediHofxExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediHofxExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediHofxExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('hofx') +class Workflow_hofx(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.CloneGeosMksi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.GetBackgroundGeosExperiment(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.GetObsNotInR2d2(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediHofxExecutable(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/hofx_cf/__init__.py b/src/swell/suites/hofx_cf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/hofx_cf/suite_config.py b/src/swell/suites/hofx_cf/suite_config.py index 9fa8ad6d1..8c98bf8e3 100644 --- a/src/swell/suites/hofx_cf/suite_config.py +++ b/src/swell/suites/hofx_cf/suite_config.py @@ -8,38 +8,40 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs -from enum import Enum +# -------------------------------------------------------------------------------------------------- + +suite_name = 'hofx_cf' + +hofx_cf = QuestionList( + questions=[ + common, + qd.swell_static_files("/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles"), + qd.start_cycle_point("2023-08-05T18:00:00Z"), + qd.final_cycle_point("2023-08-05T18:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_cf']), + qd.check_for_obs(False) # don't check empty for empty obs + ], + geos_cf=[ + ] +) + +suite_configs.register(suite_name, 'hofx_cf', hofx_cf) # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - - hofx_cf = QuestionList( - list_name="hofx_cf", - questions=[ - sq.common, - qd.swell_static_files("/discover/nobackup/projects/gmao/geos_cf_dev/SwellStaticFiles"), - qd.start_cycle_point("2023-08-05T18:00:00Z"), - qd.final_cycle_point("2023-08-05T18:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_cf']), - qd.check_for_obs(False) # don't check empty for empty obs - ], - - geos_cf=[ - ] - ) - - hofx_cf_tier1 = QuestionList( - list_name="hofx_cf_tier1", - questions=[ - hofx_cf - ] - ) +hofx_cf_tier1 = QuestionList( + questions=[ + hofx_cf + ] +) + +suite_configs.register(suite_name, 'hofx_cf_tier1', hofx_cf_tier1) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/hofx_cf/flow.cylc b/src/swell/suites/hofx_cf/workflow.py similarity index 55% rename from src/swell/suites/hofx_cf/flow.cylc rename to src/swell/suites/hofx_cf/workflow.py index 0c75d1561..581dc1340 100644 --- a/src/swell/suites/hofx_cf/flow.cylc +++ b/src/swell/suites/hofx_cf/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing JEDI-based h(x) @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -87,67 +98,50 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[GetBackground-{{model_component}}]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediHofxExecutable-{{model_component}}]] - script = "swell task RunJediHofxExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediHofxExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediHofxExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaObservations-{{model_component}}]] - script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('hofx_cf') +class Workflow_hofx_cf(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJediByLinking()) + self.tasks.append(ta.BuildJedi()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.GetBackground(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediHofxExecutable(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/ingest_obs/__init__.py b/src/swell/suites/ingest_obs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/ingest_obs/flow.cylc b/src/swell/suites/ingest_obs/flow.cylc deleted file mode 100644 index 86bf56584..000000000 --- a/src/swell/suites/ingest_obs/flow.cylc +++ /dev/null @@ -1,102 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for ingesting observations - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - initial cycle point = {{start_cycle_point}} - final cycle point = {{final_cycle_point}} - - [[graph]] - {% if download_convert_pipeline %} - R1 = """ - # Triggers for non cycle time dependent tasks - # ------------------------------------------- - # Clone JEDI source code - CloneJedi - - # Build JEDI source code by linking - CloneJedi => BuildJediByLinking? - - # If not able to link to build create the build - BuildJediByLinking:fail? => BuildJedi - - """ - {% endif %} - - {% for cycle_time in cycle_times %} - {{cycle_time.cycle_time}} = """ - {% for model_component in model_components %} - {% if download_convert_pipeline %} - DownloadObs-{{model_component}} => ConvertObsToIoda-{{model_component}} - BuildJediByLinking[^]? | BuildJedi[^] => ConvertObsToIoda-{{model_component}} - ConvertObsToIoda-{{model_component}} => IngestObs-{{model_component}} - {% else %} - IngestObs-{{model_component}} - {% endif %} - {% endfor %} - """ - {% endfor %} - -# -------------------------------------------------------------------------------------------------- - -[runtime] - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - {% if download_convert_pipeline %} - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% endif %} - - {% for model_component in model_components %} - - {% if download_convert_pipeline %} - [[DownloadObs-{{model_component}}]] - script = "swell task DownloadObs $config -d $datetime -m {{model_component}}" - execution time limit = PT30M - - [[ConvertObsToIoda-{{model_component}}]] - script = "swell task ConvertObsToIoda $config -d $datetime -m {{model_component}}" - execution time limit = PT15M - {% endif %} - - [[IngestObs-{{model_component}}]] - script = "swell task IngestObs $config -d $datetime -m {{model_component}}" - execution time limit = PT10M - - {% endfor %} diff --git a/src/swell/suites/ingest_obs/suite_config.py b/src/swell/suites/ingest_obs/suite_config.py index 24c432767..0363dd50d 100644 --- a/src/swell/suites/ingest_obs/suite_config.py +++ b/src/swell/suites/ingest_obs/suite_config.py @@ -5,64 +5,78 @@ """ -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq -from enum import Enum - - -class SuiteConfig(QuestionContainer, Enum): - - ingest_obs = QuestionList( - list_name="ingest_obs", - questions=[ - sq.common, - qd.download_convert_pipeline(False) - ], - ) - # This name should be unique and not conflict with other suites - # (otherwise it might get overwritten) - ingest_obs_marine = QuestionList( - list_name="ingest_obs_marine", - questions=[ - ingest_obs, - sq.marine, - qd.start_cycle_point("2023-07-01T00:00:00Z"), - qd.final_cycle_point("2023-07-02T12:00:00Z"), - qd.model_components(['geos_marine']), - qd.runahead_limit("P5"), - ], - geos_marine=[ - qd.window_length("PT6H"), - qd.cycle_times(['T00', 'T06', 'T12', 'T18']), - qd.obs_to_ingest(['adt_cryosat2n', - 'adt_sentinel6a', - 'adt_swot_nadir', - 'sss_smos', - ]), - qd.dry_run(True), - ] - ) - - ingest_obs_cf = QuestionList( - list_name="ingest_obs_cf", - questions=[ - ingest_obs, - qd.start_cycle_point("2024-01-01T18:00:00Z"), - qd.final_cycle_point("2024-01-01T18:00:00Z"), - qd.model_components(['geos_cf']), - qd.runahead_limit("P5"), - qd.download_convert_pipeline(True), - qd.jedi_build_method("use_existing"), # For pyioda - ], - geos_cf=[ - qd.window_length("PT6H"), - qd.obs_to_download(['tempo_no2_tropo']), - qd.obs_to_ingest(['tempo_no2_tropo']), - qd.converter_path( - "/discover/nobackup/projects/jcsda/s2127/maryamao/" - "jedi-bundle/build-intel-1.9/bin/" - ), - qd.dry_run(False), - ] - ) +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common, marine +from swell.suites.base.suite_attributes import suite_configs + + +# -------------------------------------------------------------------------------------------------- + +suite_name = 'ingest_obs' + +ingest_obs = QuestionList( + questions=[ + common, + qd.download_convert_pipeline(False) + ], +) + +suite_configs.register(suite_name, 'ingest_obs', ingest_obs) + +# -------------------------------------------------------------------------------------------------- + +# This name should be unique and not conflict with other suites +# (otherwise it might get overwritten) +ingest_obs_marine = QuestionList( + questions=[ + ingest_obs, + marine, + qd.start_cycle_point("2023-07-01T00:00:00Z"), + qd.final_cycle_point("2023-07-02T12:00:00Z"), + qd.model_components(['geos_marine']), + qd.runahead_limit("P5"), + ], + geos_marine=[ + qd.window_length("PT6H"), + qd.cycle_times(['T00', 'T06', 'T12', 'T18']), + qd.obs_to_ingest(['adt_cryosat2n', + 'adt_sentinel6a', + 'adt_swot_nadir', + 'sss_smos', + ]), # List of obs names + qd.dry_run(True), + ] +) + +suite_configs.register(suite_name, 'ingest_obs_marine', ingest_obs_marine) + +# -------------------------------------------------------------------------------------------------- + +ingest_obs_cf = QuestionList( + questions=[ + ingest_obs, + qd.start_cycle_point("2023-08-10T00:00:00Z"), + qd.final_cycle_point("2023-08-11T00:00:00Z"), + qd.model_components(['geos_cf']), + qd.runahead_limit("P5"), + qd.download_convert_pipeline(True), + qd.jedi_build_method("use_existing"), # For pyioda + ], + geos_cf=[ + qd.window_length("PT6H"), + qd.obs_to_download(['tempo_no2_tropo']), + qd.obs_to_ingest(['tempo_no2_tropo']), + qd.converter_path( + "/discover/nobackup/projects/jcsda/s2127/maryamao/" + "jedi-bundle/build-intel-1.9/bin/" + ), + qd.dry_run(False), + ] +) + +suite_configs.register(suite_name, 'ingest_obs_cf', ingest_obs_cf) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/ingest_obs/workflow.py b/src/swell/suites/ingest_obs/workflow.py new file mode 100644 index 000000000..9d4f65cd2 --- /dev/null +++ b/src/swell/suites/ingest_obs/workflow.py @@ -0,0 +1,76 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' + +[scheduler] + UTC mode = True + allow implicit tasks = False + +{{stall_timeout}} + +[scheduling] + initial cycle point = {{start_cycle_point}} + final cycle point = {{final_cycle_point}} + + [[graph]] + + {% for cycle_time in cycle_times %} + {{cycle_time.cycle_time}} = """ + {% for model_component in model_components %} + IngestObs-{{model_component}} + {% endfor %} + """ + {% endfor %} + +[runtime] + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('ingest_obs') +class Workflow_ingest_obs(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.IngestObs(model=model)) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/localensembleda/__init__.py b/src/swell/suites/localensembleda/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/localensembleda/flow.cylc b/src/swell/suites/localensembleda/flow.cylc deleted file mode 100644 index e6699b882..000000000 --- a/src/swell/suites/localensembleda/flow.cylc +++ /dev/null @@ -1,241 +0,0 @@ -#!jinja2 -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for executing JEDI-based LocalEnsembleDA Algorithm - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -{{scheduling['stall_timeout']}} - -# -------------------------------------------------------------------------------------------------- - -[scheduling] - - initial cycle point = {{start_cycle_point}} - final cycle point = {{final_cycle_point}} - runahead limit = {{runahead_limit}} - - [[graph]] - R1 = """ - # Triggers for non cycle time dependent tasks - # ------------------------------------------- - # Clone JEDI source code - CloneJedi - - # Build JEDI source code by linking - CloneJedi => BuildJediByLinking? - - # If not able to link to build create the build - BuildJediByLinking:fail? => BuildJedi - - {% for model_component in model_components %} - # Clone geos ana for generating observing system records - CloneGeosMksi-{{model_component}} - {% endfor %} - """ - - {% for cycle_time in cycle_times %} - {{cycle_time.cycle_time}} = """ - {% for model_component in model_components %} - {% if cycle_time[model_component] %} - # Task triggers for: {{model_component}} - # ------------------ - - # Perform staging that is cycle dependent - BuildJediByLinking[^]? | BuildJedi[^] => StageJediCycle-{{model_component}} => sync_point - - GetObsNotInR2d2-{{model_component}}: fail? => GetObservations-{{model_component}} - - GetObsNotInR2d2-{{model_component}}? | GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} - - RenderJediObservations-{{model_component}} => sync_point - - CloneGeosMksi-{{model_component}}[^] => GenerateObservingSystemRecords-{{model_component}} => sync_point - - GetEnsembleGeosExperiment-{{model_component}} => sync_point - - sync_point => RunJediObsfiltersExecutable-{{model_component}} - {% if skip_ensemble_hofx %} - sync_point => RunJediObsfiltersExecutable-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} - {% else %} - # Run hofx for ensemble members according to strategy - {% if ensemble_hofx_strategy == 'serial' %} - sync_point => RunJediEnsembleMeanVariance-{{model_component}} => RunJediHofxEnsembleExecutable-{{model_component}} - RunJediHofxEnsembleExecutable-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} - - {% elif ensemble_hofx_strategy == 'parallel' %} - {% for packet in range(ensemble_hofx_packets) %} - # When strategy is parallel, only proceed if all RunJediHofxEnsembleExecutable completes successfully for each packet - - # There is a need for a task to combine all hofx observations together, compute node preferred, put here as placeholder - # RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} => RunEnsembleHofxCombiner-{{model_component}} - # RunEnsembleHofxCombiner-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} - - sync_point => RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} - RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} => RunJediLocalEnsembleDaExecutable-{{model_component}} - {% endfor %} - {% endif %} - {% endif %} - - - # EvaIncrement - RunJediLocalEnsembleDaExecutable-{{model_component}} => EvaIncrement-{{model_component}} - - # EvaObservations - # RunJediLocalEnsembleDaExecutable-{{model_component}} => EvaObservations-{{model_component}} - - # Save observations - # RunJediLocalEnsembleDaExecutable-{{model_component}} => SaveObsDiags-{{model_component}} - - # Clean up large files - # EvaObservations-{{model_component}} & SaveObsDiags-{{model_component}} & - EvaIncrement-{{model_component}} => CleanCycle-{{model_component}} - - {% endif %} - {% endfor %} - """ - {% endfor %} - -# -------------------------------------------------------------------------------------------------- - -[runtime] - - # Task defaults - # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% for model_component in model_components %} - - [[CloneGeosMksi-{{model_component}}]] - script = "swell task CloneGeosMksi $config -m {{model_component}}" - - [[GenerateObservingSystemRecords-{{model_component}}]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m {{model_component}}" - - [[StageJediCycle-{{model_component}}]] - script = "swell task StageJedi $config -d $datetime -m {{model_component}}" - - [[ GetBackground-{{model_component}} ]] - script = "swell task GetBackground $config -d $datetime -m {{model_component}}" - - [[GetEnsembleGeosExperiment-{{model_component}}]] - script = "swell task GetEnsembleGeosExperiment $config -d $datetime -m {{model_component}}" - - [[RenderJediObservations-geos_atmosphere]] - script = "swell task RenderJediObservations $config -d $datetime -m {{model_component}}" - - [[RunJediObsfiltersExecutable-{{model_component}}]] - script = "swell task RunJediObsfiltersExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediObsfiltersExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediObsfiltersExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[RunJediEnsembleMeanVariance-{{model_component}}]] - script = "swell task RunJediEnsembleMeanVariance $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediEnsembleMeanVariance"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediEnsembleMeanVariance"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[GetObservations-{{model_component}}]] - script = "swell task GetObservations $config -d $datetime -m {{model_component}}" - - [[GetObsNotInR2d2-{{model_component}}]] - script = "swell task GetObsNotInR2d2 $config -d $datetime -m {{model_component}}" - - {% if not skip_ensemble_hofx %} - {% if ensemble_hofx_strategy == 'serial' %} - [[RunJediHofxEnsembleExecutable-{{model_component}}]] - script = "swell task RunJediHofxEnsembleExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediHofxEnsembleExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediHofxEnsembleExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - {% elif ensemble_hofx_strategy == 'parallel' %} - {% for packet in range(ensemble_hofx_packets) %} - [[RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}}]] - script = "swell task RunJediHofxEnsembleExecutable $config -d $datetime -m {{model_component}} -p {{packet}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediHofxEnsembleExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediHofxEnsembleExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - {% endfor %} - {% endif %} - {% endif %} - - [[RunJediLocalEnsembleDaExecutable-{{model_component}}]] - script = "swell task RunJediLocalEnsembleDaExecutable $config -d $datetime -m {{model_component}}" - platform = {{platform}} - execution time limit = {{scheduling["RunJediLocalEnsembleDaExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediLocalEnsembleDaExecutable"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaIncrement-{{model_component}}]] - script = "swell task EvaIncrement $config -d $datetime -m {{model_component}}" - - [[EvaObservations-{{model_component}}]] - script = true -# EnKF not ready to use Eva -# script = "swell task EvaObservations $config -d $datetime -m {{model_component}}" -# platform = {{platform}} -# execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} -# [[[directives]]] -# {%- for key, value in scheduling["EvaObservations"]["directives"][model_component].items() %} -# --{{key}} = {{value}} -# {%- endfor %} - - [[SaveObsDiags-{{model_component}}]] - script = "swell task SaveObsDiags $config -d $datetime -m {{model_component}}" - - [[CleanCycle-{{model_component}}]] - script = "swell task CleanCycle $config -d $datetime -m {{model_component}}" - {% endfor %} - - - [[sync_point]] - script = true -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/localensembleda/suite_config.py b/src/swell/suites/localensembleda/suite_config.py index e4c670272..40f4b36e0 100644 --- a/src/swell/suites/localensembleda/suite_config.py +++ b/src/swell/suites/localensembleda/suite_config.py @@ -8,133 +8,135 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import marine +from swell.suites.base.suite_attributes import suite_configs -from enum import Enum +# -------------------------------------------------------------------------------------------------- +suite_name = 'localensembleda' -# -------------------------------------------------------------------------------------------------- +localensembleda_tier1 = QuestionList( + questions=[ + marine, + qd.cycling_varbc(), + qd.ensemble_hofx_packets(), + qd.ensemble_hofx_strategy(), + qd.skip_ensemble_hofx(), + qd.final_cycle_point("2023-10-10T12:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.horizontal_resolution('91'), + qd.background_experiment('x0050'), + qd.geos_x_background_directory('/discover/nobackup/projects/gmao/dadev/' + 'rtodling/archive/Restarts/JEDI/541x'), + qd.geos_x_ensemble_directory('/discover/nobackup/projects/gmao/dadev/' + 'rtodling/archive/541/Milan'), + qd.npx_proc(3), + qd.npy_proc(3), + qd.cycle_times(['T00']), + qd.background_time_offset("PT3H"), + qd.ensemble_num_members(3), + qd.skip_ensemble_hofx(True), + qd.local_ensemble_solver("Deterministic GETKF"), + qd.local_ensemble_use_linear_observer(False), + qd.ensmean_only(False), + qd.local_ensemble_save_posterior_mean(True), + qd.local_ensemble_save_posterior_mean_increment(True), + qd.local_ensemble_save_posterior_ensemble(False), + qd.local_ensemble_save_posterior_ensemble_increments(False), + qd.obs_thinning_rej_fraction(0.75), + qd.observations([ + "atms_n20", + ]), + qd.window_type("3D"), + qd.clean_patterns(['*.txt']) + ] +) + +suite_configs.register(suite_name, 'localensembleda_tier1', localensembleda_tier1) -class SuiteConfig(QuestionContainer, Enum): +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +localensembleda_tier2 = QuestionList( + questions=[ + marine, + qd.ensemble_hofx_packets(), + qd.ensemble_hofx_strategy(), + qd.skip_ensemble_hofx(), + qd.final_cycle_point("2023-10-10T12:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.horizontal_resolution('91'), + qd.background_experiment('x0050'), + qd.geos_x_background_directory('/discover/nobackup/projects/gmao/dadev/' + 'rtodling/archive/Restarts/JEDI/541x'), + qd.geos_x_ensemble_directory('/discover/nobackup/projects/gmao/dadev/' + 'rtodling/archive/541/Milan'), + qd.npx_proc(4), + qd.npy_proc(4), + # qd.perhost(32), + qd.cycle_times(['T00']), + qd.background_time_offset("PT3H"), + qd.ensemble_num_members(16), + qd.skip_ensemble_hofx(True), + qd.local_ensemble_solver("Deterministic GETKF"), + qd.local_ensemble_use_linear_observer(True), + qd.ensmean_only(False), + qd.local_ensemble_save_posterior_mean(True), + qd.local_ensemble_save_posterior_mean_increment(True), + qd.local_ensemble_save_posterior_ensemble(False), + qd.local_ensemble_save_posterior_ensemble_increments(False), + qd.obs_thinning_rej_fraction(0.75), + qd.observations([ + "aircraft_temperature", + "aircraft_wind", + "sondes", + "gps", + "amsua_aqua", + "amsua_n15", + "amsua_n18", + "amsua_n19", + "amsr2_gcom-w1", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n18", + "avhrr3_n19", + "scatwind", + "sfcship", + "sfc", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "mls55_aura", + "omi_aura", + "ompsnm_npp", + "pibal", + "ssmis_f17", + "amsua_metop-b", + "amsua_metop-c" + ]), + qd.window_type("3D"), + qd.clean_patterns(['*.txt']) + ] +) - localensembleda_tier1 = QuestionList( - list_name="localensembleda", - questions=[ - sq.marine, - qd.ensemble_hofx_packets(), - qd.ensemble_hofx_strategy(), - qd.skip_ensemble_hofx(), - qd.final_cycle_point("2023-10-10T12:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.horizontal_resolution('91'), - qd.background_experiment('x0050'), - qd.geos_x_background_directory('/discover/nobackup/projects/gmao/dadev/' - 'rtodling/archive/Restarts/JEDI/541x'), - qd.geos_x_ensemble_directory('/discover/nobackup/projects/gmao/dadev/' - 'rtodling/archive/541/Milan'), - qd.npx_proc(3), - qd.npy_proc(3), - qd.cycle_times(['T00']), - qd.background_time_offset("PT3H"), - qd.ensemble_num_members(3), - qd.skip_ensemble_hofx(True), - qd.local_ensemble_solver("Deterministic GETKF"), - qd.local_ensemble_use_linear_observer(False), - qd.ensmean_only(False), - qd.local_ensemble_save_posterior_mean(True), - qd.local_ensemble_save_posterior_mean_increment(True), - qd.local_ensemble_save_posterior_ensemble(False), - qd.local_ensemble_save_posterior_ensemble_increments(False), - qd.obs_thinning_rej_fraction(0.75), - qd.observations([ - "atms_n20", - ]), - qd.window_type("3D"), - qd.clean_patterns(['*.txt']) - ] - ) +suite_configs.register(suite_name, 'localensembleda_tier2', localensembleda_tier2) - localensembleda_tier2 = QuestionList( - list_name="localensembleda", - questions=[ - sq.marine, - qd.ensemble_hofx_packets(), - qd.ensemble_hofx_strategy(), - qd.skip_ensemble_hofx(), - qd.final_cycle_point("2023-10-10T12:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.horizontal_resolution('91'), - qd.background_experiment('x0050'), - qd.geos_x_background_directory('/discover/nobackup/projects/gmao/dadev/' - 'rtodling/archive/Restarts/JEDI/541x'), - qd.geos_x_ensemble_directory('/discover/nobackup/projects/gmao/dadev/' - 'rtodling/archive/541/Milan'), - qd.npx_proc(4), - qd.npy_proc(4), - # qd.perhost(32), - qd.cycle_times(['T00']), - qd.background_time_offset("PT3H"), - qd.ensemble_num_members(16), - qd.skip_ensemble_hofx(True), - qd.local_ensemble_solver("Deterministic GETKF"), - qd.local_ensemble_use_linear_observer(True), - qd.ensmean_only(False), - qd.local_ensemble_save_posterior_mean(True), - qd.local_ensemble_save_posterior_mean_increment(True), - qd.local_ensemble_save_posterior_ensemble(False), - qd.local_ensemble_save_posterior_ensemble_increments(False), - qd.obs_thinning_rej_fraction(0.75), - qd.observations([ - "aircraft_temperature", - "aircraft_wind", - "sondes", - "gps", - "amsua_aqua", - "amsua_n15", - "amsua_n18", - "amsua_n19", - "amsr2_gcom-w1", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n18", - "avhrr3_n19", - "scatwind", - "sfcship", - "sfc", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "mls55_aura", - "omi_aura", - "ompsnm_npp", - "pibal", - "ssmis_f17", - "amsua_metop-b", - "amsua_metop-c" - ]), - qd.window_type("3D"), - qd.clean_patterns(['*.txt']) - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +localensembleda = QuestionList( + questions=[ + localensembleda_tier2 + ] +) - localensembleda = QuestionList( - list_name="localensembleda", - questions=[ - localensembleda_tier2 - ] - ) +suite_configs.register(suite_name, 'localensembleda', localensembleda) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/localensembleda/workflow.py b/src/swell/suites/localensembleda/workflow.py new file mode 100644 index 000000000..8b1890e35 --- /dev/null +++ b/src/swell/suites/localensembleda/workflow.py @@ -0,0 +1,181 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows +from swell.utilities.jinja2 import template_string_jinja2 + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' + +# -------------------------------------------------------------------------------------------------- + +# Cylc suite for executing JEDI-based LocalEnsembleDA Algorithm + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + UTC mode = True + allow implicit tasks = False + +{{stall_timeout}} + +# -------------------------------------------------------------------------------------------------- + +[scheduling] + + initial cycle point = {{start_cycle_point}} + final cycle point = {{final_cycle_point}} + runahead limit = {{runahead_limit}} + + [[graph]] + R1 = """ + # Triggers for non cycle time dependent tasks + # ------------------------------------------- + # Clone JEDI source code + CloneJedi + + # Build JEDI source code by linking + CloneJedi => BuildJediByLinking? + + # If not able to link to build create the build + BuildJediByLinking:fail? => BuildJedi + + {% for model_component in model_components %} + # Clone geos ana for generating observing system records + CloneGeosMksi-{{model_component}} + {% endfor %} + """ + + {% for cycle_time in cycle_times %} + {{cycle_time.cycle_time}} = """ + {% for model_component in model_components %} + {% if cycle_time[model_component] %} + # Task triggers for: {{model_component}} + # ------------------ + + # Perform staging that is cycle dependent + BuildJediByLinking[^]? | BuildJedi[^] => StageJediCycle-{{model_component}} => sync_point + + GetObsNotInR2d2-{{model_component}}: fail? => GetObservations-{{model_component}} + + GetObsNotInR2d2-{{model_component}}? | GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} + + RenderJediObservations-{{model_component}} => sync_point + + CloneGeosMksi-{{model_component}}[^] => GenerateObservingSystemRecords-{{model_component}} => sync_point + + GetEnsembleGeosExperiment-{{model_component}} => sync_point + + sync_point => RunJediObsfiltersExecutable-{{model_component}} + {% if models[model_component]['skip_ensemble_hofx'] %} + sync_point => RunJediObsfiltersExecutable-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} + {% else %} + # Run hofx for ensemble members according to strategy + {% if ensemble_hofx_strategy == 'serial' %} + sync_point => RunJediEnsembleMeanVariance-{{model_component}} => RunJediHofxEnsembleExecutable-{{model_component}} + RunJediHofxEnsembleExecutable-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} + + {% elif ensemble_hofx_strategy == 'parallel' %} + {% for packet in range(ensemble_hofx_packets) %} + # When strategy is parallel, only proceed if all RunJediHofxEnsembleExecutable completes successfully for each packet + + # There is a need for a task to combine all hofx observations together, compute node preferred, put here as placeholder + # RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} => RunEnsembleHofxCombiner-{{model_component}} + # RunEnsembleHofxCombiner-{{model_component}} => RunJediLocalEnsembleDaExecutable-{{model_component}} + + sync_point => RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} + RunJediHofxEnsembleExecutable-{{model_component}}_pack{{packet}} => RunJediLocalEnsembleDaExecutable-{{model_component}} + {% endfor %} + {% endif %} + {% endif %} + + + # EvaIncrement + RunJediLocalEnsembleDaExecutable-{{model_component}} => EvaIncrement-{{model_component}} + + # EvaObservations + # RunJediLocalEnsembleDaExecutable-{{model_component}} => EvaObservations-{{model_component}} + + # Save observations + # RunJediLocalEnsembleDaExecutable-{{model_component}} => SaveObsDiags-{{model_component}} + + # Clean up large files + # EvaObservations-{{model_component}} & SaveObsDiags-{{model_component}} & + EvaIncrement-{{model_component}} => CleanCycle-{{model_component}} + + {% endif %} + {% endfor %} + """ + {% endfor %} + +[runtime] + + # Task defaults + # ------------- + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('localensembleda') +class Workflow_localensembleda(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str = template_string_jinja2(self.logger, workflow_str, self.experiment_dict, True) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildJediByLinking()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.StageJediCycle(model=model)) + self.tasks.append(ta.GetObsNotInR2d2(model=model)) + self.tasks.append(ta.GetObservations(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.GetEnsembleGeosExperiment(model=model)) + self.tasks.append(ta.sync_point(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediObsfiltersExecutable(model=model)) + self.tasks.append(ta.RunJediLocalEnsembleDaExecutable(model=model)) + self.tasks.append(ta.RunJediEnsembleMeanVariance(model=model)) + self.tasks.append(ta.RunJediHofxEnsembleExecutable(model=model)) + self.tasks.append(ta.EvaIncrement(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.SaveObsDiags(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/suite_questions.py b/src/swell/suites/suite_questions.py deleted file mode 100644 index 31a58acdc..000000000 --- a/src/swell/suites/suite_questions.py +++ /dev/null @@ -1,61 +0,0 @@ -# -------------------------------------------------------------------------------------------------- -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - -from enum import Enum - -from swell.utilities.swell_questions import QuestionList, QuestionContainer -from swell.utilities.question_defaults import QuestionDefaults as qd - - -# -------------------------------------------------------------------------------------------------- - -class SuiteQuestions(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - # Shared groups of questions across suites - # -------------------------------------------------------------------------------------------------- - - all_suites = QuestionList( - list_name="all_suites", - questions=[ - qd.experiment_id(), - qd.experiment_root() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - common = QuestionList( - list_name="common", - questions=[ - all_suites, - qd.cycle_times(), - qd.start_cycle_point(), - qd.final_cycle_point(), - qd.model_components(), - qd.runahead_limit(), - qd.r2d2_experiment_id(), - qd.r2d2_server(), - qd.r2d2_datastore(), - qd.skip_r2d2(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - marine = QuestionList( - list_name="marine", - questions=[ - common, - qd.marine_models() - ] - ) - - # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/ufo_testing/__init__.py b/src/swell/suites/ufo_testing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/ufo_testing/suite_config.py b/src/swell/suites/ufo_testing/suite_config.py index 8a2875d6b..1564ecde7 100644 --- a/src/swell/suites/ufo_testing/suite_config.py +++ b/src/swell/suites/ufo_testing/suite_config.py @@ -8,93 +8,91 @@ # -------------------------------------------------------------------------------------------------- -from swell.utilities.swell_questions import QuestionContainer, QuestionList -from swell.utilities.question_defaults import QuestionDefaults as qd -from swell.suites.suite_questions import SuiteQuestions as sq - -from enum import Enum - +from swell.utilities.swell_questions import QuestionList +import swell.configuration.question_defaults as qd +from swell.suites.base.suite_questions import common +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +suite_name = 'ufo_testing' - # -------------------------------------------------------------------------------------------------- +ufo_testing_tier1 = QuestionList( + questions=[ + common, + qd.final_cycle_point("2023-10-10T00:00:00Z"), + qd.jedi_build_method("use_existing"), + qd.bundles("REMOVE"), + qd.model_components(['geos_atmosphere']), + ], + geos_atmosphere=[ + qd.cycle_times(['T00']), + qd.observations([ + "abi_g16", + "abi_g18", + "aircraft_temperature", + "aircraft_wind", + "airs_aqua", + "amsr2_gcom-w1", + "amsua_aqua", + "amsua_metop-b", + "amsua_metop-c", + "amsua_n15", + "amsua_n19", + "atms_n20", + "atms_npp", + "avhrr3_metop-b", + "avhrr3_n19", + "cris-fsr_n20", + "cris-fsr_npp", + "gmi_gpm", + "gps", + "iasi_metop-b", + "iasi_metop-c", + "mhs_metop-b", + "mhs_metop-c", + "mhs_n19", + "pibal", + "satwind", + "scatwind", + "sfc", + "sfcship", + "sondes", + "ssmis_f17" + ]), + qd.produce_geovals(False), + qd.clean_patterns([ + "*.txt", + "*.log", + "*.yaml", + "*.csv", + "gsi_bcs/*.nc4", + "gsi_bcs/*.txt", + "gsi_bcs/*.yaml", + "gsi_bcs", + "gsi_ncdiags/*.nc4", + "gsi_ncdiags/aircraft/*.nc4", + "gsi_ncdiags/aircraft", + "gsi_ncdiags" + ]), + qd.path_to_gsi_bc_coefficients("/discover/nobackup/projects/gmao/dadev/rtodling/" + "archive/541/Milan/x0050/ana/Y%Y/M%m/" + "*bias*%Y%m%d_%Hz.txt"), + qd.path_to_gsi_nc_diags("/discover/nobackup/projects/gmao/dadev/rtodling/archive/" + "541/Milan/x0050/obs/Y%Y/M%m/D%d/H%H/"), + ] +) - ufo_testing_tier1 = QuestionList( - list_name="ufo_testing", - questions=[ - sq.common, - qd.final_cycle_point("2023-10-10T00:00:00Z"), - qd.jedi_build_method("use_existing"), - qd.bundles("REMOVE"), - qd.model_components(['geos_atmosphere']), - ], - geos_atmosphere=[ - qd.cycle_times(['T00']), - qd.observations([ - "abi_g16", - "abi_g18", - "aircraft_temperature", - "aircraft_wind", - "airs_aqua", - "amsr2_gcom-w1", - "amsua_aqua", - "amsua_metop-b", - "amsua_metop-c", - "amsua_n15", - "amsua_n19", - "atms_n20", - "atms_npp", - "avhrr3_metop-b", - "avhrr3_n19", - "cris-fsr_n20", - "cris-fsr_npp", - "gmi_gpm", - "gps", - "iasi_metop-b", - "iasi_metop-c", - "mhs_metop-b", - "mhs_metop-c", - "mhs_n19", - "pibal", - "satwind", - "scatwind", - "sfc", - "sfcship", - "sondes", - "ssmis_f17" - ]), - qd.produce_geovals(False), - qd.clean_patterns([ - "*.txt", - "*.log", - "*.yaml", - "*.csv", - "gsi_bcs/*.nc4", - "gsi_bcs/*.txt", - "gsi_bcs/*.yaml", - "gsi_bcs", - "gsi_ncdiags/*.nc4", - "gsi_ncdiags/aircraft/*.nc4", - "gsi_ncdiags/aircraft", - "gsi_ncdiags" - ]), - qd.path_to_gsi_bc_coefficients("/discover/nobackup/projects/gmao/dadev/rtodling/" - "archive/541/Milan/x0050/ana/Y%Y/M%m/" - "*bias*%Y%m%d_%Hz.txt"), - qd.path_to_gsi_nc_diags("/discover/nobackup/projects/gmao/dadev/rtodling/archive/" - "541/Milan/x0050/obs/Y%Y/M%m/D%d/H%H/"), - ] - ) +suite_configs.register(suite_name, 'ufo_testing_tier1', ufo_testing_tier1) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - ufo_testing = QuestionList( - list_name="ufo_testing", - questions=[ - ufo_testing_tier1 - ] - ) +ufo_testing = QuestionList( + questions=[ + ufo_testing_tier1 + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, 'ufo_testing', ufo_testing) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/ufo_testing/flow.cylc b/src/swell/suites/ufo_testing/workflow.py similarity index 52% rename from src/swell/suites/ufo_testing/flow.cylc rename to src/swell/suites/ufo_testing/workflow.py index 365eb21e1..0cb8e6479 100644 --- a/src/swell/suites/ufo_testing/flow.cylc +++ b/src/swell/suites/ufo_testing/workflow.py @@ -5,6 +5,17 @@ # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +from swell.utilities.jinja2 import template_string_jinja2 +from swell.suites.base.cylc_workflow import CylcWorkflow +from swell.tasks.base.task_attributes import task_attributes as ta +from swell.suites.base.suite_attributes import workflows + +# -------------------------------------------------------------------------------------------------- + +template_str = ''' # -------------------------------------------------------------------------------------------------- # Cylc suite for executing geos_atmosphere ObsFilters tests @@ -15,7 +26,7 @@ UTC mode = True allow implicit tasks = False -{{scheduling['stall_timeout']}} +{{stall_timeout}} # -------------------------------------------------------------------------------------------------- @@ -84,73 +95,52 @@ # Task defaults # ------------- - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - # Tasks - # ----- - [[CloneJedi]] - script = "swell task CloneJedi $config" - - [[BuildJediByLinking]] - script = "swell task BuildJediByLinking $config" - - [[BuildJedi]] - script = "swell task BuildJedi $config" - platform = {{platform}} - execution time limit = {{scheduling["BuildJedi"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["BuildJedi"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[CloneGeosMksi]] - script = "swell task CloneGeosMksi $config -m geos_atmosphere" - - [[GenerateObservingSystemRecords]] - script = "swell task GenerateObservingSystemRecords $config -d $datetime -m geos_atmosphere" - - [[ GetGsiBc ]] - script = "swell task GetGsiBc $config -d $datetime -m geos_atmosphere" - - [[ GsiBcToIoda ]] - script = "swell task GsiBcToIoda $config -d $datetime -m geos_atmosphere" - - [[ GetGsiNcdiag ]] - script = "swell task GetGsiNcdiag $config -d $datetime -m geos_atmosphere" - - [[ GsiNcdiagToIoda ]] - script = "swell task GsiNcdiagToIoda $config -d $datetime -m geos_atmosphere" - - [[ GetGeovals ]] - script = "swell task GetGeovals $config -d $datetime -m geos_atmosphere" - - [[RenderJediObservations]] - script = "swell task RenderJediObservations $config -d $datetime -m geos_atmosphere" - - [[RunJediUfoTestsExecutable]] - script = "swell task RunJediUfoTestsExecutable $config -d $datetime -m geos_atmosphere" - platform = {{platform}} - execution time limit = {{scheduling["RunJediUfoTestsExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunJediUfoTestsExecutable"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[EvaObservations]] - script = "swell task EvaObservations $config -d $datetime -m geos_atmosphere" - platform = {{platform}} - execution time limit = {{scheduling["EvaObservations"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["EvaObservations"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[CleanCycle]] - script = "swell task CleanCycle $config -d $datetime -m geos_atmosphere" +''' + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('ufo_testing') +class Workflow_ufo_testing(CylcWorkflow): + + def get_workflow_string(self): + workflow_str = self.default_header() + + self.experiment_dict['stall_timeout'] = """\ + {% if environ.get('SWELL_CYLC_TIMEOUT') %} + [[events]] + stall timeout = {{environ['SWELL_CYLC_TIMEOUT']}} + {% endif %}""" + + workflow_str += template_string_jinja2(logger=self.logger, + templated_string=template_str, + dictionary_of_templates=self.experiment_dict, + allow_unresolved=True) + + for task in self.tasks: + workflow_str += task.runtime_string(self.experiment_dict, + self.slurm_external) + + return workflow_str + + def set_tasks(self) -> list: + + self.tasks.append(ta.root()) + self.tasks.append(ta.CloneJedi()) + self.tasks.append(ta.BuildJedi()) + self.tasks.append(ta.BuildJediByLinking()) + + for model in self.experiment_dict['model_components']: + self.tasks.append(ta.CloneGeosMksi(model=model)) + self.tasks.append(ta.GenerateObservingSystemRecords(model=model)) + self.tasks.append(ta.GetGsiBc(model=model)) + self.tasks.append(ta.GsiBcToIoda(model=model)) + self.tasks.append(ta.GetGsiNcdiag(model=model)) + self.tasks.append(ta.GsiNcdiagToIoda(model=model)) + self.tasks.append(ta.RenderJediObservations(model=model)) + self.tasks.append(ta.RunJediUfoTestsExecutable(model=model)) + self.tasks.append(ta.GetGeovals(model=model)) + self.tasks.append(ta.EvaObservations(model=model)) + self.tasks.append(ta.CleanCycle(model=model)) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/swell.py b/src/swell/swell.py index b1bed63b7..819b3a1d6 100644 --- a/src/swell/swell.py +++ b/src/swell/swell.py @@ -9,7 +9,8 @@ import click -from typing import Union, Optional, Literal +from ruamel.yaml import YAML +from typing import Literal from swell.deployment.platforms.platforms import get_platforms from swell.deployment.create_experiment import clone_config, create_experiment_directory @@ -17,9 +18,10 @@ from swell.tasks.base.task_base import task_wrapper, get_tasks from swell.test.test_driver import test_wrapper, valid_tests from swell.test.suite_tests.suite_tests import run_suite, TestSuite -from swell.suites.all_suites import AllSuites +from swell.suites.base.suite_attributes import suite_configs from swell.utilities.welcome_message import write_welcome_message from swell.utilities.scripts.utility_driver import get_utilities, utility_wrapper +from swell.deployment.create_task_config import task_config_wrapper from swell.utilities.datetime_util import is_duration from swell.utilities.suite_utils import read_override_file @@ -84,17 +86,22 @@ def swell_driver() -> None: or for task-model combinations. """ +cwd_help = """ +For task configs, set flag to create directory at the user's cwd, otherwise directory will be +created in default experiment_root.""" + skip_r2d2_help = """Skip registering this experiment and storing products in R2D2.""" cylc_timeout_help = """ Set the cylc stall timeout manually for experiment. If unset, defaults to user value in ~/.cylc/flow/global.cylc, or the Cylc default of 1 hour. Uses ISO duration format (e.g. PT30S)""" + # -------------------------------------------------------------------------------------------------- @swell_driver.command() -@click.argument('suite', type=click.Choice(AllSuites.config_names())) +@click.argument('suite', type=click.Choice(suite_configs.all_configs())) @click.option('-m', '--input_method', 'input_method', default='defaults', type=click.Choice(['defaults', 'cli']), help=input_method_help) @click.option('-p', '--platform', 'platform', default='nccs_discover_sles15', @@ -107,7 +114,7 @@ def create( suite: str, input_method: str, platform: str, - override: Union[dict, str, None], + override: dict | str | None, advanced: bool, slurm: str, skip_r2d2: bool @@ -129,6 +136,46 @@ def create( create_experiment_directory(suite, input_method, platform, override_dict, advanced, slurm, skip_r2d2) +# -------------------------------------------------------------------------------------------------- + + +@swell_driver.command() +@click.argument('task', type=click.Choice(get_tasks())) +@click.option('-p', '--platform', 'platform', default='nccs_discover_sles15', + type=click.Choice(get_platforms()), help=platform_help) +@click.option('-d', '--datetime', 'datetime', default=None, help=datetime_help) +@click.option('-m', '--model', 'model', default=None, help=model_help) +@click.option('-i', '--input_method', 'input_method', default='defaults', + type=click.Choice(['defaults', 'cli']), help=input_method_help) +@click.option('-o', '--override', 'override', default=None, help=override_help) +@click.option('-s', '--slurm', 'slurm', default=None, help=slurm_help) +@click.option('-c', '--cwd', 'cwd', is_flag=True, help=cwd_help) +def create_task_config( + task: str, + platform: str, + datetime: str | None, + model: str | None, + input_method: str, + override: str | None, + slurm: str | None, + cwd: bool, +) -> None: + """ + Create a config for a single task + + This command generates a config to be used to run a single task. + + Arguments:\n + task (str): Name of the task to execute.\n + + """ + if override is not None: + yaml = YAML(typ='safe') + with open(override, 'r') as f: + override_dict = yaml.load(f) + else: + override_dict = {} + task_config_wrapper(task, platform, datetime, model, input_method, override_dict, slurm, cwd) # -------------------------------------------------------------------------------------------------- @@ -140,12 +187,16 @@ def create( type=click.Choice(['defaults', 'cli']), help=input_method_help) @click.option('-p', '--platform', 'platform', default=None, help=platform_help) @click.option('-a', '--advanced', 'advanced', default=False, help=advanced_help) +@click.option('-s', '--slurm', 'slurm', default=None, help=slurm_help) +@click.option('-k', '--skip-r2d2', 'skip_r2d2', is_flag=True, default=False, help=skip_r2d2_help) def clone( configuration: str, experiment_id: str, input_method: str, platform: str, - advanced: bool + advanced: bool, + slurm: str, + skip_r2d2: bool ) -> None: """ Clone an existing experiment @@ -161,9 +212,14 @@ def clone( experiment_dict_str = clone_config(configuration, experiment_id, input_method, platform, advanced) - # Create the experiment directory - create_experiment_directory(experiment_dict_str) + yaml = YAML(typ='safe') + experiment_override = yaml.load(experiment_dict_str) + suite = experiment_override['suite_to_run'] + # Create the experiment directory + create_experiment_directory(suite, method=input_method, platform=platform, + override=experiment_override, advanced=advanced, slurm=slurm, + skip_r2d2=skip_r2d2) # -------------------------------------------------------------------------------------------------- @@ -172,12 +228,17 @@ def clone( @click.argument('suite_path') @click.option('-b', '--no-detach', 'no_detach', is_flag=True, default=False, help=no_detach_help) @click.option('-l', '--log_path', 'log_path', default=None, help=log_path_help) +@click.option('-m', '--send-messages', 'send_messages', is_flag=True) +@click.option('-d', '--pause-workflow', 'pause_workflow', is_flag=True) @click.option('-t', '--cylc-timeout', 'cylc_timeout', default=None, help=cylc_timeout_help) def launch( suite_path: str, no_detach: bool, log_path: str, + send_messages: bool, + pause_workflow: bool, cylc_timeout: bool + ) -> None: """ Launch an experiment with the cylc workflow manager @@ -193,8 +254,7 @@ def launch( if not is_duration(cylc_timeout): raise ValueError(f'Specified cylc timeout does not match ISO duration format') - launch_experiment(suite_path, no_detach, log_path, cylc_timeout) - + launch_experiment(suite_path, no_detach, log_path, send_messages, pause_workflow, cylc_timeout) # -------------------------------------------------------------------------------------------------- @@ -208,9 +268,9 @@ def launch( def task( task: str, config: str, - datetime: Optional[str], - model: Optional[str], - ensemblePacket: Optional[str] + datetime: str | None, + model: str | None, + ensemblePacket: str | None ) -> None: """ Run a workflow task @@ -245,7 +305,6 @@ def utility(utility: str) -> None: # -------------------------------------------------------------------------------------------------- - @swell_driver.command() @click.argument('test', type=click.Choice(valid_tests)) def test(test: str) -> None: @@ -271,7 +330,7 @@ def test(test: str) -> None: "localensembleda", "3dvar_cycle"))) def t1test( suite: Literal["hofx", "3dvar_marine", "3dvar_atmos", "localensembleda", "3dvar_cycle"], - platform: Optional[str] = "nccs_discover_sles15" + platform: str = "nccs_discover_sles15" ) -> None: """ Run a particular swell suite from the tier 1 tests. @@ -293,7 +352,7 @@ def t1test( def t2test( suite: Literal["hofx", "3dvar_marine", "ufo_testing", "convert_ncdiags", "3dfgat_atmos", "build_jedi"], - platform: Optional[str] = "nccs_discover_sles15" + platform: str = "nccs_discover_sles15" ) -> None: """ Run a particular swell suite from the tier 2 tests. diff --git a/src/swell/tasks/base/task_attributes.py b/src/swell/tasks/base/task_attributes.py new file mode 100644 index 000000000..d1d7dcd87 --- /dev/null +++ b/src/swell/tasks/base/task_attributes.py @@ -0,0 +1,85 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +import swell.tasks +from swell.tasks.base.task_setup import TaskSetup +from swell.utilities.plugins import discover_plugins + +# -------------------------------------------------------------------------------------------------- + +''' +The TaskAttributes class provides tracking of TaskSetup classes, which should be defined in each +task's file. It handles this by providing a wrapper method to register each class. + +Attributes: +root and sync_point: Setup for tasks used swell-wide that don't require separate files +discover_plugins: Handles discovery of packages, which then run the register hooks when imported +TaskAttributes: class that registers TaskSetup classes in each task file + +Example for task registry: + + +from swell.tasks.base.task_attributes import task_attributes + +@task_attributes.register('Example') +class Setup(TaskSetup): + def __init__(self): + pass +''' + +# -------------------------------------------------------------------------------------------------- + + +class root(TaskSetup): + def set_defaults(self): + # root is a precursor to all tasks, it runs the pre-script before any task's script + self.script = False + self.pre_script = "source $CYLC_SUITE_DEF_PATH/modules" + self.additional_sections = [self.create_new_section('environment', + {'datetime': '$CYLC_TASK_CYCLE_POINT', + 'config': '$CYLC_SUITE_DEF_PATH/experiment.yaml'})] # noqa + + +class sync_point(TaskSetup): + # placeholder task to check run dependencies in cylc graph + # The command "true" is run in the shell as a placeholder + def set_defaults(self): + self.script = "true" + + +# -------------------------------------------------------------------------------------------------- + + +class TaskAttributes(): + def __init__(self) -> None: + setattr(self, 'root', root) + setattr(self, 'sync_point', sync_point) + + def register(self, name): + '''Provides wrapper to register class using . + + Parameters: + name: Name to refer to Setup object + ''' + def wrapper(cls): + setattr(self, name, cls) + return cls + return wrapper + + def get(self, task_name): + return getattr(self, task_name) + + +# -------------------------------------------------------------------------------------------------- + +task_attributes = TaskAttributes() + +discover_plugins(swell.tasks) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/base/task_base.py b/src/swell/tasks/base/task_base.py index 109b3af74..e4de2f49d 100644 --- a/src/swell/tasks/base/task_base.py +++ b/src/swell/tasks/base/task_base.py @@ -16,12 +16,10 @@ import os import time from datetime import datetime as dt -from typing import Union, Optional # swell imports from swell.swell_path import get_swell_path from swell.utilities.case_switching import camel_case_to_snake_case, snake_case_to_camel_case -from swell.utilities.config import Config from swell.utilities.data_assimilation_window_params import DataAssimilationWindowParams from swell.utilities.datetime_util import Datetime from swell.utilities.logger import get_logger @@ -38,9 +36,9 @@ class taskBase(ABC): def __init__( self, config_input: str, - datetime_input: Optional[str], + datetime_input: str | None, model: str, - ensemblePacket: Optional[str], + ensemblePacket: str | None, task_name: str ) -> None: @@ -48,6 +46,8 @@ def __init__( # --------------------- self.logger = get_logger(task_name) + from swell.utilities.config import Config + # Write out the initialization info # --------------------------------- self.logger.info(' Initializing task with the following parameters:') @@ -163,7 +163,7 @@ def experiment_config_path(self) -> str: # ---------------------------------------------------------------------------------------------- - def get_ensemble_packet(self) -> Optional[str]: + def get_ensemble_packet(self) -> str | None: return self.__ensemble_packet__ # ---------------------------------------------------------------------------------------------- @@ -173,7 +173,7 @@ def get_model(self) -> str: # ---------------------------------------------------------------------------------------------- - def get_model_components(self) -> Union[str, list]: + def get_model_components(self) -> str | list: return self.__model_components__ # ---------------------------------------------------------------------------------------------- @@ -192,22 +192,31 @@ def cycle_dir(self) -> str: self.logger.assert_abort(self.__model__ is not None, 'In get_cycle_dir but this ' + 'should not be called if the task does not receive model.') - # Combine datetime string (directory format) with the model - cycle_dir = os.path.join(self.experiment_path(), 'run', - self.__datetime__.string_directory(), self.__model__) + # Check whether to send to cycle dir + # Set to true since not set by default + if self.config.use_cycle_dir(True): + + # Combine datetime string (directory format) with the model + cycle_dir = os.path.join(self.experiment_path(), 'run', + self.__dto__().string_directory(), self.__model_str__()) + else: + return self.experiment_path() # Return return cycle_dir # ---------------------------------------------------------------------------------------------- - def forecast_dir(self, paths: Union[str, list[str]] = []) -> Optional[str]: + def forecast_dir(self, paths: str | list[str] = []) -> str: ''' Method to provide "forecast" directory to geos class If paths are provided, it is combined with the forecast directory and returned ''' + if self.str_forecast_dir is None: + raise ValueError('str_forecast_dir is None') + # Make sure forecast directory exists # ----------------------------------- os.makedirs(self.str_forecast_dir, 0o755, exist_ok=True) @@ -223,15 +232,31 @@ def forecast_dir(self, paths: Union[str, list[str]] = []) -> Optional[str]: # ---------------------------------------------------------------------------------------------- + def __dto__(self) -> Datetime: + if self.__datetime__ is None: + raise ValueError('Trying to call cycle datetime, but task was called without cyle time') + + return self.__datetime__ + + # ---------------------------------------------------------------------------------------------- + + def __model_str__(self) -> str: + if self.__model__ is None: + raise ValueError('Trying to call the model component, but task was not called with one') + + return self.__model__ + + # ---------------------------------------------------------------------------------------------- + def cycle_time_dto(self) -> dt: - return self.__datetime__.dto() + return self.__dto__().dto() # ---------------------------------------------------------------------------------------------- def cycle_time(self) -> str: - return self.__datetime__.string_iso() + return self.__dto__().string_iso() # ---------------------------------------------------------------------------------------------- @@ -272,9 +297,9 @@ def create_task( self, task: str, config: str, - datetime: Union[str, dt, None], - model: str, - ensemblePacket: Optional[str] + datetime: str | dt | None, + model: str | None, + ensemblePacket: str | None ) -> taskBase: # Convert camel case string to snake case @@ -303,7 +328,8 @@ def create_task( factory_logger.info(f'Using module swell.tasks.{task_lower}') # Return task object - return task_class(config, datetime, model, ensemblePacket, task) + return task_class(config, datetime, model, ensemblePacket, + task) # -------------------------------------------------------------------------------------------------- @@ -320,7 +346,7 @@ def get_tasks() -> list: tasks = [] for task_file in task_files: base_name = os.path.basename(task_file) - if '__' not in base_name: + if '__' not in base_name and base_name != 'task_attributes.py': tasks.append(snake_case_to_camel_case(base_name[0:-3])) # Return list of valid task choices @@ -332,15 +358,16 @@ def get_tasks() -> list: def task_wrapper( task: str, config: str, - datetime: Union[str, dt, None], - model: Optional[str], - ensemblePacket: Optional[str] + datetime: str | dt | None, + model: str | None, + ensemblePacket: str | None ) -> None: # Create the object constrc_start = time.perf_counter() creator = taskFactory() task_object = creator.create_task(task, config, datetime, model, ensemblePacket) + constrc_final = time.perf_counter() constrc_time = f'Constructed in {constrc_final - constrc_start:0.4f} seconds' diff --git a/src/swell/tasks/base/task_setup.py b/src/swell/tasks/base/task_setup.py new file mode 100644 index 000000000..ddf60da17 --- /dev/null +++ b/src/swell/tasks/base/task_setup.py @@ -0,0 +1,426 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from collections.abc import Mapping +from abc import abstractmethod, ABC +from typing import Literal + +from swell.utilities.cylc_formatting import CylcSection, indent_lines +from swell.utilities.suite_utils import get_model_components +from swell.utilities.dictionary import update_dict +from swell.utilities.swell_questions import QuestionList +from swell.utilities.jinja2 import template_string_jinja2 +from swell.utilities.logger import get_logger + +# -------------------------------------------------------------------------------------------------- + +blank_spec = 'BLANKSPEC' + + +class TaskSetup(ABC): + + ''' + Contains the basic properties and information needed to format the cylc [runtime] section. + + Attributes: + model: model the task is being run under at runtime + platform: platform the task is being run on + + base_name: basic name of the task within Swell + scheduling_name: name for the task within cylc + is_cycling: boolean for whether the task is run on cycles + model_dep: boolean for whether the task is run on a certain model + pre_script: cylc setting for scripts run before the main script + script: string of shell code to be run by cylc for the task + retry: times * time interval cylc should retry the task, e.g. 2*PT10s + task_time_limit: execution time limit for slurm + slurm: dictionary of slurm parameters + mail events: list of events for email messaging through cylc + question_list: list of questions keys used by the task + additional_sections: list of additional CylcSection objects to append to the runtime section + ''' + + model: str | None + platform: str | None + + base_name: str | None + scheduling_name: str | None + + is_cycling: bool + model_dep: bool + + pre_script: bool | str | None + script: bool | str | None + retry: str | None + task_time_limit: str | dict | None + slurm: dict | None + + mail_events: list + questions: list + additional_sections: list + + def __init__(self, model: str | None = None, + platform: str | None = None, + base_name: str = blank_spec, + scheduling_name: str = blank_spec, + is_cycling: str | bool = blank_spec, + model_dep: str | bool = blank_spec, + pre_script: str | Literal[False] = blank_spec, + script: str | Literal[False] | None = blank_spec, + retry: str | None = blank_spec, + task_time_limit: str | dict | None = blank_spec, + slurm: Literal['BLANKSPEC'] | dict | None = blank_spec, + mail_events: list | None = None, + questions: list | None = None, + additional_sections: list | None = None + ) -> None: + + # Set the base defaults needed by the class + self.model = model + self.platform = platform + + self.base_name = None + self.scheduling_name = None + + self.is_cycling = False + self.model_dep = False + + self.pre_script = False + self.script = None + + self.retry = None + self.task_time_limit = None + self.slurm = None + + self.mail_events = ['failed', 'submit-failed'] + + self.questions = [] + self.additional_sections = [] + + self.logger = get_logger('TaskSetup') + + # Set the defaults for the individual task + self.set_defaults() + + # Override the task defaults with the defaults being provided by the suite + if base_name != blank_spec: + self.base_name = base_name + + if scheduling_name != blank_spec: + self.scheduling_name = scheduling_name + + if is_cycling != blank_spec: + self.is_cycling = is_cycling + + if model_dep != blank_spec: + self.model_dep = model_dep + + if pre_script != blank_spec: + self.pre_script = pre_script + + if script != blank_spec: + self.script = pre_script + + if retry != blank_spec: + self.retry = retry + + if task_time_limit != blank_spec: + self.task_time_limit = task_time_limit + + if slurm != blank_spec: + self.slurm = slurm + + if mail_events is not None: + self.mail_events = mail_events + + if questions is not None: + self.questions = questions + + if additional_sections is not None: + self.additional_sections = additional_sections + + self.post_init() + + # -------------------------------------------------------------------------------------------------- + + @abstractmethod + def set_defaults(self) -> None: + '''Abstract method to be overridden by each task in order to set attributes. + ''' + pass + + # -------------------------------------------------------------------------------------------------- + + def post_init(self): + '''Sets and resolves defaults for tasks after assignment + ''' + + if self.base_name is None: + self.base_name = self.__class__.__name__ + + if self.scheduling_name is None: + self.scheduling_name = self.base_name + + if self.model_dep and self.model is not None: + self.scheduling_name += f'-{self.model}' + + if self.script is None: + self.script = f'swell task {self.base_name} $config' + + if self.is_cycling: + self.script += ' -d $datetime' + + if self.model_dep and self.model is not None: + self.script += ' -m {model}' + + if self.model_dep and self.model is not None: + if '{model}' in self.script: + self.script = self.script.format(model=self.model) + self.script = template_string_jinja2(self.logger, self.script, + {'model_component': self.model}, True) + self.scheduling_name = self.scheduling_name.format(model=self.model) + + # Set retry defaults + if self.retry is True: + self.retry = '2*PT1M' + else: + self.retry = self.match_platform(self.retry) + + # Set time limit defaults + if self.task_time_limit is True: + self.task_time_limit = 'PT1H' + elif self.task_time_limit: + self.task_time_limit = self.match_platform(self.task_time_limit) + + # Convert questions list into object + self.question_list = QuestionList(self.questions) + + # -------------------------------------------------------------------------------------------------- + + def format_string_block(self, string: str) -> str: + """Format a string block with indentation for use in cylc. + + Arguments: + string: string to be placed in quotes and indented + + Returns: + Indented and quoted string. + """ + out_string = '"""\n' + out_string += indent_lines(string, 1) + out_string += '"""' + + return out_string + + # -------------------------------------------------------------------------------------------------- + + def match_platform(self, content: str | dict): + '''Resolve platform-specific entries in mapping. + + Arguments: + content: string or mapping containing platform-designated entries + + Returns: + content filtered by the current platform, if specified + + Examples: + >>> self.match_platform('a') + 'a' + + self.platform = 'nccs_discover_sles15' + >>> self.match_platform({'nccs_discover_sles15': 'a', 'nccs_discover_cascade': 'b'}) + 'a' + ''' + + if isinstance(content, Mapping): + if self.platform in content.keys(): + content = content[self.platform] + elif 'all' in content.keys(): + content = content['all'] + + return content + + # -------------------------------------------------------------------------------------------------- + + def create_new_section(self, + name: str | None = None, + content: str | dict = '' + ) -> CylcSection: + '''Create and retrun a new CylcSection object for use in formatting. + + Arguments: + name: Name of cylc section to be created + content: string or dictionary of contents for the section + ''' + return CylcSection(name, content) + + # -------------------------------------------------------------------------------------------------- + + def resolve_model(self, slurm_dict: Mapping) -> dict: + '''Resolve model-specific entries in slurm dictionary specification, if they exist. + + Arguments: + slurm_dict: dictionary of slurm settings + + Returns: + dictionary of slurm settings with any model-specific defaults resolved + + Examples: + >>> self.model = 'geos_marine' + >>> self.resolve_model({'time': '01:00:00', 'nodes': {'geos_atmosphere': 1, 'geos_marine': 3}}) + + {'time': '01:00:00', 'nodes': 3} + ''' # noqa + if 'all' in slurm_dict.keys() and isinstance(slurm_dict['all'], Mapping): + slurm_dict = update_dict(slurm_dict, slurm_dict['all']) + del slurm_dict['all'] + if self.model in slurm_dict.keys() and isinstance(slurm_dict[self.model], Mapping): + slurm_dict = update_dict(slurm_dict, slurm_dict[self.model]) + + for model in get_model_components(): + if model in slurm_dict.keys(): + del slurm_dict[model] + + return slurm_dict + + # -------------------------------------------------------------------------------------------------- + + def generate_task_slurm_dict(self, slurm_external: Mapping) -> Mapping: + '''Take the external slurm dictionary and merge it with the task's parameters + to get the dict that will be output in the runtime section + + Arguments: + slurm_external: dictionary from `utilities/slurm.py` with defaults from the + platform and user. + + Returns: + Finalized dictionary of slurm defaults for the task. + ''' + + slurm_dict = {} + if self.slurm is not None: + for key, value in self.slurm.items(): + slurm_dict[key] = self.match_platform(value) + + slurm_globals = slurm_external['slurm_directives_global'] + slurm_task = {} + + if 'slurm_directives_tasks' in slurm_external.keys(): + task_directives = slurm_external['slurm_directives_tasks'] + + if self.base_name in task_directives: + slurm_task = task_directives[self.base_name] + if self.scheduling_name in task_directives: + slurm_task = task_directives[self.scheduling_name] + + slurm_dict = {'job-name': self.scheduling_name, + **self.resolve_model(slurm_globals), + **self.resolve_model(slurm_dict), + **self.resolve_model(slurm_task)} + + return slurm_dict + + # -------------------------------------------------------------------------------------------------- + + def runtime_string(self, experiment_dict: Mapping, slurm_external: Mapping) -> str: + '''Return the runtime section for the given task. + + Constructs a CylcSection object by filling in a dictionary with the following components + from the task: + + 1) pre-script + 2) script + 3) platform + 4) execution time limit + 5) execution retry delays + 6) slurm subsection + 7) any additional subsections + + The CylcSection's contents is then converted into a string + + Arguments: + experiment_dict: experiment dictionary from `create_experiment` + slurm_external: external slurm defaults from globals and user defaults + + Returns: + String to place in flow.cylc. + ''' + + platform = experiment_dict['platform'] + runtime_dict = {} + + # Set the pre_script only if it is specified + if self.pre_script: + runtime_dict['pre-script'] = self.format_string_block(self.pre_script) + + # Set the script + if self.script: + script_str = self.script + + if 'pause_on_tasks' in experiment_dict.keys(): + if len(set([self.base_name, self.scheduling_name]) + & set(experiment_dict['pause_on_tasks'])) > 0: + script_str += '\ncylc pause $CYLC_WORKFLOW_ID' + + runtime_dict['script'] = self.format_string_block(script_str) + + # Specify the platform if this is a slurm task + if self.slurm is not None: + runtime_dict['platform'] = platform + + if self.task_time_limit is not None: + runtime_dict['execution time limit'] = self.task_time_limit + + # Set the retry if this task needs it + if self.retry: + runtime_dict['execution retry delays'] = self.retry + + runtime_section = self.create_new_section(self.scheduling_name, runtime_dict) + + # Specify the slurm dictionary with defaults from user and global settings + if self.slurm is not None: + + slurm_dict = self.generate_task_slurm_dict(slurm_external) + + slurm_section_dict = {} + for key, value in slurm_dict.items(): + slurm_section_dict[f'--{key}'] = value + + directive_section = self.create_new_section('directives', slurm_section_dict) + + runtime_section.add_subsection(directive_section) + + # Append additional sections to runtime + for section in self.additional_sections: + runtime_section.add_subsection(section) + + # Check slurm messaging parameters + events = self.mail_events + + # Add messaging section + if len(events) > 0 and "email_address" in experiment_dict and \ + experiment_dict["email_address"] != "defer_to_user": + email_address = experiment_dict['email_address'] + address_section = self.create_new_section('mail', f'to = {email_address}') + runtime_section.add_subsection(address_section) + + event_str = "{% if environ['SWELL_SEND_MESSAGES'] %}\n" + event_str += "mail events = " + ', '.join(events) + event_str += "\n{% endif %}\n" + + event_section = self.create_new_section('events', event_str) + runtime_section.add_subsection(event_section) + + runtime_string = runtime_section.get_section_str(level=1) + + runtime_string += ' # ' + '-' * 96 + '\n\n' + + return runtime_string + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/bufr_to_ioda.py b/src/swell/tasks/bufr_to_ioda.py index 0ea370754..3a0edd99d 100644 --- a/src/swell/tasks/bufr_to_ioda.py +++ b/src/swell/tasks/bufr_to_ioda.py @@ -14,6 +14,8 @@ import shutil from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes # -------------------------------------------------------------------------------------------------- @@ -79,6 +81,18 @@ } # -------------------------------------------------------------------------------------------------- +task_name = 'BufrToIoda' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + +# -------------------------------------------------------------------------------------------------- + class BufrToIoda(taskBase): diff --git a/src/swell/tasks/build_geos.py b/src/swell/tasks/build_geos.py index 614db7023..04634d1fe 100644 --- a/src/swell/tasks/build_geos.py +++ b/src/swell/tasks/build_geos.py @@ -11,12 +11,27 @@ import os from swell.tasks.base.task_base import taskBase -from swell.utilities.build import build_and_source_dirs +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.shell_commands import run_subprocess, create_executable_file # -------------------------------------------------------------------------------------------------- +task_name = 'BuildGeos' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.geos_build_method() + ] + +# -------------------------------------------------------------------------------------------------- + class BuildGeos(taskBase): @@ -27,15 +42,19 @@ def execute(self) -> None: swell_exp_path = self.experiment_path() geos_gcm_path = os.path.join(swell_exp_path, 'GEOSgcm') + from swell.utilities.build import build_and_source_dirs + # Get paths to build and source # ----------------------------- geos_gcm_build_path, geos_gcm_source_path = build_and_source_dirs(geos_gcm_path) os.makedirs(geos_gcm_build_path, exist_ok=True) + geos_build_method = self.config.geos_build_method() + # Check that the choice is to create build # ---------------------------------------- - if not self.config.geos_build_method() == 'create': - self.logger.abort(f'Found \'{jedi_build_method}\' for jedi_build_method in the ' + if not geos_build_method == 'create': + self.logger.abort(f'Found \'{geos_build_method}\' for jedi_build_method in the ' f'experiment dictionary. Must be \'create\'.') # Create script that encapsulates the steps of building GEOS diff --git a/src/swell/tasks/build_geos_by_linking.py b/src/swell/tasks/build_geos_by_linking.py index 16fc91ee6..8ffdf19bf 100644 --- a/src/swell/tasks/build_geos_by_linking.py +++ b/src/swell/tasks/build_geos_by_linking.py @@ -11,11 +11,28 @@ import os from swell.tasks.base.task_base import taskBase -from swell.utilities.build import build_and_source_dirs, link_path +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'BuildGeosByLinking' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.mail_events = ['submit-failed'] + self.questions = [ + qd.existing_geos_gcm_build_path(), + qd.geos_build_method() + ] + +# -------------------------------------------------------------------------------------------------- + class BuildGeosByLinking(taskBase): @@ -26,6 +43,8 @@ def execute(self) -> None: swell_exp_path = self.experiment_path() geos_gcm_path = os.path.join(swell_exp_path, 'GEOSgcm') + from swell.utilities.build import build_and_source_dirs, link_path + # Get paths to build and source # ----------------------------- geos_gcm_build_path, geos_gcm_source_path = build_and_source_dirs(geos_gcm_path) diff --git a/src/swell/tasks/build_jedi.py b/src/swell/tasks/build_jedi.py index d64bd0b62..b2e3d9a61 100644 --- a/src/swell/tasks/build_jedi.py +++ b/src/swell/tasks/build_jedi.py @@ -10,10 +10,26 @@ import os -from jedi_bundle.bin.jedi_bundle import execute_tasks, get_bundles - from swell.tasks.base.task_base import taskBase -from swell.utilities.build import set_jedi_bundle_config, build_and_source_dirs +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'BuildJedi' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = 'PT3H' + self.slurm = {} + self.questions = [ + qd.bundles(), + qd.jedi_build_method() + ] # -------------------------------------------------------------------------------------------------- @@ -22,11 +38,15 @@ class BuildJedi(taskBase): def execute(self) -> None: + from jedi_bundle.bin.jedi_bundle import execute_tasks, get_bundles + # Get the experiment/jedi_bundle directory # ---------------------------------------- swell_exp_path = self.experiment_path() jedi_bundle_path = os.path.join(swell_exp_path, 'jedi_bundle') + from swell.utilities.build import set_jedi_bundle_config, build_and_source_dirs + # Get paths to build and source # ----------------------------- jedi_bundle_build_path, jedi_bundle_source_path = build_and_source_dirs(jedi_bundle_path) diff --git a/src/swell/tasks/build_jedi_by_linking.py b/src/swell/tasks/build_jedi_by_linking.py index f746a70f8..fa8fb0156 100644 --- a/src/swell/tasks/build_jedi_by_linking.py +++ b/src/swell/tasks/build_jedi_by_linking.py @@ -11,7 +11,26 @@ import os from swell.tasks.base.task_base import taskBase -from swell.utilities.build import build_and_source_dirs, link_path +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd +from swell.tasks.base.task_attributes import task_attributes + +# -------------------------------------------------------------------------------------------------- + +task_name = 'BuildJediByLinking' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.mail_events = ['submit-failed'] + self.questions = [ + qd.existing_jedi_build_directory(), + qd.existing_jedi_build_directory_pinned(), + qd.jedi_build_method() + ] # -------------------------------------------------------------------------------------------------- @@ -30,6 +49,8 @@ def execute(self) -> None: swell_exp_path = self.experiment_path() jedi_bundle_path = os.path.join(swell_exp_path, 'jedi_bundle') + from swell.utilities.build import build_and_source_dirs, link_path + # Get paths to build and source jedi_bundle_build_path, jedi_bundle_source_path = build_and_source_dirs(jedi_bundle_path) diff --git a/src/swell/tasks/clean_cycle.py b/src/swell/tasks/clean_cycle.py index f552bbd5c..e9b7f6e48 100644 --- a/src/swell/tasks/clean_cycle.py +++ b/src/swell/tasks/clean_cycle.py @@ -11,12 +11,30 @@ import os import shutil from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.datetime_util import datetime_formats from datetime import datetime as dt import glob # -------------------------------------------------------------------------------------------------- +task_name = 'CleanCycle' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.clean_patterns() + ] + +# -------------------------------------------------------------------------------------------------- + class CleanCycle(taskBase): diff --git a/src/swell/tasks/clone_geos.py b/src/swell/tasks/clone_geos.py index 6190a81a0..b36512a1a 100644 --- a/src/swell/tasks/clone_geos.py +++ b/src/swell/tasks/clone_geos.py @@ -11,12 +11,29 @@ import os from swell.tasks.base.task_base import taskBase -from swell.utilities.build import build_and_source_dirs, link_path +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.git_utils import git_clone from swell.utilities.shell_commands import run_subprocess # -------------------------------------------------------------------------------------------------- +task_name = 'CloneGeos' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.existing_geos_gcm_source_path(), + qd.geos_build_method(), + qd.geos_gcm_tag() + ] + +# -------------------------------------------------------------------------------------------------- + class CloneGeos(taskBase): @@ -27,6 +44,8 @@ def execute(self) -> None: swell_exp_path = self.experiment_path() geos_gcm_path = os.path.join(swell_exp_path, 'GEOSgcm') + from swell.utilities.build import build_and_source_dirs, link_path + # Get paths to build and source # ----------------------------- geos_gcm_build_path, geos_gcm_source_path = build_and_source_dirs(geos_gcm_path) diff --git a/src/swell/tasks/clone_geos_mksi.py b/src/swell/tasks/clone_geos_mksi.py index 555b6638a..3dd5c2501 100644 --- a/src/swell/tasks/clone_geos_mksi.py +++ b/src/swell/tasks/clone_geos_mksi.py @@ -9,8 +9,26 @@ import os +import subprocess from swell.tasks.base.task_base import taskBase -from swell.utilities.build import link_path +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'CloneGeosMksi' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.model_dep = True + self.questions = [ + qd.observing_system_records_mksi_path(), + qd.observing_system_records_mksi_path_tag() + ] # -------------------------------------------------------------------------------------------------- @@ -23,6 +41,8 @@ def execute(self) -> None: Generate the satellite channel record from GEOSmksi files """ + from swell.utilities.build import link_path + # This task should only execute for geos_atmosphere # ------------------------------------------------- if self.get_model() != 'geos_atmosphere': @@ -41,9 +61,17 @@ def execute(self) -> None: branch = 'develop' else: branch = tag - # Clone GEOS_mksi develop repo to experiment directory - os.system(f'git clone -b {branch} https://github.com/GEOS-ESM/GEOS_mksi.git ' - + os.path.join(self.experiment_path(), 'GEOS_mksi')) + + mksi_path = os.path.join(self.experiment_path(), 'GEOS_mksi') + + if os.path.exists(mksi_path): + # Checkout the branch + subprocess.run(['git', 'checkout', branch], cwd=mksi_path, check=True) + else: + # Clone GEOS_mksi develop repo to experiment directory + subprocess.run(['git', 'clone', '-b', branch, + 'https://github.com/GEOS-ESM/GEOS_mksi.git', + mksi_path], check=True) else: # Link the source code directory link_path(self.config.observing_system_records_mksi_path(), diff --git a/src/swell/tasks/clone_gmao_perllib.py b/src/swell/tasks/clone_gmao_perllib.py index 573876822..5123ca75e 100644 --- a/src/swell/tasks/clone_gmao_perllib.py +++ b/src/swell/tasks/clone_gmao_perllib.py @@ -12,6 +12,23 @@ import subprocess from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'CloneGmaoPerllib' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.existing_perllib_path(), + qd.gmao_perllib_tag() + ] # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/clone_jedi.py b/src/swell/tasks/clone_jedi.py index 043975437..8b26b6430 100644 --- a/src/swell/tasks/clone_jedi.py +++ b/src/swell/tasks/clone_jedi.py @@ -10,21 +10,42 @@ import os -from jedi_bundle.bin.jedi_bundle import execute_tasks, get_bundles - -from swell.utilities.build import link_path from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.pinned_versions.check_hashes import check_hashes -from swell.utilities.build import set_jedi_bundle_config, build_and_source_dirs +from swell.tasks.base.task_attributes import task_attributes # -------------------------------------------------------------------------------------------------- +task_name = 'CloneJedi' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.bundles(), + qd.existing_jedi_source_directory(), + qd.existing_jedi_source_directory_pinned(), + qd.jedi_build_method() + ] + +# -------------------------------------------------------------------------------------------------- + class CloneJedi(taskBase): def execute(self) -> None: + # Import JEDI modules + # ------------------- + from jedi_bundle.bin.jedi_bundle import execute_tasks, get_bundles + from swell.utilities.build import set_jedi_bundle_config, build_and_source_dirs, link_path + # Get the experiment/jedi_bundle directory # ---------------------------------------- swell_exp_path = self.experiment_path() diff --git a/src/swell/tasks/convert_obs_to_ioda.py b/src/swell/tasks/convert_obs_to_ioda.py index 0f4c97910..314ab950e 100644 --- a/src/swell/tasks/convert_obs_to_ioda.py +++ b/src/swell/tasks/convert_obs_to_ioda.py @@ -20,8 +20,28 @@ import yaml from datetime import datetime +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.tasks.base.task_base import taskBase +# -------------------------------------------------------------------------------------------------- + +task_name = 'ConvertObsToIoda' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.converter_path(), + qd.dry_run(), + qd.obs_to_download() + ] + +# -------------------------------------------------------------------------------------------------- + class ConvertObsToIoda(taskBase): """Convert downloaded raw observation files to IODA format. diff --git a/src/swell/tasks/download_obs.py b/src/swell/tasks/download_obs.py index 6f3f79b97..f7fc1dad4 100644 --- a/src/swell/tasks/download_obs.py +++ b/src/swell/tasks/download_obs.py @@ -31,6 +31,25 @@ import requests from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes + +# -------------------------------------------------------------------------------------------------- + +task_name = 'DownloadObs' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.dry_run(), + qd.obs_to_download(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- class DownloadObs(taskBase): diff --git a/src/swell/tasks/eva_comparison_increment.py b/src/swell/tasks/eva_comparison_increment.py index 8cfc16918..2189df651 100644 --- a/src/swell/tasks/eva_comparison_increment.py +++ b/src/swell/tasks/eva_comparison_increment.py @@ -12,15 +12,33 @@ from ruamel.yaml import YAML import glob -from eva.eva_driver import eva - from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.data_assimilation_window_params import DataAssimilationWindowParams from swell.utilities.comparisons import comparison_tags, experiment_ids # -------------------------------------------------------------------------------------------------- +task_name = 'EvaComparisonIncrement' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.marine_models(), + qd.window_length(), + qd.window_type() + ] + +# -------------------------------------------------------------------------------------------------- + class EvaComparisonIncrement(taskBase): @@ -39,6 +57,9 @@ def window_info_from_config(self, path: str): def execute(self) -> None: + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva + model = self.get_model() # Open the eva configuration file @@ -110,9 +131,9 @@ def execute(self) -> None: incr_file_2 = f'ocn.*.incr.{ocn_cycle_time}.nc' cycle_dir_1 = os.path.join(os.path.dirname(experiment_path_1), '..', 'run', - self.__datetime__.string_directory(), self.get_model()) + self.__dto__().string_directory(), self.get_model()) cycle_dir_2 = os.path.join(os.path.dirname(experiment_path_2), '..', 'run', - self.__datetime__.string_directory(), self.get_model()) + self.__dto__().string_directory(), self.get_model()) # Files to fill the template in the config file increment_file_path_1 = glob.glob(os.path.join(cycle_dir_1, incr_file_1))[0] diff --git a/src/swell/tasks/eva_comparison_jedi_log.py b/src/swell/tasks/eva_comparison_jedi_log.py index 7c96a4155..cec065610 100644 --- a/src/swell/tasks/eva_comparison_jedi_log.py +++ b/src/swell/tasks/eva_comparison_jedi_log.py @@ -11,20 +11,39 @@ import os from ruamel.yaml import YAML -from eva.eva_driver import eva - from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.comparisons import comparison_tags # -------------------------------------------------------------------------------------------------- +task_name = 'EvaComparisonJediLog' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.comparison_log_type() + ] + +# -------------------------------------------------------------------------------------------------- + class EvaComparisonJediLog(taskBase): def execute(self) -> None: + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva + # Get the model # ------------- model = self.get_model() @@ -63,9 +82,9 @@ def execute(self) -> None: experiment_id_2 = experiment_dict_2['experiment_id'] cycle_dir_1 = os.path.join(os.path.dirname(experiment_path_1), '..', 'run', - self.__datetime__.string_directory(), self.get_model()) + self.__dto__().string_directory(), self.get_model()) cycle_dir_2 = os.path.join(os.path.dirname(experiment_path_2), '..', 'run', - self.__datetime__.string_directory(), self.get_model()) + self.__dto__().string_directory(), self.get_model()) # Info to task log info_string = 'Running Eva to plot from the jedi_log file' diff --git a/src/swell/tasks/eva_comparison_observations.py b/src/swell/tasks/eva_comparison_observations.py index 59a318cbe..9fac4763f 100644 --- a/src/swell/tasks/eva_comparison_observations.py +++ b/src/swell/tasks/eva_comparison_observations.py @@ -12,11 +12,12 @@ import os import yaml -from eva.eva_driver import eva - from swell.swell_path import get_swell_path from swell.deployment.platforms.platforms import login_or_compute from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.dictionary import remove_matching_keys, replace_string_in_dictionary from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.observations import ioda_name_to_long_name @@ -26,9 +27,28 @@ # -------------------------------------------------------------------------------------------------- +task_name = 'EvaComparisonObservations' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {} + self.questions = [ + qd.comparison_log_type(), + ] + +# -------------------------------------------------------------------------------------------------- + # Pass through to avoid confusion with optional logger argument inside eva -def run_eva(eva_dict: dict) -> eva: +def run_eva(eva_dict: dict): + from eva.eva_driver import eva + eva(eva_dict) diff --git a/src/swell/tasks/eva_increment.py b/src/swell/tasks/eva_increment.py index ae798fd3d..ee80dd555 100644 --- a/src/swell/tasks/eva_increment.py +++ b/src/swell/tasks/eva_increment.py @@ -11,18 +11,39 @@ import os from ruamel.yaml import YAML -from eva.eva_driver import eva - from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.jinja2 import template_string_jinja2 # -------------------------------------------------------------------------------------------------- +task_name = 'EvaIncrement' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.marine_models(), + qd.window_length(), + qd.window_type() + ] + +# -------------------------------------------------------------------------------------------------- + class EvaIncrement(taskBase): def execute(self) -> None: + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva + # Get the model and window type # ----------------------------- model = self.get_model() @@ -50,8 +71,8 @@ def execute(self) -> None: dto=True) window_begin = window_begin_dto.strftime('%Y%m%d_%H%M%Sz') - local_bkg_dir, local_bkg_dto = self.da_window_params.local_background_time( - self.config.window_length(), self.config.window_type(), dto=True) + local_bkg_dir, local_bkg_dto = self.da_window_params.local_background_time_dto( + self.config.window_length(), self.config.window_type()) local_bkg_time = local_bkg_dto.strftime('%Y%m%d_%H%M%Sz') # Define the increment filename and path diff --git a/src/swell/tasks/eva_jedi_log.py b/src/swell/tasks/eva_jedi_log.py index a648606d0..1f98dc031 100644 --- a/src/swell/tasks/eva_jedi_log.py +++ b/src/swell/tasks/eva_jedi_log.py @@ -11,19 +11,34 @@ import os from ruamel.yaml import YAML -from eva.eva_driver import eva - from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes from swell.utilities.jinja2 import template_string_jinja2 # -------------------------------------------------------------------------------------------------- +task_name = 'EvaJediLog' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + +# -------------------------------------------------------------------------------------------------- + class EvaJediLog(taskBase): def execute(self) -> None: + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva + # Get the model # ------------- model = self.get_model() diff --git a/src/swell/tasks/eva_observations.py b/src/swell/tasks/eva_observations.py index 71544937a..e2cf9d5d9 100644 --- a/src/swell/tasks/eva_observations.py +++ b/src/swell/tasks/eva_observations.py @@ -12,10 +12,11 @@ import os from ruamel.yaml import YAML -from eva.eva_driver import eva - from swell.deployment.platforms.platforms import login_or_compute from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.dictionary import remove_matching_keys, replace_string_in_dictionary from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.observations import ioda_name_to_long_name @@ -25,12 +26,39 @@ # Pass through to avoid confusion with optional logger argument inside eva -def run_eva(eva_dict: dict) -> eva: +def run_eva(eva_dict: dict): + + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva eva(eva_dict) # -------------------------------------------------------------------------------------------------- +task_name = 'EvaObservations' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {} + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.marine_models(), + qd.observing_system_records_path(), + qd.window_length(), + qd.marine_models(), + ] + +# -------------------------------------------------------------------------------------------------- + class EvaObservations(taskBase): diff --git a/src/swell/tasks/eva_timeseries.py b/src/swell/tasks/eva_timeseries.py index 0610654f6..8c4f715ce 100644 --- a/src/swell/tasks/eva_timeseries.py +++ b/src/swell/tasks/eva_timeseries.py @@ -14,10 +14,11 @@ import os from ruamel.yaml import YAML -from eva.eva_driver import eva - from swell.deployment.platforms.platforms import login_or_compute from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.datetime_util import datetime_formats from swell.utilities.dictionary import remove_matching_keys, replace_string_in_dictionary from swell.utilities.jinja2 import template_string_jinja2 @@ -27,12 +28,38 @@ # Pass through to avoid confusion with optional logger argument inside eva -def run_eva(eva_dict: dict) -> eva: +def run_eva(eva_dict: dict): + + # Local import because module is not loaded until experiment launch + from eva.eva_driver import eva eva(eva_dict) # -------------------------------------------------------------------------------------------------- +task_name = 'EvaTimeseries' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {} + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.window_length(), + qd.ncdiag_experiments(), + qd.marine_models(), + ] + +# -------------------------------------------------------------------------------------------------- + class EvaTimeseries(taskBase): diff --git a/src/swell/tasks/generate_b_climatology.py b/src/swell/tasks/generate_b_climatology.py index 0dc8f7872..7cbfeb96d 100644 --- a/src/swell/tasks/generate_b_climatology.py +++ b/src/swell/tasks/generate_b_climatology.py @@ -9,11 +9,54 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.shell_commands import run_track_log_subprocess from swell.utilities.file_system_operations import check_if_files_exist_in_path # -------------------------------------------------------------------------------------------------- +task_name = 'GenerateBClimatology' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.retry = '2*PT1M' + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.swell_static_files(), + qd.swell_static_files_user(), + qd.analysis_variables(), + qd.background_error_model(), + qd.generate_yaml_and_exit(), + qd.gradient_norm_reduction(), + qd.gsibec_configuration(), + qd.gsibec_nlats(), + qd.gsibec_nlons(), + qd.jedi_forecast_model(), + qd.marine_models(), + qd.minimizer(), + qd.number_of_iterations(), + qd.observing_system_records_path(), + qd.total_processors(), + qd.window_length(), + qd.window_type() + ] + +# -------------------------------------------------------------------------------------------------- + class GenerateBClimatology(taskBase): diff --git a/src/swell/tasks/generate_b_climatology_by_linking.py b/src/swell/tasks/generate_b_climatology_by_linking.py index 11ffe21c1..bc5ab9ee9 100644 --- a/src/swell/tasks/generate_b_climatology_by_linking.py +++ b/src/swell/tasks/generate_b_climatology_by_linking.py @@ -8,11 +8,35 @@ import os from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.file_system_operations import link_all_files_from_first_in_hierarchy_of_sources # -------------------------------------------------------------------------------------------------- +task_name = 'GenerateBClimatologyByLinking' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.swell_static_files(), + qd.swell_static_files_user(), + qd.background_error_model(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type() + ] + +# -------------------------------------------------------------------------------------------------- + class GenerateBClimatologyByLinking(taskBase): diff --git a/src/swell/tasks/generate_observing_system_records.py b/src/swell/tasks/generate_observing_system_records.py index bf8d71fdc..1f89a67b6 100644 --- a/src/swell/tasks/generate_observing_system_records.py +++ b/src/swell/tasks/generate_observing_system_records.py @@ -11,10 +11,30 @@ import os from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.observing_system_records import ObservingSystemRecords # -------------------------------------------------------------------------------------------------- +task_name = 'GenerateObservingSystemRecords' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.observations(), + qd.observing_system_records_mksi_path(), + qd.observing_system_records_path() + ] + +# -------------------------------------------------------------------------------------------------- + class GenerateObservingSystemRecords(taskBase): diff --git a/src/swell/tasks/get_background.py b/src/swell/tasks/get_background.py index e401d394c..379641065 100644 --- a/src/swell/tasks/get_background.py +++ b/src/swell/tasks/get_background.py @@ -9,11 +9,37 @@ from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials, get_r2d2_model_name + +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +from swell.utilities.r2d2_utils import load_r2d2_credentials, get_r2d2_model_name import isodate import os -import r2d2 + +# -------------------------------------------------------------------------------------------------- + + +task_name = 'GetBackground' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.window_length(), + qd.window_type(), + qd.window_length(), + qd.background_experiment(), + qd.background_frequency(), + qd.horizontal_resolution(), + qd.marine_models(), + ] # -------------------------------------------------------------------------------------------------- @@ -31,6 +57,7 @@ def execute(self) -> None: # Load R2D2 credentials # --------------------- + import r2d2 load_r2d2_credentials( self.logger, self.platform(), diff --git a/src/swell/tasks/get_background_geos_experiment.py b/src/swell/tasks/get_background_geos_experiment.py index 778a0406c..7a5ff4b4f 100644 --- a/src/swell/tasks/get_background_geos_experiment.py +++ b/src/swell/tasks/get_background_geos_experiment.py @@ -14,10 +14,32 @@ import tarfile from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.datetime_util import datetime_formats # -------------------------------------------------------------------------------------------------- +task_name = 'GetBackgroundGeosExperiment' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.mail_events = ['submit-failed'] + self.questions = [ + qd.horizontal_resolution(), + qd.background_experiment(), + qd.background_time_offset(), + qd.geos_x_background_directory() + ] + +# -------------------------------------------------------------------------------------------------- + class GetBackgroundGeosExperiment(taskBase): diff --git a/src/swell/tasks/get_bufr.py b/src/swell/tasks/get_bufr.py index bef9fbedd..ef1b6a716 100644 --- a/src/swell/tasks/get_bufr.py +++ b/src/swell/tasks/get_bufr.py @@ -14,6 +14,24 @@ from datetime import datetime as dt from swell.utilities.datetime_util import datetime_formats from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'GetBufr' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.bufr_obs_classes() + ] # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/get_coupled_geos_restart.py b/src/swell/tasks/get_coupled_geos_restart.py index 75c1d57c0..e35ab9605 100644 --- a/src/swell/tasks/get_coupled_geos_restart.py +++ b/src/swell/tasks/get_coupled_geos_restart.py @@ -11,10 +11,31 @@ import glob from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_attributes import task_attributes +from swell.tasks.base.task_setup import TaskSetup +from swell.configuration import question_defaults as qd from swell.utilities.file_system_operations import copy_to_dst_dir # -------------------------------------------------------------------------------------------------- +task_name = 'GetCoupledGeosRestart' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.geos_homdir(), + qd.geos_expdir_different(), + qd.geos_expdir(), + qd.initial_restarts_method() + ] + +# -------------------------------------------------------------------------------------------------- + class GetCoupledGeosRestart(taskBase): diff --git a/src/swell/tasks/get_ensemble.py b/src/swell/tasks/get_ensemble.py index 08b77b97b..b3c9c7f29 100644 --- a/src/swell/tasks/get_ensemble.py +++ b/src/swell/tasks/get_ensemble.py @@ -12,10 +12,26 @@ import os from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'GetEnsemble' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.questions = [ + qd.path_to_ensemble() + ] + +# -------------------------------------------------------------------------------------------------- + class GetEnsemble(taskBase): diff --git a/src/swell/tasks/get_ensemble_geos_experiment.py b/src/swell/tasks/get_ensemble_geos_experiment.py index 11d69feab..166a96fc7 100644 --- a/src/swell/tasks/get_ensemble_geos_experiment.py +++ b/src/swell/tasks/get_ensemble_geos_experiment.py @@ -13,10 +13,30 @@ import tarfile from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.datetime_util import datetime_formats # -------------------------------------------------------------------------------------------------- +task_name = 'GetEnsembleGeosExperiment' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.background_experiment(), + qd.background_time_offset(), + qd.geos_x_ensemble_directory() + ] + +# -------------------------------------------------------------------------------------------------- + class GetEnsembleGeosExperiment(taskBase): diff --git a/src/swell/tasks/get_geos_adas_background.py b/src/swell/tasks/get_geos_adas_background.py index 354d53515..c6ea42689 100644 --- a/src/swell/tasks/get_geos_adas_background.py +++ b/src/swell/tasks/get_geos_adas_background.py @@ -14,10 +14,28 @@ import re from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'GetGeosAdasBackground' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.path_to_geos_adas_background() + ] + +# -------------------------------------------------------------------------------------------------- + class GetGeosAdasBackground(taskBase): diff --git a/src/swell/tasks/get_geovals.py b/src/swell/tasks/get_geovals.py index 2f04c884f..152f7a01a 100644 --- a/src/swell/tasks/get_geovals.py +++ b/src/swell/tasks/get_geovals.py @@ -11,16 +11,41 @@ import os from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials -from r2d2 import fetch +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd +from swell.utilities.r2d2_utils import load_r2d2_credentials +# -------------------------------------------------------------------------------------------------- + +task_name = 'GetGeovals' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.geovals_experiment(), + qd.geovals_provider(), + qd.window_length(), + ] # -------------------------------------------------------------------------------------------------- + class GetGeovals(taskBase): def execute(self) -> None: + from r2d2 import fetch + # Load R2D2 credentials # --------------------- load_r2d2_credentials( diff --git a/src/swell/tasks/get_gsi_bc.py b/src/swell/tasks/get_gsi_bc.py index 9ca84ae20..ca02b804a 100644 --- a/src/swell/tasks/get_gsi_bc.py +++ b/src/swell/tasks/get_gsi_bc.py @@ -15,10 +15,29 @@ import tarfile from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'GetGsiBc' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.path_to_gsi_bc_coefficients(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- + class GetGsiBc(taskBase): diff --git a/src/swell/tasks/get_gsi_ncdiag.py b/src/swell/tasks/get_gsi_ncdiag.py index 3567e9581..45e2c39fd 100644 --- a/src/swell/tasks/get_gsi_ncdiag.py +++ b/src/swell/tasks/get_gsi_ncdiag.py @@ -12,10 +12,28 @@ import os from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'GetGsiNcdiag' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.path_to_gsi_nc_diags() + ] + +# -------------------------------------------------------------------------------------------------- + class GetGsiNcdiag(taskBase): @@ -41,7 +59,7 @@ def execute(self) -> None: os.makedirs(gsi_diag_dir, 0o755, exist_ok=True) # Assert that some files were found - self.logger.assert_abort(len(gsi_diag_path_files) != 0 is not None, f'No ncdiag ' + + self.logger.assert_abort(len(gsi_diag_path_files) != 0, f'No ncdiag ' + f'files found in the source directory ' + f'\'{gsi_diag_path}\'') diff --git a/src/swell/tasks/get_ncdiags.py b/src/swell/tasks/get_ncdiags.py index 9e0a0aff7..fac272331 100644 --- a/src/swell/tasks/get_ncdiags.py +++ b/src/swell/tasks/get_ncdiags.py @@ -9,8 +9,31 @@ import os from swell.tasks.base.task_base import taskBase -from r2d2 import fetch -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd +from swell.utilities.r2d2_utils import load_r2d2_credentials + +# -------------------------------------------------------------------------------------------------- + +task_name = 'GetNcdiags' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.ncdiag_experiments(), + qd.marine_models(), + qd.window_length(), + ] # -------------------------------------------------------------------------------------------------- @@ -23,6 +46,10 @@ class GetNcdiags(taskBase): def execute(self) -> None: + # Import modules + # -------------- + from r2d2 import fetch + # Load R2D2 credentials # --------------------- load_r2d2_credentials( diff --git a/src/swell/tasks/get_obs_not_in_r2d2.py b/src/swell/tasks/get_obs_not_in_r2d2.py index 6ad2e21e2..418700784 100644 --- a/src/swell/tasks/get_obs_not_in_r2d2.py +++ b/src/swell/tasks/get_obs_not_in_r2d2.py @@ -13,10 +13,29 @@ import subprocess from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd # -------------------------------------------------------------------------------------------------- +task_name = 'GetObsNotInR2d2' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.mail_events = ['submit-failed'] + self.questions = [ + qd.ioda_locations_not_in_r2d2(), + ] + +# -------------------------------------------------------------------------------------------------- + class GetObsNotInR2d2(taskBase): diff --git a/src/swell/tasks/get_observations.py b/src/swell/tasks/get_observations.py index 974c7d950..3231f2204 100644 --- a/src/swell/tasks/get_observations.py +++ b/src/swell/tasks/get_observations.py @@ -8,22 +8,46 @@ # -------------------------------------------------------------------------------------------------- import isodate -import netCDF4 as nc import numpy as np import os -import r2d2 import shutil -from typing import Union +from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor from datetime import timedelta, datetime as dt from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials, get_r2d2_model_name +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +from swell.utilities.r2d2_utils import get_r2d2_model_name, load_r2d2_credentials from swell.utilities.datetime_util import datetime_formats from swell.utilities.observations import get_ioda_names_list, get_provider_for_observation # ---------------------------------------------------------------------------------------------- +task_name = 'GetObservations' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.cycling_varbc(), + qd.obs_experiment(), + qd.observing_system_records_path(), + qd.window_length(), + ] + +# -------------------------------------------------------------------------------------------------- + def run_r2d2_fetch(r2d2_dict: dict) -> None: @@ -38,6 +62,8 @@ def run_r2d2_fetch(r2d2_dict: dict) -> None: These values will be popped from the dictionary before running the fetch command """ + import r2d2 + fetch_empty_obs = r2d2_dict.pop('fetch_empty', False) cycle_dir = r2d2_dict.pop('cycle_dir') logger = r2d2_dict.pop('logger') @@ -79,9 +105,9 @@ def run_r2d2_fetch(r2d2_dict: dict) -> None: # Change the permissions os.chmod(target_file, 0o644) - # -------------------------------------------------------------------------------------------------- + class GetObservations(taskBase): def execute(self) -> None: @@ -404,7 +430,7 @@ def execute(self) -> None: # ---------------------------------------------------------------------------------------------- - def get_tlapse_files(self, observation_dict: dict) -> Union[None, int]: + def get_tlapse_files(self, observation_dict: dict) -> Iterator[int | None]: # Function to locate instances of tlapse in the obs operator config @@ -506,6 +532,7 @@ def create_obs_time_list( # Get the target data from the netcdf file # ---------------------------------------- def get_data(self, input_file: str, group: str, var_name: str) -> object: + import netCDF4 as nc with nc.Dataset(input_file, 'r') as ds: return ds[group][var_name][:] @@ -531,6 +558,8 @@ def read_and_combine(self, input_filenames: list, output_filename: str) -> None: if os.path.exists(output_filename): os.remove(output_filename) + import netCDF4 as nc + # Reduce the list of input files to only those that exist # ------------------------------------------------------------- existing_files = [f for f in input_filenames if os.path.exists(f)] diff --git a/src/swell/tasks/get_restart_cf.py b/src/swell/tasks/get_restart_cf.py index 93f768811..bbd75ad95 100644 --- a/src/swell/tasks/get_restart_cf.py +++ b/src/swell/tasks/get_restart_cf.py @@ -9,11 +9,31 @@ from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd +from swell.utilities.r2d2_utils import load_r2d2_credentials import isodate import os -import r2d2 + +# ---------------------------------------------------------------------------------------------- + +task_name = 'GetRestartCf' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.window_length(), + qd.rst_experiment(), + qd.rst_file_types(), + qd.horizontal_resolution() + ] # -------------------------------------------------------------------------------------------------- @@ -25,6 +45,8 @@ def execute(self) -> None: """ + import r2d2 + # Load R2D2 credentials # --------------------- load_r2d2_credentials(self.logger, self.platform()) diff --git a/src/swell/tasks/gsi_bc_to_ioda.py b/src/swell/tasks/gsi_bc_to_ioda.py index e86e61921..cd2811fec 100644 --- a/src/swell/tasks/gsi_bc_to_ioda.py +++ b/src/swell/tasks/gsi_bc_to_ioda.py @@ -13,12 +13,31 @@ from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.dictionary import write_dict_to_yaml from swell.utilities.shell_commands import run_track_log_subprocess # -------------------------------------------------------------------------------------------------- +task_name = 'GsiBcToIoda' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.path_to_gsi_bc_coefficients(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- + class GsiBcToIoda(taskBase): diff --git a/src/swell/tasks/gsi_ncdiag_to_ioda.py b/src/swell/tasks/gsi_ncdiag_to_ioda.py index 18d296cda..cb558cfaf 100644 --- a/src/swell/tasks/gsi_ncdiag_to_ioda.py +++ b/src/swell/tasks/gsi_ncdiag_to_ioda.py @@ -14,22 +14,43 @@ import os import re -# Ioda converters -import pyiodaconv.gsi_ncdiag as gsid -from pyiodaconv.combine_obsspace import combine_obsspace - from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.datetime_util import datetime_formats from swell.utilities.shell_commands import run_subprocess, create_executable_file # -------------------------------------------------------------------------------------------------- +task_name = 'GsiNcdiagToIoda' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.observations(), + qd.produce_geovals(), + qd.single_observations(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- + class GsiNcdiagToIoda(taskBase): def execute(self) -> None: + # Ioda converters + import pyiodaconv.gsi_ncdiag as gsid + from pyiodaconv.combine_obsspace import combine_obsspace + # Parse configuration # ------------------- observations = self.config.observations() diff --git a/src/swell/tasks/ingest_obs.py b/src/swell/tasks/ingest_obs.py index ff1bac89f..573bbf0bc 100644 --- a/src/swell/tasks/ingest_obs.py +++ b/src/swell/tasks/ingest_obs.py @@ -15,12 +15,32 @@ import yaml from datetime import datetime -import requests - from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd +from swell.utilities.r2d2_utils import load_r2d2_credentials from swell.utilities.observations import get_ioda_names_list, get_provider_for_observation -import r2d2 + +# -------------------------------------------------------------------------------------------------- + +task_name = 'IngestObs' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.dry_run(), + qd.obs_to_ingest(), + qd.window_length(), + # qd.window_offset(), + ] + +# -------------------------------------------------------------------------------------------------- class IngestObs(taskBase): @@ -154,6 +174,10 @@ def process_obs_config( dry_run: bool, r2d2_datastore: str | None = None, ) -> tuple[list[str], list[tuple[str, str]]]: + + import r2d2 + import requests + """Process a single observation configuration file.""" ingested = [] failed = [] diff --git a/src/swell/tasks/jedi_log_comparison.py b/src/swell/tasks/jedi_log_comparison.py index ebb081d6f..b7eb81bb0 100644 --- a/src/swell/tasks/jedi_log_comparison.py +++ b/src/swell/tasks/jedi_log_comparison.py @@ -14,6 +14,9 @@ import numpy as np from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.comparisons import comparison_tags # -------------------------------------------------------------------------------------------------- @@ -22,6 +25,21 @@ # -------------------------------------------------------------------------------------------------- +task_name = 'JediLogComparison' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.model_dep = True + self.questions = [ + qd.number_of_iterations(), + qd.comparison_log_type(), + ] + +# -------------------------------------------------------------------------------------------------- + class JediLogComparison(taskBase): diff --git a/src/swell/tasks/jedi_oops_log_parser.py b/src/swell/tasks/jedi_oops_log_parser.py index 543f77dda..01781072e 100644 --- a/src/swell/tasks/jedi_oops_log_parser.py +++ b/src/swell/tasks/jedi_oops_log_parser.py @@ -12,6 +12,25 @@ import subprocess from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'JediOopsLogParser' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.parser_options(), + qd.comparison_log_type() + ] # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/link_coupled_geos_output.py b/src/swell/tasks/link_coupled_geos_output.py index fb566c6c1..abae3f484 100644 --- a/src/swell/tasks/link_coupled_geos_output.py +++ b/src/swell/tasks/link_coupled_geos_output.py @@ -13,10 +13,30 @@ from netCDF4 import Dataset import numpy as np import xarray as xr -from typing import Tuple from swell.utilities.datetime_util import datetime_formats from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_attributes import task_attributes +from swell.tasks.base.task_setup import TaskSetup +from swell.configuration import question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'LinkCoupledGeosOutput' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.window_length(), + qd.window_type(), + qd.background_frequency(), + qd.marine_models() + ] # -------------------------------------------------------------------------------------------------- @@ -44,10 +64,9 @@ def execute(self) -> None: if self.window_type == '4D' or 'fgat' in self.suite_name(): self.background_frequency = self.config.background_frequency() - self.bkgr_time_iso, self.bkgr_time_dto = self.da_window_params.local_background_time( + self.bkgr_time_iso, self.bkgr_time_dto = self.da_window_params.local_background_time_dto( self.window_length, - self.window_type, - dto=True) + self.window_type) # Create source and destination files for linking model output to cycle directories # ----------------------------------------------------------------------------------- @@ -202,7 +221,7 @@ def prepare_cice6_history(self, # ---------------------------------------------------------------------------------------------- - def prepare_cice6_restart(self) -> Tuple[str, str]: + def prepare_cice6_restart(self) -> tuple[str, str]: # CICE6 input in SOCA requires aggregation of multiple variables and # time dimension added to the dataset. # SOCA needs icea area (aicen), ice volume (vicen), and snow area (vsnon) diff --git a/src/swell/tasks/link_geos_output.py b/src/swell/tasks/link_geos_output.py index f2853728a..f81284add 100644 --- a/src/swell/tasks/link_geos_output.py +++ b/src/swell/tasks/link_geos_output.py @@ -12,11 +12,30 @@ import os from netCDF4 import Dataset import numpy as np -import xarray as xr -from typing import Tuple from swell.utilities.datetime_util import datetime_formats from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'LinkGeosOutput' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.window_length(), + qd.window_type(), + qd.background_frequency(), + qd.marine_models() + ] # -------------------------------------------------------------------------------------------------- @@ -43,10 +62,9 @@ def execute(self) -> None: if self.window_type == '4D' or 'fgat' in self.suite_name(): self.background_frequency = self.config.background_frequency() - self.bkgr_time_iso, self.bkgr_time_dto = self.da_window_params.local_background_time( + self.bkgr_time_iso, self.bkgr_time_dto = self.da_window_params.local_background_time_dto( self.window_length, - self.window_type, - dto=True) + self.window_type) # Create source and destination files for linking model output to cycle directories # ----------------------------------------------------------------------------------- @@ -188,6 +206,8 @@ def prepare_cice6_history(self, dst_history: str, ) -> None: + import xarray as xr + # Since history already has the aggregated variables, we just need to rename # the dimensions and variables to match SOCA requirements ds = xr.open_dataset(src_history) @@ -201,7 +221,7 @@ def prepare_cice6_history(self, # ---------------------------------------------------------------------------------------------- - def prepare_cice6_restart(self) -> Tuple[str, str]: + def prepare_cice6_restart(self) -> tuple[str, str]: # CICE6 input in SOCA requires aggregation of multiple variables and # time dimension added to the dataset. # SOCA needs icea area (aicen), ice volume (vicen), and snow area (vsnon) @@ -210,6 +230,8 @@ def prepare_cice6_restart(self) -> Tuple[str, str]: 'hi_h': 'vicen', 'hs_h': 'vsnon'} + import xarray as xr + # read CICE6 restart # ----------------- ds = xr.open_dataset(self.forecast_dir(['RESTART', 'iced.nc'])) diff --git a/src/swell/tasks/move_da_restart.py b/src/swell/tasks/move_da_restart.py index deec7de29..8d1ff4414 100644 --- a/src/swell/tasks/move_da_restart.py +++ b/src/swell/tasks/move_da_restart.py @@ -13,10 +13,29 @@ import re from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.file_system_operations import move_files # -------------------------------------------------------------------------------------------------- +task_name = 'MoveDaRestart' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.mom6_iau(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- + class MoveDaRestart(taskBase): diff --git a/src/swell/tasks/move_forecast_restart.py b/src/swell/tasks/move_forecast_restart.py index e82f62565..0425f4021 100644 --- a/src/swell/tasks/move_forecast_restart.py +++ b/src/swell/tasks/move_forecast_restart.py @@ -11,10 +11,27 @@ import glob from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.file_system_operations import move_files # -------------------------------------------------------------------------------------------------- +task_name = 'MoveForecastRestart' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.questions = [ + qd.forecast_duration() + ] + +# -------------------------------------------------------------------------------------------------- + class MoveForecastRestart(taskBase): diff --git a/src/swell/tasks/prep_coupled_geos_run_dir.py b/src/swell/tasks/prep_coupled_geos_run_dir.py index 84f1bb1c6..50b987b2e 100644 --- a/src/swell/tasks/prep_coupled_geos_run_dir.py +++ b/src/swell/tasks/prep_coupled_geos_run_dir.py @@ -12,10 +12,34 @@ import re from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_attributes import task_attributes +from swell.tasks.base.task_setup import TaskSetup +from swell.configuration import question_defaults as qd from swell.utilities.file_system_operations import copy_to_dst_dir # -------------------------------------------------------------------------------------------------- +task_name = 'PrepCoupledGeosRunDir' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.questions = [ + qd.swell_static_files(), + qd.swell_static_files_user(), + qd.geos_homdir(), + qd.geos_expdir_different(), + qd.geos_expdir(), + qd.existing_geos_gcm_build_path(), + qd.forecast_duration(), + qd.mom6_iau_nhours() + ] + +# -------------------------------------------------------------------------------------------------- + class PrepCoupledGeosRunDir(taskBase): diff --git a/src/swell/tasks/prep_forecast_cf.py b/src/swell/tasks/prep_forecast_cf.py index 0b10766e2..39873395a 100644 --- a/src/swell/tasks/prep_forecast_cf.py +++ b/src/swell/tasks/prep_forecast_cf.py @@ -16,10 +16,41 @@ from swell.configuration.jedi.interfaces.geos_cf.model.r2d2 import forecast_history from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + from swell.utilities.shell_commands import run_subprocess # -------------------------------------------------------------------------------------------------- +task_name = 'PrepForecastCf' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.analysis_variables(), + qd.forecast_length(), + qd.forecast_output_frequency(), + qd.geos_cf_install_dir(), + qd.geos_cf_run_dir(), + qd.geosfp_exp(), + qd.geosfp_path(), + qd.horizontal_resolution(), + qd.iau(), + qd.inc_template(), + qd.window_length(), + qd.rst_experiment() + ] + + +# -------------------------------------------------------------------------------------------------- class PrepForecastCf(taskBase): diff --git a/src/swell/tasks/prepare_analysis.py b/src/swell/tasks/prepare_analysis.py index 50738c963..96540451f 100644 --- a/src/swell/tasks/prepare_analysis.py +++ b/src/swell/tasks/prepare_analysis.py @@ -12,10 +12,30 @@ import netCDF4 as nc import os import shutil -from typing import Union from swell.utilities.shell_commands import run_subprocess from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'PrepareAnalysis' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.analysis_variables(), + qd.mom6_iau(), + qd.total_processors(), + qd.window_length(), + ] # -------------------------------------------------------------------------------------------------- @@ -100,7 +120,7 @@ def execute(self) -> None: # ---------------------------------------------------------------------------------------- - def at_cycledir(self, paths: Union[list, str] = []) -> str: + def at_cycledir(self, paths: list | str = []) -> str: """ Get the absolute path to the model cycle directory for the given relative paths. diff --git a/src/swell/tasks/publish_comparisons.py b/src/swell/tasks/publish_comparisons.py index 553066c7d..94a5c843c 100644 --- a/src/swell/tasks/publish_comparisons.py +++ b/src/swell/tasks/publish_comparisons.py @@ -12,6 +12,18 @@ from pathlib import Path from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes + +# -------------------------------------------------------------------------------------------------- + +task_name = 'PublishComparisons' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/render_jedi_observations.py b/src/swell/tasks/render_jedi_observations.py index a2c43f26a..59f57fd96 100644 --- a/src/swell/tasks/render_jedi_observations.py +++ b/src/swell/tasks/render_jedi_observations.py @@ -12,10 +12,34 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import check_obs # -------------------------------------------------------------------------------------------------- +task_name = 'RenderJediObservations' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.check_for_obs(), + qd.crtm_coeff_dir(), + qd.background_time_offset(), + qd.observing_system_records_path(), + qd.observations(), + qd.window_length(), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RenderJediObservations(taskBase): diff --git a/src/swell/tasks/run_jedi_convert_state_soca2cice_executable.py b/src/swell/tasks/run_jedi_convert_state_soca2cice_executable.py index 297f0bfb3..016f8e957 100644 --- a/src/swell/tasks/run_jedi_convert_state_soca2cice_executable.py +++ b/src/swell/tasks/run_jedi_convert_state_soca2cice_executable.py @@ -12,15 +12,42 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediConvertStateSoca2ciceExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {'nodes': 1} + self.questions = [ + qd.analysis_variables(), + qd.generate_yaml_and_exit(), + qd.jedi_forecast_model(), + qd.marine_models(), + qd.observations(), + qd.total_processors(), + qd.window_length(), + qd.window_type(), + qd.comparison_log_type('convert_state_soca2cice'), + qd.mock_experiment() + ] -class RunJediConvertStateSoca2ciceExecutable(taskBase): +# -------------------------------------------------------------------------------------------------- - # ---------------------------------------------------------------------------------------------- + +class RunJediConvertStateSoca2ciceExecutable(taskBase): def execute(self) -> None: diff --git a/src/swell/tasks/run_jedi_ensemble_mean_variance.py b/src/swell/tasks/run_jedi_ensemble_mean_variance.py index 7d98c327c..8479bb5f6 100644 --- a/src/swell/tasks/run_jedi_ensemble_mean_variance.py +++ b/src/swell/tasks/run_jedi_ensemble_mean_variance.py @@ -12,11 +12,46 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediEnsembleMeanVariance' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.analysis_variables(), + qd.ensemble_num_members(), + qd.generate_yaml_and_exit(), + qd.jedi_forecast_model(), + qd.observations(), + qd.observing_system_records_path(), + qd.comparison_log_type('ensmeanvariance'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediEnsembleMeanVariance(taskBase): diff --git a/src/swell/tasks/run_jedi_fgat_executable.py b/src/swell/tasks/run_jedi_fgat_executable.py index 0c2eaed80..d16f8cef3 100644 --- a/src/swell/tasks/run_jedi_fgat_executable.py +++ b/src/swell/tasks/run_jedi_fgat_executable.py @@ -11,11 +11,56 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediFgatExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.analysis_variables(), + qd.background_frequency(), + qd.generate_yaml_and_exit(), + qd.gradient_norm_reduction(), + qd.gsibec_configuration(), + qd.jedi_forecast_model(), + qd.minimizer(), + qd.gsibec_nlats(), + qd.gsibec_nlons(), + qd.number_of_iterations(), + qd.total_processors(), + qd.marine_models(), + qd.comparison_log_type('fgat'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediFgatExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_hofx_ensemble_executable.py b/src/swell/tasks/run_jedi_hofx_ensemble_executable.py index 2a5be2c7a..cb4136974 100644 --- a/src/swell/tasks/run_jedi_hofx_ensemble_executable.py +++ b/src/swell/tasks/run_jedi_hofx_ensemble_executable.py @@ -12,11 +12,51 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable from swell.tasks.run_jedi_hofx_executable import RunJediHofxExecutable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediHofxEnsembleExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.background_frequency(), + qd.ensemble_hofx_packets(), + qd.ensemble_hofx_strategy(), + qd.ensemble_num_members(), + qd.generate_yaml_and_exit(), + qd.jedi_forecast_model(), + qd.total_processors(), + qd.comparison_log_type('hofx'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediHofxEnsembleExecutable(RunJediHofxExecutable, taskBase): diff --git a/src/swell/tasks/run_jedi_hofx_executable.py b/src/swell/tasks/run_jedi_hofx_executable.py index 46e039811..4010b8b77 100644 --- a/src/swell/tasks/run_jedi_hofx_executable.py +++ b/src/swell/tasks/run_jedi_hofx_executable.py @@ -11,26 +11,64 @@ import glob import os from ruamel.yaml import YAML -from typing import Optional from swell.tasks.base.task_base import taskBase -from swell.utilities.netcdf_files import combine_files_without_groups +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediHofxExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.background_frequency(), + qd.generate_yaml_and_exit(), + qd.jedi_forecast_model(), + qd.save_geovals(), + qd.total_processors(), + qd.comparison_log_type('ensemblehofx'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediHofxExecutable(taskBase): # ---------------------------------------------------------------------------------------------- - def execute(self, ensemble_members: Optional[list] = None) -> None: + def execute(self, ensemble_members: list | None = None) -> None: # Jedi application name # --------------------- jedi_application = 'hofx' + from swell.utilities.netcdf_files import combine_files_without_groups + # Parse configuration # ------------------- window_type = self.config.window_type() @@ -213,7 +251,6 @@ def execute(self, ensemble_members: Optional[list] = None) -> None: jedi_config_dict = \ self.jedi_rendering.render_oops_file(f'{jedi_application}{window_type}', window_type, - observations, jedi_forecast_model) # Continue with the yaml edits below some of which need to be @@ -260,7 +297,7 @@ def append_gomsaver( observations: list, jedi_config_dict: dict, window_begin: str, - mem: Optional[str] = None + mem: str | None = None ) -> None: # We may need to save the GeoVaLs for ensemble members. This will diff --git a/src/swell/tasks/run_jedi_local_ensemble_da_executable.py b/src/swell/tasks/run_jedi_local_ensemble_da_executable.py index 30bc0db45..e73b770b1 100644 --- a/src/swell/tasks/run_jedi_local_ensemble_da_executable.py +++ b/src/swell/tasks/run_jedi_local_ensemble_da_executable.py @@ -13,6 +13,9 @@ from swell.swell_path import get_swell_path from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- @@ -33,6 +36,67 @@ def replace_key(obj, old_key, new_key): else: return obj +# -------------------------------------------------------------------------------------------------- + + +task_name = 'RunJediLocalEnsembleDaExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.ensemble_hofx_packets(), + qd.ensemble_hofx_strategy(), + qd.ensemble_num_members(), + qd.ensmean_only(), + qd.ensmeanvariance_only(), + qd.generate_yaml_and_exit(), + qd.horizontal_localization_lengthscale(), + qd.horizontal_localization_max_nobs(), + qd.horizontal_localization_method(), + qd.jedi_forecast_model(), + qd.local_ensemble_inflation_mult(), + qd.local_ensemble_inflation_rtpp(), + qd.local_ensemble_inflation_rtps(), + qd.local_ensemble_save_posterior_ensemble(), + qd.local_ensemble_save_posterior_ensemble_increments(), + qd.local_ensemble_save_posterior_mean(), + qd.local_ensemble_save_posterior_mean_increment(), + qd.local_ensemble_solver(), + qd.local_ensemble_use_linear_observer(), + qd.skip_ensemble_hofx(), + qd.total_processors(), + qd.vertical_localization_apply_log_transform(), + qd.vertical_localization_function(), + qd.vertical_localization_ioda_vertical_coord(), + qd.vertical_localization_ioda_vertical_coord_group(), + qd.vertical_localization_lengthscale(), + qd.vertical_localization_method(), + qd.perhost(), + qd.comparison_log_type('localensembleda'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediLocalEnsembleDaExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_obsfilters_executable.py b/src/swell/tasks/run_jedi_obsfilters_executable.py index a76f934ce..10143ca4d 100644 --- a/src/swell/tasks/run_jedi_obsfilters_executable.py +++ b/src/swell/tasks/run_jedi_obsfilters_executable.py @@ -10,19 +10,59 @@ import os import shutil from ruamel.yaml import YAML -from typing import Optional import random from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediObsfiltersExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.script = ("swell task RunJediObsfiltersExecutable $config" + " -d $datetime -m geos_atmosphere") + self.is_cycling = True + self.model_dep = True + self.task_time_limit = True + self.slurm = {} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.background_frequency(), + qd.generate_yaml_and_exit(), + qd.jedi_forecast_model(), + qd.observing_system_records_path(), + qd.total_processors(), + qd.obs_thinning_rej_fraction(), + qd.comparison_log_type('obsfilters'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediObsfiltersExecutable(taskBase): # ---------------------------------------------------------------------------------------------- - def execute(self, ensemble_members: Optional[list] = None) -> None: + def execute(self, ensemble_members: list | None = None) -> None: # Jedi application name # --------------------- diff --git a/src/swell/tasks/run_jedi_ufo_tests_executable.py b/src/swell/tasks/run_jedi_ufo_tests_executable.py index 308d5c8ee..164d12fc2 100644 --- a/src/swell/tasks/run_jedi_ufo_tests_executable.py +++ b/src/swell/tasks/run_jedi_ufo_tests_executable.py @@ -13,12 +13,40 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.dictionary import update_dict from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediUfoTestsExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {'ntasks-per-node': 1} + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.generate_yaml_and_exit(), + qd.single_observations(), + qd.window_length(), + qd.comparison_log_type('ufo_tests'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediUfoTestsExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_variational_executable.py b/src/swell/tasks/run_jedi_variational_executable.py index bb09b9cc1..74dbfe379 100644 --- a/src/swell/tasks/run_jedi_variational_executable.py +++ b/src/swell/tasks/run_jedi_variational_executable.py @@ -11,11 +11,56 @@ from ruamel.yaml import YAML from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.run_jedi_executables import run_executable # -------------------------------------------------------------------------------------------------- +task_name = 'RunJediVariationalExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.task_time_limit = True + self.is_cycling = True + self.model_dep = True + self.slurm = {'nodes': 3} + self.questions = [ + qd.npx_proc(), + qd.npy_proc(), + qd.npx(), + qd.npy(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.analysis_variables(), + qd.background_frequency(), + qd.generate_yaml_and_exit(), + qd.gradient_norm_reduction(), + qd.gsibec_configuration(), + qd.jedi_forecast_model(), + qd.minimizer(), + qd.gsibec_nlats(), + qd.gsibec_nlons(), + qd.number_of_iterations(), + qd.total_processors(), + qd.perhost(), + qd.comparison_log_type('variational'), + qd.mock_experiment() + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediVariationalExecutable(taskBase): diff --git a/src/swell/tasks/save_forecast_cf.py b/src/swell/tasks/save_forecast_cf.py index 9ec2f7b83..62bdbee46 100644 --- a/src/swell/tasks/save_forecast_cf.py +++ b/src/swell/tasks/save_forecast_cf.py @@ -10,16 +10,35 @@ import isodate import os -from r2d2 import store - +import swell.configuration.question_defaults as qd from swell.configuration.jedi.interfaces.geos_cf.model.r2d2 import forecast_filename, r2d2 from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +from swell.utilities.r2d2_utils import load_r2d2_credentials # -------------------------------------------------------------------------------------------------- +task_name = 'SaveForecastCf' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.forecast_length(), + qd.forecast_output_frequency(), + qd.horizontal_resolution(), + qd.window_length(), + ] + +# -------------------------------------------------------------------------------------------------- + class SaveForecastCf(taskBase): @@ -33,6 +52,8 @@ def execute(self) -> None: See the taskBase constructor for more information. """ + from r2d2 import store + # Load R2D2 credentials # --------------------- load_r2d2_credentials(self.logger, self.platform()) diff --git a/src/swell/tasks/save_obs_diags.py b/src/swell/tasks/save_obs_diags.py index eacf9ba58..2a83e6099 100644 --- a/src/swell/tasks/save_obs_diags.py +++ b/src/swell/tasks/save_obs_diags.py @@ -7,13 +7,36 @@ # -------------------------------------------------------------------------------------------------- -import r2d2 from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd + +from swell.utilities.r2d2_utils import load_r2d2_credentials from swell.utilities.run_jedi_executables import check_obs # -------------------------------------------------------------------------------------------------- +task_name = 'SaveObsDiags' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.background_time_offset(), + qd.crtm_coeff_dir(), + qd.observations(), + qd.observing_system_records_path(), + qd.window_length(), + qd.marine_models() + ] + +# -------------------------------------------------------------------------------------------------- + class SaveObsDiags(taskBase): @@ -23,6 +46,9 @@ class SaveObsDiags(taskBase): def execute(self) -> None: + # Local import because module is not loaded until experiment launch + import r2d2 + # Load R2D2 credentials # --------------------- load_r2d2_credentials( diff --git a/src/swell/tasks/save_restart.py b/src/swell/tasks/save_restart.py index 28f5b7254..b0e22e4ac 100644 --- a/src/swell/tasks/save_restart.py +++ b/src/swell/tasks/save_restart.py @@ -8,6 +8,21 @@ # -------------------------------------------------------------------------------------------------- from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +# import swell.configuration.question_defaults as qd + +# -------------------------------------------------------------------------------------------------- + +task_name = 'SaveRestart' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/save_restart_cf.py b/src/swell/tasks/save_restart_cf.py index 9ee252f0e..f5f5d36a5 100644 --- a/src/swell/tasks/save_restart_cf.py +++ b/src/swell/tasks/save_restart_cf.py @@ -9,10 +9,30 @@ import isodate import os -from r2d2 import store +import swell.configuration.question_defaults as qd from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import load_r2d2_credentials +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +from swell.utilities.r2d2_utils import load_r2d2_credentials + +# -------------------------------------------------------------------------------------------------- + +task_name = 'SaveRestartCf' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + self.model_dep = True + self.questions = [ + qd.window_length(), + qd.horizontal_resolution(), + qd.rst_file_types(), + qd.rst_store_interval(), + ] # -------------------------------------------------------------------------------------------------- @@ -27,6 +47,8 @@ def execute(self): can be retrieved by GetRestartCf in the subsequent cycle. """ + from r2d2 import store + model = self.__model__ # Load R2D2 credentials diff --git a/src/swell/tasks/stage_jedi.py b/src/swell/tasks/stage_jedi.py index 06847a72e..4725f12e0 100644 --- a/src/swell/tasks/stage_jedi.py +++ b/src/swell/tasks/stage_jedi.py @@ -12,6 +12,9 @@ from swell.swell_path import get_swell_path from swell.tasks.base.task_base import taskBase +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd from swell.utilities.filehandler import get_file_handler from swell.utilities.exceptions import SwellError from swell.utilities.file_system_operations import check_if_files_exist_in_path @@ -19,6 +22,38 @@ # -------------------------------------------------------------------------------------------------- +task_name = 'StageJedi' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.model_dep = True + self.questions = [ + qd.swell_static_files(), + qd.swell_static_files_user(), + qd.npx_proc(), + qd.npy_proc(), + qd.gsibec_configuration(), + qd.gsibec_nlats(), + qd.gsibec_nlons(), + qd.horizontal_resolution(), + qd.vertical_resolution(), + ] + + +@task_attributes.register('StageJediCycle') +class StageJediCycle(Setup): + def set_defaults(self): + super().set_defaults() + self.base_name = "StageJedi" + self.scheduling_name = "StageJediCycle-{model}" + self.is_cycling = True + self.model_dep = True + +# -------------------------------------------------------------------------------------------------- + class StageJedi(taskBase): diff --git a/src/swell/tasks/task_questions.py b/src/swell/tasks/task_questions.py deleted file mode 100644 index 640509295..000000000 --- a/src/swell/tasks/task_questions.py +++ /dev/null @@ -1,877 +0,0 @@ -# -------------------------------------------------------------------------------------------------- -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - - -from enum import Enum - -from swell.utilities.swell_questions import QuestionList, QuestionContainer -from swell.utilities.question_defaults import QuestionDefaults as qd - - -# -------------------------------------------------------------------------------------------------- - -class TaskQuestions(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - # Helper question lists used by multiple tasks (in order of use) - # -------------------------------------------------------------------------------------------------- - - background_crtm_obs = QuestionList( - list_name="background_crtm_obs", - questions=[ - qd.background_time_offset(), - qd.crtm_coeff_dir(), - qd.observations(), - qd.observing_system_records_path() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - np_proc_resolution = QuestionList( - list_name="np_resolution", - questions=[ - qd.npx_proc(), - qd.npy_proc(), - qd.npx(), - qd.npy(), - qd.horizontal_resolution(), - qd.vertical_resolution() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - window_questions = QuestionList( - list_name="window_questions", - questions=[ - qd.window_length(), - qd.window_type() - ] - ) - # -------------------------------------------------------------------------------------------------- - - geos_gcm_questions = QuestionList( - list_name="geos_gcm_questions", - questions=[ - qd.geos_homdir(), - qd.geos_expdir_different(), - qd.geos_expdir(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - run_jedi_executable = QuestionList( - list_name="run_jedi_executable", - questions=[ - background_crtm_obs, - np_proc_resolution, - window_questions, - qd.analysis_variables(), - qd.background_frequency(), - qd.generate_yaml_and_exit(), - qd.gradient_norm_reduction(), - qd.gsibec_configuration(), - qd.jedi_forecast_model(), - qd.minimizer(), - qd.gsibec_nlats(), - qd.gsibec_nlons(), - qd.number_of_iterations(), - qd.total_processors(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - swell_static_file_questions = QuestionList( - list_name="swell_static_file_questions", - questions=[ - qd.swell_static_files(), - qd.swell_static_files_user() - ] - ) - - # -------------------------------------------------------------------------------------------------- - # Task-specific question lists (in alphabetical order) - # -------------------------------------------------------------------------------------------------- - - BuildGeos = QuestionList( - list_name="BuildGeos", - questions=[ - qd.geos_build_method() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - BuildGeosByLinking = QuestionList( - list_name="BuildGeosByLinking", - questions=[ - qd.existing_geos_gcm_build_path(), - qd.geos_build_method() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - BuildJedi = QuestionList( - list_name="BuildJedi", - questions=[ - qd.bundles(), - qd.jedi_build_method() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - BuildJediByLinking = QuestionList( - list_name="BuildJediByLinking", - questions=[ - qd.existing_jedi_build_directory(), - qd.existing_jedi_build_directory_pinned(), - qd.jedi_build_method() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - CleanCycle = QuestionList( - list_name="CleanCycle", - questions=[ - qd.clean_patterns(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - CloneGeos = QuestionList( - list_name="CloneGeos", - questions=[ - qd.existing_geos_gcm_source_path(), - qd.geos_build_method(), - qd.geos_gcm_tag() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - CloneGeosMksi = QuestionList( - list_name="CloneGeosMksi", - questions=[ - qd.observing_system_records_mksi_path(), - qd.observing_system_records_mksi_path_tag() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - CloneGmaoPerllib = QuestionList( - list_name="CloneGmaoPerllib", - questions=[ - qd.existing_perllib_path(), - qd.gmao_perllib_tag() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - CloneJedi = QuestionList( - list_name="CloneJedi", - questions=[ - qd.bundles(), - qd.existing_jedi_source_directory(), - qd.existing_jedi_source_directory_pinned(), - qd.jedi_build_method() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - ConvertObsToIoda = QuestionList( - list_name="ConvertObsToIoda", - questions=[ - qd.converter_path(), - qd.dry_run(), - qd.obs_to_download(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - DownloadObs = QuestionList( - list_name="DownloadObs", - questions=[ - qd.dry_run(), - qd.obs_to_download(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - EvaComparisonJediLog = QuestionList( - list_name="EvaJediLog", - questions=[ - qd.comparison_log_type() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - EvaComparisonObservations = QuestionList( - list_name="EvaComparisonObservations", - questions=[ - qd.comparison_log_type(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - EvaIncrement = QuestionList( - list_name="EvaIncrement", - questions=[ - qd.marine_models(), - qd.window_length(), - qd.window_type() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - EvaObservations = QuestionList( - list_name="EvaObservations", - questions=[ - background_crtm_obs, - qd.marine_models(), - qd.observing_system_records_path(), - qd.window_length(), - qd.marine_models(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - EvaTimeseries = QuestionList( - list_name="EvaTimeseries", - questions=[ - background_crtm_obs, - qd.window_length(), - qd.ncdiag_experiments(), - qd.marine_models(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GenerateBClimatology = QuestionList( - list_name="GenerateBClimatology", - questions=[ - np_proc_resolution, - swell_static_file_questions, - qd.analysis_variables(), - qd.background_error_model(), - qd.generate_yaml_and_exit(), - qd.gradient_norm_reduction(), - qd.gsibec_configuration(), - qd.gsibec_nlats(), - qd.gsibec_nlons(), - qd.jedi_forecast_model(), - qd.marine_models(), - qd.minimizer(), - qd.number_of_iterations(), - qd.observing_system_records_path(), - qd.total_processors(), - qd.window_length(), - qd.window_type() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GenerateBClimatologyByLinking = QuestionList( - list_name="GenerateBClimatologyByLinking", - questions=[ - swell_static_file_questions, - qd.background_error_model(), - qd.horizontal_resolution(), - qd.vertical_resolution(), - qd.window_type() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GenerateObservingSystemRecords = QuestionList( - list_name="GenerateObservingSystemRecords", - questions=[ - qd.observations(), - qd.observing_system_records_mksi_path(), - qd.observing_system_records_path() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetBackground = QuestionList( - list_name="GetBackground", - questions=[ - window_questions, - qd.background_experiment(), - qd.background_frequency(), - qd.horizontal_resolution(), - qd.marine_models(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetRestartCf = QuestionList( - list_name="GetRestartCf", - questions=[ - qd.window_length(), - qd.rst_experiment(), - qd.rst_file_types(), - qd.horizontal_resolution(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetBackgroundGeosExperiment = QuestionList( - list_name="GetBackgroundGeosExperiment", - questions=[ - qd.horizontal_resolution(), - qd.background_experiment(), - qd.background_time_offset(), - qd.geos_x_background_directory() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetBufr = QuestionList( - list_name="GetBufr", - questions=[ - qd.bufr_obs_classes() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetCoupledGeosRestart = QuestionList( - list_name="GetCoupledGeosRestart", - questions=[ - geos_gcm_questions, - qd.initial_restarts_method(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetEnsemble = QuestionList( - list_name="GetEnsemble", - questions=[ - qd.path_to_ensemble() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetEnsembleGeosExperiment = QuestionList( - list_name="GetEnsembleGeosExperiment", - questions=[ - qd.background_experiment(), - qd.background_time_offset(), - qd.geos_x_ensemble_directory() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGeovals = QuestionList( - list_name="GetGeovals", - questions=[ - background_crtm_obs, - qd.geovals_experiment(), - qd.geovals_provider(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGeosAdasBackground = QuestionList( - list_name="GetGeosAdasBackground", - questions=[ - qd.path_to_geos_adas_background() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGsiBc = QuestionList( - list_name="GetGsiBc", - questions=[ - qd.path_to_gsi_bc_coefficients(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGsiNcdiag = QuestionList( - list_name="GetGsiNcdiag", - questions=[ - qd.path_to_gsi_nc_diags() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetNcdiags = QuestionList( - list_name="GetNcdiags", - questions=[ - background_crtm_obs, - qd.ncdiag_experiments(), - qd.marine_models(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetObservations = QuestionList( - list_name="GetObservations", - questions=[ - background_crtm_obs, - qd.cache_fetch(), - qd.cycling_varbc(), - qd.obs_experiment(), - qd.observing_system_records_path(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetObsNotInR2d2 = QuestionList( - list_name="GetExistingObservations", - questions=[ - qd.ioda_locations_not_in_r2d2(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GsiBcToIoda = QuestionList( - list_name="GsiBcToIoda", - questions=[ - background_crtm_obs, - qd.observing_system_records_path(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GsiNcdiagToIoda = QuestionList( - list_name="GsiNcdiagToIoda", - questions=[ - qd.observations(), - qd.produce_geovals(), - qd.single_observations(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - IngestObs = QuestionList( - list_name="IngestObs", - questions=[ - qd.dry_run(), - qd.obs_to_ingest(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - JediLogComparison = QuestionList( - list_name="JediComparisonLog", - questions=[ - qd.comparison_log_type(), - qd.number_of_iterations() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - JediOopsLogParser = QuestionList( - list_name="JediOopsLogParser", - questions=[ - qd.comparison_log_type(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - LinkCoupledGeosOutput = QuestionList( - list_name="LinkCoupledGeosOutput", - questions=[ - window_questions, - qd.background_frequency(), - qd.marine_models() - ] - ) - # -------------------------------------------------------------------------------------------------- - - LinkGeosOutput = QuestionList( - list_name="LinkGeosOutput", - questions=[ - window_questions, - qd.background_frequency(), - qd.marine_models() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - MoveEraseDaRestart = QuestionList( - list_name="MoveEraseDaRestart", - questions=[ - qd.mom6_iau(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - MoveDaRestart = QuestionList( - list_name="MoveDaRestart", - questions=[ - qd.mom6_iau(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - MoveForecastRestart = QuestionList( - list_name="MoveForecastRestart", - questions=[ - qd.forecast_duration() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PublishComparisons = QuestionList( - list_name="PublishComparisons", - questions=[ - qd.model_components(), - qd.publish_directory() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PrepareAnalysis = QuestionList( - list_name="PrepareAnalysis", - questions=[ - qd.analysis_variables(), - qd.mom6_iau(), - qd.total_processors(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PrepForecastCf = QuestionList( - list_name="PrepForecastCf", - questions=[ - qd.analysis_variables(), - qd.forecast_length(), - qd.forecast_output_frequency(), - qd.geos_cf_install_dir(), - qd.geos_cf_run_dir(), - qd.geosfp_exp(), - qd.geosfp_path(), - qd.horizontal_resolution(), - qd.iau(), - qd.inc_template(), - qd.window_length(), - qd.rst_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PrepCoupledGeosRunDir = QuestionList( - list_name="PrepCoupledGeosRunDir", - questions=[ - swell_static_file_questions, - geos_gcm_questions, - qd.existing_geos_gcm_build_path(), - qd.forecast_duration(), - qd.mom6_iau_nhours() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RenderJediObservations = QuestionList( - list_name="RenderJediObservations", - questions=[ - qd.check_for_obs(), - qd.crtm_coeff_dir(), - qd.background_time_offset(), - qd.observing_system_records_path(), - qd.observations(), - qd.window_length(), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediConvertStateSoca2ciceExecutable = QuestionList( - list_name="RunJediConvertStateSoca2ciceExecutable", - questions=[ - qd.analysis_variables(), - qd.generate_yaml_and_exit(), - qd.jedi_forecast_model(), - qd.marine_models(), - qd.observations(), - qd.total_processors(), - qd.window_length(), - qd.window_type(), - qd.comparison_log_type('convert_state_soca2cice'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediEnsembleMeanVariance = QuestionList( - list_name="RunJediEnsembleMeanVariance", - questions=[ - np_proc_resolution, - window_questions, - qd.analysis_variables(), - qd.ensemble_num_members(), - qd.generate_yaml_and_exit(), - qd.jedi_forecast_model(), - qd.observations(), - qd.observing_system_records_path(), - qd.comparison_log_type('ensmeanvariance'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediFgatExecutable = QuestionList( - list_name="RunJediFgatExecutable", - questions=[ - run_jedi_executable, - qd.marine_models(), - qd.comparison_log_type('fgat'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediHofxEnsembleExecutable = QuestionList( - list_name="RunJediHofxEnsembleExecutable", - questions=[ - np_proc_resolution, - window_questions, - background_crtm_obs, - qd.background_frequency(), - qd.ensemble_hofx_packets(), - qd.ensemble_hofx_strategy(), - qd.ensemble_num_members(), - qd.generate_yaml_and_exit(), - qd.jedi_forecast_model(), - qd.total_processors(), - qd.comparison_log_type('hofx'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediHofxExecutable = QuestionList( - list_name="RunJediHofxExecutable", - questions=[ - np_proc_resolution, - window_questions, - background_crtm_obs, - qd.background_frequency(), - qd.generate_yaml_and_exit(), - qd.jedi_forecast_model(), - qd.save_geovals(), - qd.total_processors(), - qd.comparison_log_type('ensemblehofx'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediLocalEnsembleDaExecutable = QuestionList( - list_name="RunJediLocalEnsembleDaExecutable", - questions=[ - np_proc_resolution, - window_questions, - background_crtm_obs, - qd.ensemble_hofx_packets(), - qd.ensemble_hofx_strategy(), - qd.ensemble_num_members(), - qd.ensmean_only(), - qd.ensmeanvariance_only(), - qd.generate_yaml_and_exit(), - qd.horizontal_localization_lengthscale(), - qd.horizontal_localization_max_nobs(), - qd.horizontal_localization_method(), - qd.jedi_forecast_model(), - qd.local_ensemble_inflation_mult(), - qd.local_ensemble_inflation_rtpp(), - qd.local_ensemble_inflation_rtps(), - qd.local_ensemble_save_posterior_ensemble(), - qd.local_ensemble_save_posterior_ensemble_increments(), - qd.local_ensemble_save_posterior_mean(), - qd.local_ensemble_save_posterior_mean_increment(), - qd.local_ensemble_solver(), - qd.local_ensemble_use_linear_observer(), - qd.skip_ensemble_hofx(), - qd.total_processors(), - qd.vertical_localization_apply_log_transform(), - qd.vertical_localization_function(), - qd.vertical_localization_ioda_vertical_coord(), - qd.vertical_localization_ioda_vertical_coord_group(), - qd.vertical_localization_lengthscale(), - qd.vertical_localization_method(), - qd.perhost(), - qd.comparison_log_type('localensembleda'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediObsfiltersExecutable = QuestionList( - list_name="RunJediObsfiltersExecutable", - questions=[ - np_proc_resolution, - window_questions, - background_crtm_obs, - qd.background_frequency(), - qd.generate_yaml_and_exit(), - qd.jedi_forecast_model(), - qd.observing_system_records_path(), - qd.total_processors(), - qd.obs_thinning_rej_fraction(), - qd.comparison_log_type('obsfilters'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediUfoTestsExecutable = QuestionList( - list_name="RunJediUfoTestsExecutable", - questions=[ - background_crtm_obs, - qd.generate_yaml_and_exit(), - qd.single_observations(), - qd.window_length(), - qd.comparison_log_type('ufo_tests'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediVariationalExecutable = QuestionList( - list_name="RunJediVariationalExecutable", - questions=[ - run_jedi_executable, - qd.perhost(), - qd.comparison_log_type('variational'), - qd.mock_experiment() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveObsDiags = QuestionList( - list_name="SaveObsDiags", - questions=[ - background_crtm_obs, - qd.window_length(), - qd.marine_models() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveRestartCf = QuestionList( - list_name="SaveRestartCf", - questions=[ - qd.window_length(), - qd.horizontal_resolution(), - qd.rst_file_types(), - qd.rst_store_interval(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveRestart = QuestionList( - list_name="SaveRestart", - questions=[] - ) - - # -------------------------------------------------------------------------------------------------- - - StageJedi = QuestionList( - list_name="StageJedi", - questions=[ - swell_static_file_questions, - qd.npx_proc(), - qd.npy_proc(), - qd.gsibec_configuration(), - qd.gsibec_nlats(), - qd.gsibec_nlons(), - qd.horizontal_resolution(), - qd.vertical_resolution() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveForecastCf = QuestionList( - list_name="SaveForecastCf", - questions=[ - qd.forecast_length(), - qd.forecast_output_frequency(), - qd.horizontal_resolution(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/code_tests.py b/src/swell/test/code_tests/code_tests.py index d8539004d..87ffdf293 100644 --- a/src/swell/test/code_tests/code_tests.py +++ b/src/swell/test/code_tests/code_tests.py @@ -15,8 +15,8 @@ from swell.test.code_tests.slurm_test import SLURMConfigTest from swell.test.code_tests.test_pinned_versions import PinnedVersionsTest from swell.test.code_tests.unused_variables_test import UnusedVariablesTest -from swell.test.code_tests.question_dictionary_comparison_test import QuestionDictionaryTest from swell.test.code_tests.test_generate_observing_system import GenerateObservingSystemTest +from swell.test.code_tests.question_order_test import QuestionOrderTest from swell.test.code_tests.suite_creation_test import SuiteCreationTest from swell.test.code_tests.jedi_config_test import JEDIConfigTest @@ -42,8 +42,8 @@ def code_tests() -> None: # Load unused variable test test_suite.addTests(unittest.TestLoader().loadTestsFromTestCase(UnusedVariablesTest)) - # Load tests from UnusedVariablesTest - test_suite.addTests(unittest.TestLoader().loadTestsFromTestCase(QuestionDictionaryTest)) + # Load question order test + test_suite.addTests(unittest.TestLoader().loadTestsFromTestCase(QuestionOrderTest)) # Load SLURM tests test_suite.addTests(unittest.TestLoader().loadTestsFromTestCase(SLURMConfigTest)) diff --git a/src/swell/test/code_tests/question_dictionary_comparison_test.py b/src/swell/test/code_tests/question_dictionary_comparison_test.py deleted file mode 100644 index 850f7b0d2..000000000 --- a/src/swell/test/code_tests/question_dictionary_comparison_test.py +++ /dev/null @@ -1,43 +0,0 @@ -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - - -import unittest - -from swell.utilities.scripts.compare_questions import compare_used_and_set_questions - - -# -------------------------------------------------------------------------------------------------- - - -class QuestionDictionaryTest(unittest.TestCase): - - def test_dictionary_comparison(self): - - used_not_set, set_not_used = compare_used_and_set_questions() - - # Throw error if there are any unassigned variables used by the code - if len(used_not_set) > 0: - error_msg = ("Questions which are required by the code are missing from the question " - "configurations:\n\n") - - for suite in used_not_set.keys(): - for task_or_suite in used_not_set[suite]: - questions_str = "" - for q in used_not_set[suite][task_or_suite]: - questions_str += q + '\n' - error_msg += (f"In suite {suite}, the {task_or_suite} question configuration " - f"is missing the required question(s):\n{questions_str}\n") - - assert len(used_not_set) == 0, error_msg - - # TODO: Implement a check for set-but-not-used questions - # This will require a fix/adjustment for some suites - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/question_order_test.py b/src/swell/test/code_tests/question_order_test.py new file mode 100644 index 000000000..66ffee8c3 --- /dev/null +++ b/src/swell/test/code_tests/question_order_test.py @@ -0,0 +1,25 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + + +import unittest + +from swell.utilities.scripts.check_question_order import check_question_order + + +# -------------------------------------------------------------------------------------------------- + + +class QuestionOrderTest(unittest.TestCase): + + def test_question_order(self): + + check_question_order() + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/slurm_test.py b/src/swell/test/code_tests/slurm_test.py index c8d75c1e7..7295a6860 100644 --- a/src/swell/test/code_tests/slurm_test.py +++ b/src/swell/test/code_tests/slurm_test.py @@ -9,8 +9,9 @@ import unittest -from swell.utilities.slurm import prepare_scheduling_dict +from swell.utilities.slurm import prepare_slurm_defaults_and_overrides from swell.utilities.logger import get_logger +from swell.tasks.base.task_attributes import task_attributes from unittest.mock import patch, Mock # -------------------------------------------------------------------------------------------------- @@ -31,7 +32,6 @@ def test_slurm_config(self, platform_mocked: Mock, mock_global_defaults: Mock) - # Nested example experiment_dict = { - "model_components": ["geos_atmosphere", "geos_marine"], "slurm_directives_global": { "account": "x1234", }, @@ -50,30 +50,52 @@ def test_slurm_config(self, platform_mocked: Mock, mock_global_defaults: Mock) - } platform_mocked.return_value = "Linux-5.14.21" - sd_discover_sles15 = prepare_scheduling_dict(logger, experiment_dict, - platform="nccs_discover_sles15") - self.assertEqual(sd_discover_sles15["RunJediVariationalExecutable"]["directives"] - ["all"]["constraint"], "mil") - self.assertEqual(sd_discover_sles15["RunJediVariationalExecutable"]["directives"] - ["all"]["qos"], "dastest") + sd_discover_sles15 = prepare_slurm_defaults_and_overrides(logger, 'nccs_discover_sles15', + experiment_dict) + + run_jedi_var_class = task_attributes.get('RunJediVariationalExecutable') + run_jedi_var_obj = run_jedi_var_class('geos_marine', 'nccs_discover_sles15') + run_jedi_var_slurm = run_jedi_var_obj.generate_task_slurm_dict( + sd_discover_sles15) + + self.assertEqual(run_jedi_var_slurm["constraint"], "mil") + self.assertEqual(run_jedi_var_slurm["qos"], "dastest") + + eva_obs_class = task_attributes.get('EvaObservations') + build_jedi_class = task_attributes.get('BuildJedi') + run_jedi_ufo_class = task_attributes.get('RunJediUfoTestsExecutable') # Platform generic tests for sd in [sd_discover_sles15]: for mc in ["all", "geos_atmosphere", "geos_marine"]: + run_jedi_var_obj = run_jedi_var_class(mc, 'nccs_discover_sles15') + eva_obs_obj = eva_obs_class(mc, 'nccs_discover_sles15') + build_jedi_obj = build_jedi_class(mc, 'nccs_discover_sles15') + run_jedi_ufo_obj = run_jedi_ufo_class(mc, 'nccs_discover_sles15') + + run_jedi_var_dict = run_jedi_var_obj.generate_task_slurm_dict( + sd) + eva_obs_dict = eva_obs_obj.generate_task_slurm_dict( + sd) + build_jedi_dict = build_jedi_obj.generate_task_slurm_dict( + sd) + run_jedi_ufo_dict = run_jedi_ufo_obj.generate_task_slurm_dict( + sd) + # Hard-coded task-specific defaults - self.assertEqual(sd["RunJediVariationalExecutable"]["directives"][mc]["nodes"], 3) - self.assertEqual(sd["RunJediUfoTestsExecutable"]["directives"][mc] - ["ntasks-per-node"], 1) + self.assertEqual(run_jedi_var_dict["nodes"], 3) + self.assertEqual(run_jedi_ufo_dict["ntasks-per-node"], 1) # Global defaults from experiment dict - self.assertEqual(sd["BuildJedi"]["directives"][mc]["account"], "x1234") - self.assertEqual(sd["RunJediUfoTestsExecutable"]["directives"][mc]["account"], - "x1234") + self.assertEqual(build_jedi_dict["account"], "x1234") + self.assertEqual(run_jedi_ufo_dict["account"], "x1234") # Task-specific, model-generic config - self.assertEqual(sd["EvaObservations"]["directives"][mc]["account"], "x5678") - self.assertEqual(sd["EvaObservations"]["directives"][mc]["ntasks-per-node"], 4) + self.assertEqual(eva_obs_dict["account"], "x5678") + self.assertEqual(eva_obs_dict["ntasks-per-node"], 4) # Task-specific, model-specific configs - self.assertEqual(sd["EvaObservations"]["directives"]["geos_marine"]["nodes"], 2) - self.assertEqual(sd["EvaObservations"]["directives"]["geos_atmosphere"]["nodes"], 4) + if mc == "geos_marine": + self.assertEqual(eva_obs_dict["nodes"], 2) + if mc == "geos_atmosphere": + self.assertEqual(eva_obs_dict["nodes"], 4) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/suite_creation_test.py b/src/swell/test/code_tests/suite_creation_test.py index be11389a2..e04f4ef41 100644 --- a/src/swell/test/code_tests/suite_creation_test.py +++ b/src/swell/test/code_tests/suite_creation_test.py @@ -11,7 +11,7 @@ import unittest import tempfile -from swell.suites.all_suites import get_suites +from swell.suites.base.suite_attributes import workflows from swell.deployment.create_experiment import create_experiment_directory from swell.utilities.logger import get_logger from swell.utilities.test_cache import get_test_cache @@ -29,7 +29,7 @@ def runTest(self) -> None: ''' - suites = get_suites() + suites = workflows.all() self.logger = get_logger('SuiteCreationTest') @@ -61,11 +61,11 @@ def suite_creation_test(self, suite: str) -> None: experiment_yaml_str = f.read() if 'defer_to_model' in experiment_yaml_str: - raise AssertionError(f'Improperly filled template, `defer_to_model`' + raise AssertionError(f'Improperly filled template for {suite}, `defer_to_model`' 'present in experiment yaml') if 'defer_to_platform' in experiment_yaml_str: - raise AssertionError(f'Improperly filled template, `defer_to_platform`' - 'present in experiment.yaml') + raise AssertionError(f'Improperly filled template for {suite}, `defer_to_platform`' + 'present in experiment yaml') # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/unused_variables_test.py b/src/swell/test/code_tests/unused_variables_test.py index 4d348b08e..ebf5daf63 100644 --- a/src/swell/test/code_tests/unused_variables_test.py +++ b/src/swell/test/code_tests/unused_variables_test.py @@ -34,7 +34,9 @@ def test_unused_variables(self) -> None: for root, _, files in os.walk(get_swell_path()): for filename in files: - if filename.endswith('.py'): # Only process Python files + # Only process Python files + # Ignore results from task_runtimes.py + if filename.endswith('.py') and filename not in ['task_runtimes.py']: file_path = os.path.join(root, filename) flake8_output = run_flake8(file_path) diff --git a/src/swell/test/platform_tests/check_hashes_discover.py b/src/swell/test/platform_tests/check_hashes_discover.py index 80a3db081..f48fde20c 100644 --- a/src/swell/test/platform_tests/check_hashes_discover.py +++ b/src/swell/test/platform_tests/check_hashes_discover.py @@ -1,5 +1,5 @@ from swell.utilities.logger import get_logger -import swell.utilities.pinned_versions.check_hashes import check_hashes +from swell.utilities.pinned_versions.check_hashes import check_hashes logger = get_logger("CheckHashesTest") bundle = "/discover/nobackup/projects/gmao/advda/jedi_bundles/current_pinned_jedi_bundle/source/" diff --git a/src/swell/test/suite_tests/suite_tests.py b/src/swell/test/suite_tests/suite_tests.py index 5b171bbe3..64c44d41b 100644 --- a/src/swell/test/suite_tests/suite_tests.py +++ b/src/swell/test/suite_tests/suite_tests.py @@ -1,6 +1,7 @@ import tempfile from ruamel.yaml import YAML import random +import os from pathlib import Path from datetime import datetime @@ -38,20 +39,20 @@ def build_jedi_for_tier2(test_dir: str, experiment_id_root: str, platform: str, if "override" in test_config: override = update_dict(override, test_config['override']) - experiment_dir = test_dir / experiment_id - experiment_dir.mkdir(parents=True, exist_ok=True) - override_yml = experiment_dir / "override.yaml" + experiment_dir = os.path.join(test_dir, experiment_id) + os.makedirs(experiment_dir, exist_ok=True) + override_yml = os.path.join(experiment_dir, "override.yaml") with open(override_yml, "w") as f: yaml.dump(override, f) create_experiment_directory( - "build_jedi", None, "defaults", platform, - str(override_yml), False, None + "build_jedi", "defaults", platform, + str(override_yml), False, None, False ) - suite_path = str(experiment_dir / f"{experiment_id}-suite") - log_path = str(experiment_dir / "log") + suite_path = os.path.join(experiment_dir, f"{experiment_id}-suite") + log_path = os.path.join(experiment_dir, "log") launch_experiment(suite_path, True, log_path) @@ -67,8 +68,9 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): experiment_id = f"{experiment_id_root}{suite}" # Get test directory from `~/.swell/swell-test.yaml` + testdir = Path(tempfile.TemporaryDirectory().name).expanduser() test_config = { - "test_root": Path(tempfile.TemporaryDirectory().name) + "test_root": testdir.name } yaml = YAML(typ='safe') yamlfile = Path("~/.swell/swell-test.yaml").expanduser() @@ -82,11 +84,10 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): except Exception as err: raise err - testdir = Path(test_config["test_root"]).expanduser() - testdir.mkdir(exist_ok=True, parents=True) + testdir.mkdir(exist_ok=True) print(f"Testing suite: {suite}") - print(f"Test directory: {testdir}") + print(f"Test directory: {testdir.name}") print(f"Experiment ID: {experiment_id}") override = { @@ -118,7 +119,8 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): and test_config["jedi_build_method"] == "use_existing" and 'existing_jedi_source_directory' in test_config and 'existing_jedi_build_directory' in test_config): - jedi_dir = build_jedi_for_tier2(testdir, experiment_id_root, platform, test_config) + jedi_dir = build_jedi_for_tier2(str(testdir), experiment_id_root, + platform, test_config) tier2_override = {"jedi_build_method": "use_existing", "existing_jedi_source_directory": f"{jedi_dir}/jedi_bundle/source", @@ -129,7 +131,7 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): if suite == "build_jedi": return None - override_yml = experiment_dir / "override.yaml" + override_yml = os.path.join(experiment_dir, "override.yaml") with open(override_yml, "w") as f: yaml.dump(override, f) @@ -139,7 +141,7 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): create_experiment_directory( suite_config, "defaults", platform, - str(override_yml), False, None + str(override_yml), False, None, True ) # TODO: Check some stuff about the experiment directory @@ -150,20 +152,3 @@ def run_suite(suite: str, platform: str, test_tier: TestSuite): launch_experiment(suite_path, True, log_path) # TODO: Check the outputs - - -if __name__ == "__main__": - from argparse import ArgumentParser - - parser = ArgumentParser(description="Swell suite tests") - parser.add_argument("suites", nargs="+", - help="Suite(s) to run (or `all` to run all suites)") - - args = parser.parse_args() - if args.suites == ["all"]: - suites = ("3dvar_marine", "hofx", "3dvar_atmos") - else: - suites = args.suites - - for suite in suites: - run_suite(suite) diff --git a/src/swell/utilities/build.py b/src/swell/utilities/build.py index f5f2eff16..27a2e81c3 100644 --- a/src/swell/utilities/build.py +++ b/src/swell/utilities/build.py @@ -9,7 +9,6 @@ import os import shutil -from typing import Tuple from swell.utilities.logger import get_logger from jedi_bundle.utils.yaml import load_yaml @@ -21,7 +20,7 @@ # -------------------------------------------------------------------------------------------------- -def build_and_source_dirs(package_path: str) -> Tuple[str, str]: +def build_and_source_dirs(package_path: str) -> tuple[str, str]: # Make package directory # ---------------------- diff --git a/src/swell/utilities/check_da_params.py b/src/swell/utilities/check_da_params.py index 6cbfb8da9..d04fcd589 100644 --- a/src/swell/utilities/check_da_params.py +++ b/src/swell/utilities/check_da_params.py @@ -8,7 +8,6 @@ from ruamel.yaml import YAML import os -from typing import Optional from datetime import datetime from swell.utilities.logger import get_logger @@ -19,9 +18,9 @@ def check_da_params(config_list: list, model_component: str, - start_cycle_point_in: Optional[str], - final_cycle_point_in: Optional[str], - cycle_times_in: Optional[str]) -> None: + start_cycle_point_in: str | None, + final_cycle_point_in: str | None, + cycle_times_in: str | None) -> tuple[list, list, list]: # From two or more experiments, check that the window parameters are the same, and gather the # common cycle times between the two. Returns times between the start and final cycle points, diff --git a/src/swell/utilities/config.py b/src/swell/utilities/config.py index 649e6dd0a..456f66342 100644 --- a/src/swell/utilities/config.py +++ b/src/swell/utilities/config.py @@ -10,9 +10,9 @@ from ruamel.yaml import YAML from typing import Callable -from swell.tasks.task_questions import TaskQuestions as task_questions +from swell.tasks.base.task_attributes import task_attributes from swell.utilities.logger import Logger -from swell.suites.all_suites import AllSuites +from swell.suites.base.suite_attributes import suite_configs # -------------------------------------------------------------------------------------------------- @@ -113,7 +113,7 @@ def __init__(self, input_file: str, logger: Logger, task_name: str, model: str) # ------------------------------------------------------------------------- # Check for suite questions - suite_questions = AllSuites.get_config( + suite_questions = suite_configs.get_config( self.__suite_to_run__).get_all_question_names('suite') question_list = [] @@ -123,8 +123,8 @@ def __init__(self, input_file: str, logger: Logger, task_name: str, model: str) question_list.append(question) # Find the questions associated with the task - if task_name in task_questions.get_all(): - question_list.extend(task_questions[task_name].value.get_all_question_names()) + task_class = getattr(task_attributes, task_name) + question_list.extend(task_class().question_list.get_all_question_names()) # Loop through the experiment dictionary for exp_key, exp_val in experiment_dict.items(): diff --git a/src/swell/utilities/cylc_formatting.py b/src/swell/utilities/cylc_formatting.py new file mode 100644 index 000000000..4a1b9f043 --- /dev/null +++ b/src/swell/utilities/cylc_formatting.py @@ -0,0 +1,142 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +from typing import Union, Optional, Self +from collections.abc import Mapping +import textwrap + +INDENT = ' ' * 4 + +# -------------------------------------------------------------------------------------------------- + + +def format_dict(dictionary: Mapping): + """Convert a dictionary into a string. + + Args: + dictionary (Mapping): The dictionary to format. + + Returns: + str: A string representation of the dictionary, with each key-value pair on a + new line in the format 'key = value'. + + Examples: + >>> format_dict({'a': 1, 'b': "test"}) + 'a = 1\nb = test\n' + + # NOTE: Strings are not quoted + >>> print(format_dict({'a': "1", 'b': "test"})) + a = 1 + b = test + + # NOTE: Nested dictionaries are printed in native dict/JSON format + >>> print(format_dict({'a': "this", 'b': {"b1": 1, "b2": 2}})) + a = this + b = {'b1': 1, 'b2': 2} + + >>> format_dict({}) + '' + """ + + dict_str = '' + + for key, value in dictionary.items(): + dict_str += f'{key} = {value}\n' + + return dict_str + +# -------------------------------------------------------------------------------------------------- + + +def indent_lines(string: str, level: int = 0, reset: bool = False): + """Indent and/or reset string lines by multiple of level + + Arguments: + string: String to indent + level: multiple of indentation + reset: boolean of whether or not to reset string indentation + """ + + if reset: + string = textwrap.dedent(string) + + string = textwrap.indent(string, INDENT*level) + '\n' + + return string + +# -------------------------------------------------------------------------------------------------- + + +class CylcSection(): + ''' + Holds the information contained in a section, including the name and contents, which can be a + string or dictionary. Also tracks child subsections, automatically handling indentation + and syntax at the time when the string is retrieved. + + Attributes: + name: Header name of section + content: String or mapping of cylc section content + subsections: tracking of additional subsections to append to the section content + ''' + + def __init__(self, name: Optional[str] = None, content: Union[str, dict] = '') -> None: + self.name = name + self.content = content + + self.subsections = [] + + def format_section(self, section: Self, level: int = 0) -> str: + # Format a string to match cylc's section syntax + # format the header with the appropriate amount of enclosing brackets and indents + + section_str = '' + + name = section.name + if name is not None: + section_str += textwrap.indent(f'{(level+1)*"["}{name}{"]"*(level+1)}\n', INDENT*level) + else: + level -= 1 + + content = section.content + if isinstance(content, Mapping): + content = format_dict(content) + + section_str += indent_lines(content, level+1) + + return section_str + + def add_subsection(self, subsection: Self) -> None: + """Add subsection to section tracking. + + Arguments: + subsection: CylcSection object to append + """ + self.subsections.append(subsection) + + def get_section_str(self, level: int = 0) -> str: + """Get string of section contents for flow.cylc + + Arguments: + level: int of indent level multiple + + Returns: + String of section content + """ + section_str = self.format_section(self, level) + + for subsection in self.subsections: + section_str += subsection.get_section_str(level+1) + + if level == 0: + section_str += f'# {"-"*98}\n\n' + + return section_str + + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/data_assimilation_window_params.py b/src/swell/utilities/data_assimilation_window_params.py index 454cbefd2..f765c186e 100644 --- a/src/swell/utilities/data_assimilation_window_params.py +++ b/src/swell/utilities/data_assimilation_window_params.py @@ -9,7 +9,6 @@ import datetime import isodate -from typing import Union, Tuple from swell.utilities.datetime_util import datetime_formats from swell.utilities.logger import Logger @@ -43,7 +42,7 @@ def __get_window_offset_dur__(self, window_length: str) -> datetime.datetime: # ---------------------------------------------------------------------------------------------- - def window_offset(self, window_length: str, dto: bool = False) -> str: + def window_offset(self, window_length: str, dto: bool = False) -> str | datetime.datetime: window_offset_dur = self.__get_window_offset_dur__(window_length) if dto: @@ -142,20 +141,27 @@ def local_background_time(self, window_length, window_type, dto=False - ) -> Union[str, Tuple[str, datetime.datetime]]: + ) -> str: local_background_time = self.__get_local_background_time__(window_type, window_length) - # Return datetime object if asked - if dto: - return local_background_time.strftime(datetime_formats['directory_format']), \ - local_background_time - return local_background_time.strftime(datetime_formats['directory_format']) # ---------------------------------------------------------------------------------------------- - def window_begin(self, window_length: str, dto: bool = False) -> Union[str, datetime.datetime]: + def local_background_time_dto(self, + window_length: str, + window_type: str) -> tuple[str, datetime.datetime]: + + local_background_time = self.__get_local_background_time__(window_type, window_length) + + # Return datetime object if asked + return local_background_time.strftime(datetime_formats['directory_format']), \ + local_background_time + + # ---------------------------------------------------------------------------------------------- + + def window_begin(self, window_length: str, dto: bool = False) -> str | datetime.datetime: window_begin_dto = self.__get_window_begin_dto__(window_length) diff --git a/src/swell/utilities/dataclass_utils.py b/src/swell/utilities/dataclass_utils.py index 472e81965..6a81591d9 100644 --- a/src/swell/utilities/dataclass_utils.py +++ b/src/swell/utilities/dataclass_utils.py @@ -6,14 +6,12 @@ # -------------------------------------------------------------------------------------------------- -from typing import Union - from dataclasses import field # -------------------------------------------------------------------------------------------------- -def mutable_field(list_dict: Union[list, dict]): +def mutable_field(list_dict: list | dict): # Need to construct field using lambda because by default dataclass fields # cannot be initialized to mutable types return field(default_factory=lambda: list_dict) diff --git a/src/swell/utilities/dictionary.py b/src/swell/utilities/dictionary.py index 3facefdd6..df6905d68 100644 --- a/src/swell/utilities/dictionary.py +++ b/src/swell/utilities/dictionary.py @@ -7,10 +7,11 @@ # -------------------------------------------------------------------------------------------------- +from collections.abc import Hashable, Mapping import io from ruamel.yaml import YAML from collections.abc import Hashable -from typing import Union +from typing import Any from swell.utilities.logger import Logger @@ -21,7 +22,7 @@ def dict_get( logger: Logger, dictionary: dict, key: str, - default: str = 'NODEFAULT' + default: Any = 'NODEFAULT' ) -> str: if key in dictionary.keys(): @@ -40,7 +41,7 @@ def dict_get( # -------------------------------------------------------------------------------------------------- -def remove_matching_keys(d: Union[dict, list], key: str) -> None: +def remove_matching_keys(d: dict | list, key: str) -> None: """ Recursively locates and removes all dictionary items matching the supplied key. Parameters @@ -181,3 +182,19 @@ def dictionary_override(logger: Logger, orig_dict: dict, override_dict: dict) -> # -------------------------------------------------------------------------------------------------- + +def add_dict(priority_dict: Mapping, additional_dict: Mapping) -> Mapping: + # Return version of dictionary 1 updated with additional keys from dictionary 2 without + # overwriting entries in dictionary 1 + + for key, value in additional_dict.items(): + if key in priority_dict.keys(): + priority_value = priority_dict[key] + if isinstance(value, Mapping) and isinstance(priority_value, Mapping): + priority_dict[key] = add_dict(priority_value, value) + else: + priority_dict[key] = value + + return priority_dict + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/filehandler.py b/src/swell/utilities/filehandler.py index d37796a31..1359e972f 100755 --- a/src/swell/utilities/filehandler.py +++ b/src/swell/utilities/filehandler.py @@ -63,10 +63,10 @@ import copy import datetime as dt from shutil import copyfile -from typing import Union, Optional, Any +from abc import ABC, abstractmethod -def get_file_handler(config: list, **kwargs) -> Union[StageFileHandler, GetDataFileHandler]: +def get_file_handler(config: list, **kwargs) -> StageFileHandler | GetDataFileHandler: """Factory for determining the file handler type for retrieving data. This method uses a heuristic algorithm to determine the staging @@ -104,17 +104,17 @@ def get_file_handler(config: list, **kwargs) -> Union[StageFileHandler, GetDataF # ------------------------------------------------------------------------------ -class FileHandler(object): +class FileHandler(ABC): def __init__(self, config: list, **kwargs) -> None: - self.listing = [] + self.listing: list = [] self.config = copy.deepcopy(config) self.strict = kwargs.get('strict', True) # ------------------------------------------------------------------------------ - def is_ready(self, fc: Optional[FileCollection] = None) -> bool: + def is_ready(self, fc: FileCollection | None = None) -> bool: """Determines if the file collection meets the criteria for readiness (e.g. minimum file count etc.) @@ -156,7 +156,7 @@ def is_ready(self, fc: Optional[FileCollection] = None) -> bool: # ------------------------------------------------------------------------ - def get(self, fc: Optional[FileCollection] = None) -> None: + def get(self, fc: FileCollection | None = None) -> None: """Retrieves the files in the specified file collection. Parameters @@ -225,6 +225,12 @@ def link(self, src: str, dst: str) -> None: if not os.path.isfile(dst): os.symlink(src, dst) +# --------------------------------------------------------------------------- + + @abstractmethod + def list(self, force: bool = False) -> list: + return [] + # --------------------------------------------------------------------------- @@ -341,7 +347,7 @@ def list(self, force: bool = False) -> list: srcfile = args[0] filelist = glob.glob(srcfile) - found = found or filelist + found = found or len(filelist) > 0 for srcfile in filelist: @@ -374,11 +380,11 @@ def list(self, force: bool = False) -> list: class FileCollection(object): - def __init__(self, config: dict[Any, Any]) -> None: + def __init__(self, config: dict) -> None: self.config = copy.deepcopy(config) - self.listing = [] + self.listing: list = [] self.link = config.get('link', False) self.min_count = config.get('min_count', 1) self.min_age = config.get('min_age', 0) diff --git a/src/swell/utilities/geos.py b/src/swell/utilities/geos.py index 129f846e5..b20c1a9a2 100644 --- a/src/swell/utilities/geos.py +++ b/src/swell/utilities/geos.py @@ -12,7 +12,6 @@ import glob import isodate import os -from typing import Tuple, Optional from swell.utilities.datetime_util import datetime_formats from swell.utilities.logger import Logger @@ -32,7 +31,7 @@ class Geos(): def __init__( self, logger: Logger, - forecast_dir: Optional[str], + forecast_dir: str | None, ) -> None: """ Initializes the Geos class. The intention is to share methods between forecast-only @@ -48,12 +47,20 @@ def __init__( # ---------------------------------------------------------------------------------------------- + def get_forecast_dir(self) -> str: + if self.forecast_dir is None: + raise ValueError('Trying to call forecast dir but it has not been set') + + return self.forecast_dir + + # ---------------------------------------------------------------------------------------------- + def iso_to_time_str( self, iso_duration: str, half: bool = False, agcm: bool = False, - ) -> Tuple[str, int, datetime.timedelta]: + ) -> tuple[str, int, datetime.timedelta]: """ Converts an ISO 8601 duration string to various time representations. @@ -109,7 +116,7 @@ def linker( self, src: str, dst: str, - dst_dir: str = None + dst_dir: str | None = None ) -> None: """ Creates a symbolic link from a source file to a destination. @@ -124,7 +131,7 @@ def linker( # Link files from BC directories # ------------------------------ if dst_dir is None: - dst_dir = self.forecast_dir + dst_dir = self.get_forecast_dir() dst = os.path.basename(dst) @@ -325,7 +332,7 @@ def process_nml( """ # Make sure input.nml is set up properly for hot/cold restart - nml_comb = f90nml.read(os.path.join(self.forecast_dir, 'input.nml')) + nml_comb = f90nml.read(os.path.join(self.get_forecast_dir(), 'input.nml')) if not cold_restart: self.logger.info('Hot start, Swell will expect rst/checkpoint files') @@ -336,10 +343,10 @@ def process_nml( if combine_fvcore: self.logger.info('Combining fvcore with input.nml') - nml2 = f90nml.read(os.path.join(self.forecast_dir, 'fvcore_layout.rc')) + nml2 = f90nml.read(os.path.join(self.get_forecast_dir(), 'fvcore_layout.rc')) nml_comb.update(nml2) - with open(os.path.join(self.forecast_dir, 'input.nml'), 'w') as f: + with open(os.path.join(self.get_forecast_dir(), 'input.nml'), 'w') as f: f90nml.write(nml_comb, f, sort=False) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/get_channels.py b/src/swell/utilities/get_channels.py index 7582d0a7d..5b29e9d45 100644 --- a/src/swell/utilities/get_channels.py +++ b/src/swell/utilities/get_channels.py @@ -11,7 +11,6 @@ import os from datetime import datetime as dt from itertools import groupby -from typing import Tuple, Optional from swell.utilities.exceptions import SwellError from swell.utilities.logger import Logger @@ -77,7 +76,7 @@ def get_channels( observation: str, dt_cycle_time: dt, logger: Logger -) -> Tuple[Optional[str], Optional[list[int]]]: +) -> tuple[str | None, list[int] | None]: ''' Comparing available channels and active channels from the observing @@ -123,7 +122,7 @@ def num_active_channels( path_to_observing_sys_yamls: str, observation: str, dt_cycle_time: dt -) -> Optional[int]: +) -> int | None: # Retrieve available and active channels from records yaml path_to_observing_sys_config = path_to_observing_sys_yamls + '/' + \ diff --git a/src/swell/utilities/gsi_record_parser.py b/src/swell/utilities/gsi_record_parser.py index 9efdc6792..4ddbd593b 100644 --- a/src/swell/utilities/gsi_record_parser.py +++ b/src/swell/utilities/gsi_record_parser.py @@ -18,10 +18,10 @@ def check_end_time(end_time: str) -> str: class GSIRecordParser: def __init__(self) -> None: - self.instr_df = None - self.return_df = None - self.sat = None - self.instr = None + self.instr_df = pd.DataFrame() + self.return_df = pd.DataFrame() + self.sat = '' + self.instr = '' def get_channel_list(self, start: int) -> list: channel_list = [] diff --git a/src/swell/utilities/jinja2.py b/src/swell/utilities/jinja2.py index b45c20735..bc0716f5a 100644 --- a/src/swell/utilities/jinja2.py +++ b/src/swell/utilities/jinja2.py @@ -7,7 +7,6 @@ # -------------------------------------------------------------------------------------------------- from __future__ import annotations -from typing import Union import jinja2 as j2 @@ -34,7 +33,7 @@ def __getattr__(self, name: str) -> SilentUndefined: # Return a new SilentUndefined instance but append the attribute access to the name. return SilentUndefined(name=f"{self._undefined_name}.{name}") - def __getitem__(self, key: Union[str, int]) -> SilentUndefined: + def __getitem__(self, key: str | int) -> SilentUndefined: # Similar to __getattr__, return a new instance with the key access incorporated. if isinstance(key, str): return SilentUndefined(name=f"{self._undefined_name}['{key}']") diff --git a/src/swell/utilities/logger.py b/src/swell/utilities/logger.py index ee73d2206..65a2fba03 100644 --- a/src/swell/utilities/logger.py +++ b/src/swell/utilities/logger.py @@ -9,7 +9,6 @@ import os import logging -from typing import Optional # -------------------------------------------------------------------------------------------------- @@ -19,7 +18,7 @@ class Logger(logging.Logger): # -------------------------------------------------------------------------------------------------- def abort(self, msg: str, - exception: Exception = Exception, *args, **kwargs) -> None: + exception: type[Exception] = Exception, *args, **kwargs) -> None: formatted_msg = ' Swell called ABORT: ' + msg @@ -38,7 +37,7 @@ def assert_abort(self, condition: bool, msg: str) -> None: # -------------------------------------------------------------------------------------------------- -def get_logger(name: Optional[str] = None) -> Logger: +def get_logger(name: str | None = None) -> Logger: ''' Get a logger with custom message formatting for swell-related tasks. diff --git a/src/swell/utilities/netcdf_files.py b/src/swell/utilities/netcdf_files.py index 8b26c7a2f..0c82a98ae 100644 --- a/src/swell/utilities/netcdf_files.py +++ b/src/swell/utilities/netcdf_files.py @@ -10,7 +10,7 @@ import os import xarray as xr -from typing import Hashable, Union +from typing import Hashable from swell.utilities.logger import Logger @@ -21,7 +21,7 @@ def combine_files_without_groups( logger: Logger, list_of_input_files: list, output_file: str, - concat_dim: Union[Hashable, xr.Variable, xr.DataArray], + concat_dim: Hashable | xr.Variable | xr.DataArray, delete_input: bool = False ) -> None: diff --git a/src/swell/utilities/observing_system_records.py b/src/swell/utilities/observing_system_records.py index 3370e8b8f..8ba4238d1 100644 --- a/src/swell/utilities/observing_system_records.py +++ b/src/swell/utilities/observing_system_records.py @@ -3,7 +3,6 @@ import pandas as pd import numpy as np import datetime as dt -from typing import Optional from swell.utilities.logger import get_logger from swell.utilities.gsi_record_parser import GSIRecordParser @@ -151,7 +150,7 @@ def parse_records(self, path_to_sat_db: str) -> None: def save_yamls( self, output_dir: str, - observation_list: Optional[list] = None + observation_list: list | None = None ) -> None: ''' diff --git a/src/swell/utilities/plugins.py b/src/swell/utilities/plugins.py new file mode 100644 index 000000000..9088f5e1f --- /dev/null +++ b/src/swell/utilities/plugins.py @@ -0,0 +1,28 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + +# -------------------------------------------------------------------------------------------------- + +import importlib +import pkgutil + +# -------------------------------------------------------------------------------------------------- + + +def discover_plugins(package): + '''Walk through packages to trigger any hooks. + + Parameters: + package: Python package + ''' + for loader, module_name, is_pkg in pkgutil.walk_packages(package.__path__): + full_module_name = f"{package.__name__}.{module_name}" + module = importlib.import_module(full_module_name) + + if is_pkg: + discover_plugins(module) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/question_defaults.py b/src/swell/utilities/question_defaults.py deleted file mode 100644 index ed72fab1f..000000000 --- a/src/swell/utilities/question_defaults.py +++ /dev/null @@ -1,1773 +0,0 @@ -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - - -# -------------------------------------------------------------------------------------------------- - - -from dataclasses import dataclass -from typing import List, Dict - -from swell.utilities.swell_questions import SuiteQuestion, TaskQuestion -from swell.utilities.swell_questions import WidgetType as WType -from swell.utilities.dataclass_utils import mutable_field - - -# -------------------------------------------------------------------------------------------------- - -class QuestionDefaults(): - - # -------------------------------------------------------------------------------------------------- - # Suite question defaults go here - # -------------------------------------------------------------------------------------------------- - - @dataclass - class comparison_experiment_paths(SuiteQuestion): - default_value: list = mutable_field([]) - question_name: str = "comparison_experiment_paths" - ask_question: bool = True - prompt: str = "Provide paths to two experiments to run comparison tests on." - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class cycle_times(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "cycle_times" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Enter the cycle times for this model." - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class cycling_varbc(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "cycling_varbc" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Do you want to use cycling VarBC option?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensemble_hofx_packets(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "ensemble_hofx_packets" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Enter the number of ensemble packets." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensemble_hofx_strategy(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "ensemble_hofx_strategy" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Enter the ensemble hofx strategy." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class experiment_id(SuiteQuestion): - default_value: str = "defer_to_code" - question_name: str = "experiment_id" - ask_question: bool = True - prompt: str = "What is the experiment id?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class experiment_root(SuiteQuestion): - default_value: str = "defer_to_platform" - question_name: str = "experiment_root" - ask_question: bool = True - prompt: str = ("What is the experiment root (the directory where the " - "experiment will be stored)?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class final_cycle_point(SuiteQuestion): - default_value: str = "2023-10-10T06:00:00Z" - question_name: str = "final_cycle_point" - ask_question: bool = True - prompt: str = "What is the time of the final cycle (middle of the window)?" - widget_type: WType = WType.ISO_DATETIME - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class marine_models(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "marine_models" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_marine" - ]) - prompt: str = "Select the active SOCA models for this model." - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class mock_experiment(SuiteQuestion): - default_value: bool = False - question_name: str = "mock_experiment" - ask_question: bool = False - prompt: str = "Dry-run option for comparing configs." - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class model_components(SuiteQuestion): - default_value: str = "defer_to_code" - question_name: str = "model_components" - ask_question: bool = True - options: str = "defer_to_code" - prompt: str = "Enter the model components for this model." - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class parser_options(SuiteQuestion): - default_value: list = mutable_field(['fgrep_residual_norm']) - question_name: str = "parser_options" - ask_question: bool = True - options: list = mutable_field(['fgrep_residual_norm']) - prompt: str = "List the test types to run on the JEDI oops log." - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class r2d2_experiment_id(SuiteQuestion): - default_value: str = "defer_to_code" - question_name: str = "r2d2_experiment_id" - prompt: str = "What experiment_id should r2d2 reference for experiment?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class r2d2_server(SuiteQuestion): - default_value: str | None = None - question_name: str = "r2d2_server" - ask_question: bool = False - prompt: str = ( - "Server/profile name in ~/.swell/r2d2_credentials.yaml " - "(e.g. 'gmao_server'). Leave empty if credentials are at the root level." - ) - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class r2d2_datastore(SuiteQuestion): - default_value: str | None = None - question_name: str = "r2d2_datastore" - ask_question: bool = False - prompt: str = ( - "Datastore name passed to R2D2 fetch and store operations " - "(e.g. a Discover directory store or an S3 bucket store). " - "Run scripts/discover_r2d2_datastores.py to list available datastores. " - "Leave empty to let R2D2 pick the highest-priority writable datastore " - "for your compute host." - ) - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class runahead_limit(SuiteQuestion): - default_value: str = "P4" - question_name: str = "runahead_limit" - ask_question: bool = True - prompt: str = ("Set the Cylc runahead limit: the maximum number of cycles " - "that may be active ahead of the current cycle " - "(e.g. P1: up to 1 cycle ahead, P3: up to 3 cycles ahead, default P4).") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class saber_central_block(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "saber_central_block" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which saber central block do you want to use?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class saber_outer_block(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "saber_outer_block" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which saber outer blocks do you want to use?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class skip_ensemble_hofx(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "skip_ensemble_hofx" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Enter if skip ensemble hofx." - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class skip_r2d2(SuiteQuestion): - default_value: bool = False - question_name: str = "skip_r2d2" - prompt: str = "Skip registering and storing results of this experiment in R2D2?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class start_cycle_point(SuiteQuestion): - default_value: str = "2023-10-10T00:00:00Z" - question_name: str = "start_cycle_point" - ask_question: bool = True - prompt: str = "What is the time of the first cycle (middle of the window)?" - widget_type: WType = WType.ISO_DATETIME - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class window_type(SuiteQuestion): - default_value: str = "defer_to_model" - question_name: str = "window_type" - options: List[str] = mutable_field([ - "3D", - "4D" - ]) - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Enter the window type for this model." - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - # Task question defaults go here - # -------------------------------------------------------------------------------------------------- - - @dataclass - class analysis_variables(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "analysis_variables" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What are the analysis variables?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class background_error_model(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "background_error_model" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which background error model do you want to use?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class background_experiment(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "background_experiment" - ask_question: bool = True - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the name of the name of the experiment providing the backgrounds?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class background_frequency(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "background_frequency" - models: List[str] = mutable_field([ - "all_models" - ]) - depends: Dict = mutable_field({ - "window_type": "4D" - }) - prompt: str = "What is the frequency of the background files?" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class background_time_offset(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "background_time_offset" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = ("How long before the middle of the analysis window did" - " the background providing forecast begin?") - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class rst_experiment(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "rst_experiment" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the name of the experiment providing the restart files in R2D2?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class rst_file_types(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "rst_file_types" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What are the restart file types to fetch/store from R2D2?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class rst_store_interval(TaskQuestion): - default_value: str = None - question_name: str = "rst_store_interval" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = ("After how many cycles should restart files be stored as real files " - "(not symlinks)? E.g. 28 means every 28th cycle (and multiples) stores " - "real files. Leave unset to always store as symlinks.") - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class bufr_obs_classes(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "bufr_obs_classes" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What BUFR observation classes will be used?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class bundles(TaskQuestion): - default_value: List[str] = mutable_field([ - "fv3-jedi", - "soca", - "iodaconv", - "ufo" - ]) - question_name: str = "bundles" - ask_question: bool = True - options: List[str] = mutable_field([ - "fv3-jedi", - "soca", - "iodaconv", - "ufo", - "ioda", - "oops", - "saber" - ]) - depends: Dict = mutable_field({ - "jedi_build_method": "create" - }) - prompt: str = "Which JEDI bundles do you wish to build?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class check_for_obs(TaskQuestion): - default_value: bool = True - question_name: str = "check_for_obs" - options: List[bool] = mutable_field([True, False]) - models: List[str] = mutable_field([ - 'all_models' - ]) - prompt: str = "Perform check for observations? Set to false for debugging purposes." - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class clean_patterns(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "clean_patterns" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Provide a list of patterns that you wish to remove from the cycle directory." - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class comparison_log_type(TaskQuestion): - default_value: str = "variational" - question_name: str = "comparison_log_type" - options: List[str] = mutable_field([ - 'variational', - 'fgat', - ]) - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Provide the log naming convention (e.g. 'variational', 'fgat')." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class crtm_coeff_dir(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "crtm_coeff_dir" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the path to the CRTM coefficient files?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensemble_hofx_packets(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "ensemble_hofx_packets" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Enter number of packets in which ensemble observers should be computed." - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensemble_hofx_strategy(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "ensemble_hofx_strategy" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Enter hofx strategy." - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensemble_num_members(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "ensemble_num_members" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "How many members comprise the ensemble?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensmean_only(TaskQuestion): - default_value: bool = False - question_name: str = "ensmean_only" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Calculate ensemble mean only?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ensmeanvariance_only(TaskQuestion): - default_value: bool = False - question_name: str = "ensmeanvariance_only" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Calculate ensemble mean and variance only?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_geos_gcm_build_path(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_geos_gcm_build_path" - ask_question: bool = True - depends: Dict = mutable_field({ - "geos_build_method": "use_existing" - }) - prompt: str = "What is the path to the existing GEOS build directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_geos_gcm_source_path(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_geos_gcm_source_path" - ask_question: bool = True - depends: Dict = mutable_field({ - "geos_build_method": "use_existing" - }) - prompt: str = "What is the path to the existing GEOS source code directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_jedi_build_directory(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_jedi_build_directory" - ask_question: bool = True - depends: Dict = mutable_field({ - "jedi_build_method": "use_existing" - }) - prompt: str = "What is the path to the existing JEDI build directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_jedi_build_directory_pinned(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_jedi_build_directory_pinned" - ask_question: bool = True - depends: Dict = mutable_field({ - "jedi_build_method": "use_pinned_existing" - }) - prompt: str = "What is the path to the existing pinned JEDI build directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_jedi_source_directory(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_jedi_source_directory" - ask_question: bool = True - depends: Dict = mutable_field({ - "jedi_build_method": "use_existing" - }) - prompt: str = "What is the path to the existing JEDI source code directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_jedi_source_directory_pinned(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "existing_jedi_source_directory_pinned" - ask_question: bool = True - depends: Dict = mutable_field({ - "jedi_build_method": "use_pinned_existing" - }) - prompt: str = "What is the path to the existing pinned JEDI source code directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class existing_perllib_path(TaskQuestion): - default_value: str = 'defer_to_platform' - question_name: str = 'existing_perllib_path' - prompt: str = "Provide a path to an existing location for GMAO_perllib." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class gmao_perllib_tag(TaskQuestion): - default_value: str = 'g1.0.1' - question_name: str = 'gmao_perllib_tag' - prompt: str = "Specify the tag at which GMAO_perllib should be cloned." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class forecast_duration(TaskQuestion): - default_value: str = "PT12H" - question_name: str = "forecast_duration" - ask_question: bool = True - prompt: str = "GEOS forecast duration" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class forecast_length(TaskQuestion): - default_value: str = "PT12H" - question_name: str = "forecast_length" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "Duration of the GEOS-CF forecast (ISO 8601 duration, e.g. PT12H)" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class forecast_output_frequency(TaskQuestion): - default_value: str = "PT1H" - question_name: str = "forecast_output_frequency" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "Frequency of forecast output files (ISO 8601 duration, e.g. PT1H)" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class generate_yaml_and_exit(TaskQuestion): - default_value: bool = False - question_name: str = "generate_yaml_and_exit" - prompt: str = "Generate JEDI executable YAML and exit?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_build_method(TaskQuestion): - default_value: str = "create" - question_name: str = "geos_build_method" - ask_question: bool = True - options: List[str] = mutable_field([ - "use_existing", - "create" - ]) - prompt: str = "Do you want to use an existing GEOS build or create a new build?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_homdir(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "geos_homdir" - ask_question: bool = True - prompt: str = ("What is the location for the HOME Directory (HOMDIR in gcm_run and " - "gcm_setup) that contains model settings and RC files?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_expdir_different(TaskQuestion): - default_value: str = False - question_name: str = "geos_expdir_different" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - prompt: str = ("Is your GEOS EXPERIMENT Directory, where restarts and scratch is located, " - "different than your GEOS HOME Directory?") - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_expdir(TaskQuestion): - default_value: str = "/dev/null/" - question_name: str = "geos_expdir" - depends: Dict = mutable_field({ - "geos_expdir_different": True - }) - prompt: str = ("What is the location for the EXPERIMENT Directory (to contain model " - "output and restart files), if it is different than your GEOS HOME " - "Directory?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_cf_install_dir(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "geos_cf_install_dir" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the path to the GEOS-CF install directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_cf_run_dir(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "geos_cf_run_dir" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the path to the GEOS-CF model run directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geosfp_exp(TaskQuestion): - default_value: str = "f5295_fp" - question_name: str = "geosfp_exp" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the GEOS FP experiment ID used for IAU analysis files?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geosfp_path(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "geosfp_path" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the path to the GEOS FP archive?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_gcm_tag(TaskQuestion): - default_value: str = "v11.6.0" - question_name: str = "geos_gcm_tag" - depends: Dict = mutable_field({ - "geos_build_method": "create" - }) - prompt: str = "Which GEOS tag do you wish to clone?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_x_background_directory(TaskQuestion): - default_value: str = "/dev/null/" - question_name: str = "geos_x_background_directory" - ask_question: bool = True - options: List[str] = mutable_field([ - "/dev/null/", - "/discover/nobackup/projects/gmao/dadev/rtodling/archive/Restarts/JEDI/541x" - ]) - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the path to the GEOS X-backgrounds directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geos_x_ensemble_directory(TaskQuestion): - default_value: str = "/dev/null/" - question_name: str = "geos_x_ensemble_directory" - ask_question: bool = True - options: List[str] = mutable_field([ - "/dev/null/", - "/gpfsm/dnb05/projects/p139/rtodling/archive/" - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the path to the GEOS X-backgrounds directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geovals_experiment(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "geovals_experiment" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the name of the R2D2 experiment providing the GeoVaLs?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class geovals_provider(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "geovals_provider" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the name of the R2D2 database providing the GeoVaLs?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class gradient_norm_reduction(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "gradient_norm_reduction" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What value of gradient norm reduction for convergence?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class gsibec_configuration(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "gsibec_configuration" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which GSIBEC climatological or hybrid?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class gsibec_nlats(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "gsibec_nlats" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "How many number of latutides in GSIBEC grid?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class gsibec_nlons(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "gsibec_nlons" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "How many number of longitudes in GSIBEC grid?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class horizontal_localization_lengthscale(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "horizontal_localization_lengthscale" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the length scale for horizontal covariance localization?" - widget_type: WType = WType.FLOAT - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class horizontal_localization_max_nobs(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "horizontal_localization_max_nobs" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ("What is the maximum number of observations to consider" - " for horizontal covariance localization?") - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class horizontal_localization_method(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "horizontal_localization_method" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which localization scheme should be applied in the horizontal?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class horizontal_resolution(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "horizontal_resolution" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the horizontal resolution for the forecast model and backgrounds?" - widget_type: WType = WType.STRING_DROP_LIST - - # ------------------------------------------------------------------------------------------------ - - @dataclass - class dry_run(TaskQuestion): - default_value: bool = True - question_name: str = "dry_run" - ask_question: bool = False - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Dry-run mode: preview what would be ingested before storing to R2D2" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class obs_to_ingest(TaskQuestion): - default_value: list = mutable_field([]) - question_name: str = "obs_to_ingest" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which observations do you want to ingest to R2D2?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class obs_to_download(TaskQuestion): - default_value: list = mutable_field([]) - question_name: str = "obs_to_download" - ask_question: bool = True - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which observations do you want to download from remote servers?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class converter_path(TaskQuestion): - default_value: str = "" - question_name: str = "converter_path" - ask_question: bool = True - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = ("Path to directory containing ioda-converter scripts" - " (leave blank to use jedi_bin)") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class initial_restarts_method(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "initial_restarts_method" - ask_question: bool = True - options: List[str] = mutable_field([ - "geos_expdir", - "r2d2", - "hotstart", - ]) - prompt: str = "How should initial GEOS restarts be obtained?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ioda_locations_not_in_r2d2(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "ioda_locations_not_in_r2d2" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ( - "Provide a path that contains observation files not in r2d2.") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class iau(TaskQuestion): - default_value: bool = True - question_name: str = "iau" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "Use Incremental Analysis Update (IAU) in the GEOS-CF forecast?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class inc_template(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "inc_template" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the path to the GEOS-CF increment template NetCDF file?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class jedi_build_method(TaskQuestion): - default_value: str = "create" - question_name: str = "jedi_build_method" - ask_question: bool = True - options: List[str] = mutable_field([ - "use_existing", - "use_pinned_existing", - "create", - "pinned_create" - ]) - prompt: str = "Do you want to use an existing JEDI build or create a new build?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class jedi_forecast_model(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "jedi_forecast_model" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - depends: Dict = mutable_field({ - "window_type": "4D" - }) - prompt: str = "What forecast model should be used within JEDI for 4D window propagation?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_inflation_mult(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "local_ensemble_inflation_mult" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Specify the multiplicative prior inflation coefficient (0 inf]." - widget_type: WType = WType.FLOAT - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_inflation_rtpp(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "local_ensemble_inflation_rtpp" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Specify the Relaxation To Prior Perturbation (RTPP) coefficient (0 1]." - widget_type: WType = WType.FLOAT - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_inflation_rtps(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "local_ensemble_inflation_rtps" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Specify the Relaxation To Prior Spread (RTPS) coefficient (0 1]." - widget_type: WType = WType.FLOAT - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_save_posterior_ensemble(TaskQuestion): - default_value: bool = False - question_name: str = "local_ensemble_save_posterior_ensemble" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Save the posterior ensemble members?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_save_posterior_ensemble_increments(TaskQuestion): - default_value: bool = False - question_name: str = "local_ensemble_save_posterior_ensemble_increments" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Save the posterior ensemble member increments?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_save_posterior_mean(TaskQuestion): - default_value: bool = False - question_name: str = "local_ensemble_save_posterior_mean" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Save the posterior ensemble mean?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_save_posterior_mean_increment(TaskQuestion): - default_value: bool = True - question_name: str = "local_ensemble_save_posterior_mean_increment" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Save the posterior ensemble mean increment?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_solver(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "local_ensemble_solver" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which local ensemble solver type should be implemented?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class local_ensemble_use_linear_observer(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "local_ensemble_use_linear_observer" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which local ensemble solver type should be implemented?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class minimizer(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "minimizer" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which data assimilation minimizer do you wish to use?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class mom6_iau(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "mom6_iau" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_marine", - ]) - prompt: str = "Do you wish to use IAU for MOM6?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class mom6_iau_nhours(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "mom6_iau_nhours" - options: List[str] = mutable_field([ - 'PT3H', - 'PT12H' - ]) - depends: dict = mutable_field({'mom6_iau': True}) - models: List[str] = mutable_field([ - "geos_marine", - ]) - prompt: str = "What is the IAU length (ODA_INCUPD_NHOURS) for MOM6?" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class ncdiag_experiments(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "ncdiag_experiments" - options: List[str] = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which previously run experiments do you wish to use for the NCdiag?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class npx_proc(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "npx_proc" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere", - "geos_cf" - ]) - prompt: str = "What number of processors do you wish to use in the x-direction?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class npy_proc(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "npy_proc" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere", - "geos_cf" - ]) - prompt: str = "What number of processors do you wish to use in the y-direction?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class npx(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "npx" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the number of grid points in the x-direction on each cube face?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class npy(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "npy" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_cf" - ]) - prompt: str = "What is the number of grid points in the y-direction on each cube face?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class number_of_iterations(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "number_of_iterations" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = ( - "What number of iterations do you wish to use for each outer loop?" - " Provide a list of integers the same length as the number of outer loops.") - widget_type: WType = WType.INTEGER_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class obs_experiment(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "obs_experiment" - ask_question: bool = True - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the database providing the observations?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class obs_thinning_rej_fraction(TaskQuestion): - default_value: float = 0.75 - question_name: str = "obs_thinning_rej_fraction" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the rejection fraction for obs thinning?" - widget_type: WType = WType.FLOAT - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class observations(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "observations" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Which observations do you want to include?" - widget_type: WType = WType.STRING_CHECK_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class observing_system_records_mksi_path(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "observing_system_records_mksi_path" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the path to the GSI formatted observing system records?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class observing_system_records_mksi_path_tag(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "observing_system_records_mksi_path_tag" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the GSI formatted observing system records tag?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class observing_system_records_path(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "observing_system_records_path" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the path to the Swell formatted observing system records?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class path_to_ensemble(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "path_to_ensemble" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere", - "geos_marine" - ]) - prompt: str = "What is the path to where ensemble members are stored?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class path_to_geos_adas_background(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "path_to_geos_adas_background" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ( - "What is the path for the GEOSadas cubed sphere backgrounds?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class path_to_gsi_bc_coefficients(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "path_to_gsi_bc_coefficients" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the location where GSI bias correction files can be found?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class path_to_gsi_nc_diags(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "path_to_gsi_nc_diags" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the path to where the GSI ncdiags are stored?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class perhost(TaskQuestion): - default_value: str = None - question_name: str = "perhost" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the number of processors per host?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class produce_geovals(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "produce_geovals" - ask_question: bool = True - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ("When running the ncdiag to ioda converted do you " - "want to produce GeoVaLs files?") - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class publish_directory(TaskQuestion): - default_value: str = None - question_name: str = "publish_directory" - ask_question: bool = False - prompt: str = "Provide an external directory to publish relevant results to." - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class cache_fetch(TaskQuestion): - default_value: bool = True - question_name: str = "cache_fetch" - options: List[bool] = mutable_field([ - True, - False - ]) - prompt: str = "Use cached observation files if they already exist?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class save_geovals(TaskQuestion): - default_value: bool = False - question_name: str = "save_geovals" - options: List[bool] = mutable_field([ - True, - False - ]) - prompt: str = "When running hofx do you want to output the GeoVaLs?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class single_observations(TaskQuestion): - default_value: bool = False - question_name: str = "single_observations" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Is it a single-observation test?" - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class swell_static_files(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "swell_static_files" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the path to the Swell Static files directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class swell_static_files_user(TaskQuestion): - default_value: str = "None" - question_name: str = "swell_static_files_user" - prompt: str = "What is the path to the user provided Swell Static Files directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class total_processors(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "total_processors" - ask_question: bool = True - models: List[str] = mutable_field([ - "geos_marine", - ]) - prompt: str = "What is the number of processors for JEDI?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_apply_log_transform(TaskQuestion): - default_value: bool = True - question_name: str = "vertical_localization_apply_log_transform" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ("Should a log (base 10) transformation be applied " - "to vertical coordinate when " - "constructing vertical localization?") - widget_type: WType = WType.BOOLEAN - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_function(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_localization_function" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which localization scheme should be applied in the vertical?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_ioda_vertical_coord(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_localization_ioda_vertical_coord" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "Which coordinate should be used in constructing vertical localization?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_ioda_vertical_coord_group(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_localization_ioda_vertical_coord_group" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ("Which vertical coordinate group should be used " - "in constructing vertical localization?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_lengthscale(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_localization_lengthscale" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = "What is the length scale for vertical covariance localization?" - widget_type: WType = WType.INTEGER - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_localization_method(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_localization_method" - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "geos_atmosphere" - ]) - prompt: str = ("What localization scheme should be applied in " - "constructing a vertical localization?") - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class vertical_resolution(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "vertical_resolution" - ask_question: bool = True - options: str = "defer_to_model" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the vertical resolution for the forecast model and background?" - widget_type: WType = WType.STRING_DROP_LIST - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class window_length(TaskQuestion): - default_value: str = "defer_to_model" - question_name: str = "window_length" - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "What is the duration for the data assimilation window?" - widget_type: WType = WType.ISO_DURATION - - # -------------------------------------------------------------------------------------------------- - - @dataclass - class window_type(TaskQuestion): - question_name: str = "window_type" - default_value: str = "defer_to_model" - ask_question: bool = True - options: List[str] = mutable_field([ - "3D", - "4D" - ]) - models: List[str] = mutable_field([ - "all_models" - ]) - prompt: str = "Do you want to use a 3D or 4D (including FGAT) window?" - widget_type: WType = WType.STRING_DROP_LIST - -# -------------------------------------------------------------------------------------------------- - @dataclass - class download_convert_pipeline(SuiteQuestion): - default_value: bool = False - question_name: str = "download_convert_pipeline" - ask_question: bool = False - prompt: str = ("Run the DownloadObs and ConvertObsToIoda tasks?" - "(DownloadObs -> ConvertObsToIoda) -> IngestObs to R2D2") - widget_type: WType = WType.BOOLEAN diff --git a/src/swell/utilities/r2d2.py b/src/swell/utilities/r2d2_utils.py similarity index 100% rename from src/swell/utilities/r2d2.py rename to src/swell/utilities/r2d2_utils.py diff --git a/src/swell/utilities/render_jedi_interface_files.py b/src/swell/utilities/render_jedi_interface_files.py index e303d754a..a3f346b6a 100644 --- a/src/swell/utilities/render_jedi_interface_files.py +++ b/src/swell/utilities/render_jedi_interface_files.py @@ -9,9 +9,9 @@ import os from ruamel.yaml import YAML -from typing import Union, Optional, Any from importlib import import_module from collections.abc import Mapping +from typing import Any from swell.utilities.jinja2 import template_string_jinja2 from swell.utilities.get_channels import get_channels @@ -28,9 +28,9 @@ def __init__( logger: Logger, experiment_root: str, experiment_id: str, - cycle_dir: Optional[str], - cycle_time: Optional[Datetime], - jedi_interface: Optional[str] = None + cycle_dir: str | None, + cycle_time: Datetime | None, + jedi_interface: str | None = None ) -> None: # Keep a copy of the logger @@ -198,8 +198,8 @@ def __open_file_render_to_dict__(self, config_file: str) -> dict[Any, Any]: # Prepare path to oops file and call rendering def render_oops_file(self, config_name: str, - window_type: Optional[str] = None, - jedi_forecast_model: Optional[str] = None) -> dict: + window_type: str | None = None, + jedi_forecast_model: str | None = None) -> dict: # Import the module module = import_module(f'swell.configuration.jedi.oops.{config_name}') @@ -290,7 +290,7 @@ def render_interface_observations(self, config_name: str) -> dict: # Prepare path to interface metadata file and call rendering - def render_interface_meta(self, model_component_in: Union[str, dict, None] = None) -> dict: + def render_interface_meta(self, model_component_in: str | dict | None = None) -> dict: # Optionally open a different model interface model_component = self.jedi_interface diff --git a/src/swell/utilities/run_jedi_executables.py b/src/swell/utilities/run_jedi_executables.py index c4457a9c7..aaec5e995 100644 --- a/src/swell/utilities/run_jedi_executables.py +++ b/src/swell/utilities/run_jedi_executables.py @@ -10,7 +10,7 @@ import os import netCDF4 as nc -from typing import Optional +import datetime from swell.utilities.shell_commands import run_track_log_subprocess from swell.utilities.logger import Logger @@ -19,11 +19,11 @@ def check_obs( - path_to_observing_sys_yamls: Optional[str], + path_to_observing_sys_yamls: str | None, observation: str, obs_dict: dict, - cycle_time: Optional[str], - input_and_output: Optional[bool] = False + cycle_time: str | datetime.datetime | None, + input_and_output: bool | None = False ) -> bool: use_observation = False @@ -69,7 +69,7 @@ def run_executable( jedi_executable_path: str, jedi_config_file: str, output_log: str, - perhost: Optional[int] = None + perhost: int | None = None ) -> None: # Run the JEDI executable diff --git a/src/swell/utilities/scripts/check_question_order.py b/src/swell/utilities/scripts/check_question_order.py new file mode 100644 index 000000000..bcd1f425a --- /dev/null +++ b/src/swell/utilities/scripts/check_question_order.py @@ -0,0 +1,77 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +import os +from typing import Literal + +from swell.swell_path import get_swell_path + +# -------------------------------------------------------------------------------------------------- + + +def check_question_order(): + question_file = os.path.join(get_swell_path(), 'configuration', 'question_defaults.py') + + with open(question_file, 'r') as f: + lines = f.readlines() + + in_task_section = False + + task_questions = [] + suite_questions = [] + + for line in lines: + if 'class ' in line: + if '(SuiteQuestion)' in line: + if in_task_section: + raise Exception('Suite and Task questions are mixed up, please ensure ' + 'that suite questions come first, then task questions.') + + question = line.split('class ')[1].split('(SuiteQuestion)')[0].strip() + suite_questions.append(question) + elif '(TaskQuestion)' in line: + in_task_section = True + question = line.split('class ')[1].split('(TaskQuestion)')[0].strip() + + task_questions.append(question) + + check_order('task', task_questions) + check_order('suite', suite_questions) + +# -------------------------------------------------------------------------------------------------- + + +def check_order(qtype: Literal['task', 'suite'], check_list: list): + in_order = True + + sorted_list = sorted(check_list) + + max_chars = max([len(item) for item in sorted_list]) + 4 + + order_str = 'Order in file: ' + order_str += '{tabs}> Should be:\n'.format(tabs='-'*(max_chars-len(order_str))) + + for i, sorted_item in enumerate(sorted_list): + check_item = check_list[i] + + tab_char = ' ' + if check_item != sorted_item: + in_order = False + tab_char = '-' + + tabs = tab_char*((max_chars-len(check_item))) + + order_str += f'{check_item} {tabs}> {sorted_item}\n' + + if not in_order: + raise Exception(f'\nQuestions in the {qtype} section are not in order, ' + f'please rearrange according to the following:\n\n{order_str}') + + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/scripts/compare_questions.py b/src/swell/utilities/scripts/compare_questions.py deleted file mode 100644 index b04dcc6c8..000000000 --- a/src/swell/utilities/scripts/compare_questions.py +++ /dev/null @@ -1,258 +0,0 @@ -# (C) Copyright 2021- United States Government as represented by the Administrator of the -# National Aeronautics and Space Administration. All Rights Reserved. -# -# This software is licensed under the terms of the Apache Licence Version 2.0 -# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. - -# -------------------------------------------------------------------------------------------------- - - -import os -from typing import Optional, Tuple -import importlib -import re -from enum import StrEnum, auto - -from swell.swell_path import get_swell_path -from swell.utilities.suite_utils import get_suites -from swell.tasks.task_questions import TaskQuestions as tq -from swell.utilities.swell_questions import QuestionList -from swell.utilities.case_switching import camel_case_to_snake_case - - -# -------------------------------------------------------------------------------------------------- - -class CodeDependentQuestions(StrEnum): - """ Questions which are set by swell during experiment creation. """ - EXPERIMENT_ID = auto() - EXPERIMENT_ROOT = auto() - PLATFORM = auto() - - @classmethod - def filter_list(cls, lst: list) -> list: - values = [item.value for item in cls] - return [item for item in lst if item not in values] - -# -------------------------------------------------------------------------------------------------- - - -def read_cylc_lines(suite: str) -> list: - """ Get lines from the suite's flow.cylc file, in list seperated by newline. """ - - suite_file = os.path.join(get_swell_path(), 'suites', suite, 'flow.cylc') - - with open(suite_file, 'r') as f: - lines = f.readlines() - - return lines - -# -------------------------------------------------------------------------------------------------- - - -def get_all_tasks(suite: str) -> list: - """ Parse the suite's flow.cylc file and get all the tasks used by the suite. """ - - lines = read_cylc_lines(suite) - - tasks = [line.split('swell task ')[1].split(' ')[0] for line in lines if 'swell task' in line] - - tasks = sorted(list(set(tasks))) - - return tasks - -# -------------------------------------------------------------------------------------------------- - - -def get_question_names(config: QuestionList, model: Optional[str] = None) -> list: - """ Get a list of question names from a QuestionList object. """ - return [q['question_name'] for q in config.expand_question_list(model)] - -# -------------------------------------------------------------------------------------------------- - - -def questions_in_cylc(suite: str) -> list: - """ Parse the suite's flow.cylc file and get a list of external questions. """ - - cylc_questions = [] - - lines = read_cylc_lines(suite) - - for line in lines: - line = line.strip() - - if '.' in line or 'scheduling' in line or 'key' in line: - None - elif re.search(".* = {{.*}}", line): - cylc_questions.append( - line.split('{{')[1].split('}}')[0].strip()) - elif 'models[' in line: - cylc_questions.append( - line.split('["')[-1].split('"]')[0].strip()) - elif re.search(".*{%.* in .* %}", line) and '(' in line: - cylc_questions.append( - line.split('(')[1].split(')')[0].strip()) - elif re.search(".*{%.* in .* %}", line): - cylc_questions.append( - line.split('in ')[1].split(' %}')[0].strip()) - - cylc_questions = sorted(list(set(cylc_questions))) - return cylc_questions - -# -------------------------------------------------------------------------------------------------- - - -def compare_used_and_set_questions() -> Tuple[dict, dict]: - """ - Finds the questions which are set in the suite/task configuration, - and those that are actually used by the suite. - - This method returns two dictionaries, used_not_set and set_not_used, indexed by suite and task. - - used_not_set[suite]['suite'_or_task] is a list of questions which are used in the code, - but are not specified in the suite config or task_questions.py - - set_not_used[suite]['suite'_or_task] consists of questions defined - in the suite or task config, which are not used in the code. - """ - - suites = get_suites() - - # Dictionary for questions used in the suite, but not specified in configuration - used_not_set = {} - # Dictionary for questions set in the configuration, but not actually used - set_not_used = {} - - # GEOS model components - possible_model_components = os.listdir(os.path.join(get_swell_path(), - 'configuration', 'jedi', 'interfaces')) - - for suite in suites: - # Sub-suite dictionary for questions used in the code - used_by = {} - # Sub-suite dictionary for questions set in the configuration - set_for = {} - - # Get the default suite configuration - config_name = ('_' if suite[0].isdigit() else '') + suite - suite_config = getattr(importlib.import_module(f'swell.suites.{suite}.suite_config'), - 'SuiteConfig') - base_config = suite_config[config_name].value - - config_questions = get_question_names(base_config) - - # Get questions which are specified as model-dependent - for model in possible_model_components: - config_questions.extend(get_question_names(base_config, model)) - - config_questions = sorted(list(set(config_questions))) - - # Set suite-defined questions - set_for['suite'] = CodeDependentQuestions.filter_list(config_questions) - # Check for questions used by flow.cylc - used_by['suite'] = CodeDependentQuestions.filter_list(questions_in_cylc(suite)) - - tasks = get_all_tasks(suite) - - for task in tasks: - # Task-specific used and set questions - used_task = [] - set_task = [] - - # Get the set questions for the task - if task in tq.get_all(): - set_task.extend(get_question_names(tq[task].value)) - - for model in possible_model_components: - set_task.extend(get_question_names(tq[task].value, model)) - - # Check the task's code for the questions it uses - task_file = os.path.join(get_swell_path(), 'tasks', - camel_case_to_snake_case(task) + '.py') - - with open(task_file, 'r') as f: - config_lines = [line for line in f.readlines() if 'self.config.' in line] - for line in config_lines: - if 'get_key_for_model' in line: - field = line.split( - 'self.config.get_key_for_model(')[1].split(')')[0].strip() + ')' - if len(field.split(',')) == 1: - field = field.split(',')[0] + '()' - else: - field = field.split(',')[ - 0].strip() + '(' + field.split(',')[-1].strip() + ')' - - field = field.replace('"', '') - field = field.replace("'", '') - else: - field = line.split('self.config.')[1].split(')')[0].strip() + ')' - # Include the parentheses, so we can later assess whether the key is optional - used_task.append(field) - - set_task = sorted(list(set(set_task))) - used_task = sorted(list(set(used_task))) - - # Filter out the questions set by the code - set_task = CodeDependentQuestions.filter_list(set_task) - used_task = CodeDependentQuestions.filter_list(used_task) - - used_by[task] = used_task - set_for[task] = set_task - - used_not_set[suite] = {} - set_not_used[suite] = {} - - # Set the dictionary for used-but-not-set questions - for suite_task, lst in used_by.items(): - used_not_set[suite][suite_task] = [] - - for question in lst: - question_name = question.split('(')[0].strip() - # Include only non-optional calls from the code - if len(question_name.split(')')[0].strip()) == 0 and ( - question_name not in set_for[suite_task] + set_for['suite']): - used_not_set[suite][suite_task].append(question_name) - - # Clear the suite or task key if there are no discrepancies - if len(used_not_set[suite][suite_task]) == 0: - del used_not_set[suite][suite_task] - - # Get a list of all questions used by tasks across the suite, to compare against the - # set suite questions. - all_used_questions = [] - for key in used_by.keys(): - for question in used_by[key]: - if '(' in question: - all_used_questions.append(question.split('(')[0]) - else: - all_used_questions.append(question) - - # Set the dictionary for set-but-not-used questions - for suite_task, lst in set_for.items(): - set_not_used[suite][suite_task] = [] - - for question in lst: - if suite_task == 'suite': - # Suite questions may be used in tasks throughout the suite - if question not in all_used_questions: - set_not_used[suite]['suite'].append(question) - else: - # Include optional questions - used_by_all = [q if '(' not in q else q.split('(')[0] - for q in used_by[suite_task]] - if question not in used_by_all: - set_not_used[suite][suite_task].append(question) - - # Clear the key if there are no discrepancies - if len(set_not_used[suite][suite_task]) == 0: - del set_not_used[suite][suite_task] - - # Clear the suite if there are no discrepancies - if len(set_not_used[suite]) == 0: - del set_not_used[suite] - - if len(used_not_set[suite]) == 0: - del used_not_set[suite] - - return used_not_set, set_not_used - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/scripts/delete_obs_in_range.py b/src/swell/utilities/scripts/delete_obs_in_range.py index d078a037e..195284771 100644 --- a/src/swell/utilities/scripts/delete_obs_in_range.py +++ b/src/swell/utilities/scripts/delete_obs_in_range.py @@ -1,4 +1,4 @@ -import r2d2 +import swell.utilities.r2d2_utils as r2d2_utils from datetime import datetime, timedelta @@ -34,7 +34,7 @@ def deregister_observations_in_range( print(f"[DRY RUN] Would delete: {observation_type} at {window_start}") else: try: - r2d2.delete( + r2d2_utils.delete( item='observation', observation_type=observation_type, provider=provider, diff --git a/src/swell/utilities/scripts/ingest_files.py b/src/swell/utilities/scripts/ingest_files.py index 8c70023cb..17e988a1c 100755 --- a/src/swell/utilities/scripts/ingest_files.py +++ b/src/swell/utilities/scripts/ingest_files.py @@ -15,7 +15,7 @@ RESET = "\033[0m" try: - import r2d2 + import swell.utilities.r2d2_utils as r2d2_utils except ImportError as e: raise ImportError( @@ -74,7 +74,7 @@ def ingest_observation(filename, file_path, parts, dry_run=True): return True try: - r2d2.store( + r2d2_utils.store( item='observation', provider=provider, observation_type=obs_type, @@ -131,7 +131,7 @@ def ingest_background(filename, file_path, parts, dry_run=True): return True try: - r2d2.store( + r2d2_utils.store( item='forecast', model='mom6', # model, experiment='s2s', # Use this for testing @@ -213,7 +213,7 @@ def ingest_bias_correction(filename, file_path, parts, dry_run=True): return True try: - r2d2.store( + r2d2_utils.store( item='bias_correction', source_file=file_path, model=model, diff --git a/src/swell/utilities/scripts/search_ingested.py b/src/swell/utilities/scripts/search_ingested.py index cabcd5843..88de9468d 100644 --- a/src/swell/utilities/scripts/search_ingested.py +++ b/src/swell/utilities/scripts/search_ingested.py @@ -1,8 +1,8 @@ -import r2d2 +import swell.utilities.r2d2_utils as r2d2_utils # providers = ['gdas'] # Search for all observations -results = r2d2.search( +results = r2d2_utils.search( item='observation', provider='odas', # gdas # or None to see all providers observation_type='adt_cryosat2n' # comment out to search only based on provider diff --git a/src/swell/utilities/settings.py b/src/swell/utilities/settings.py new file mode 100644 index 000000000..9f595e197 --- /dev/null +++ b/src/swell/utilities/settings.py @@ -0,0 +1,37 @@ +# (C) Copyright 2021- United States Government as represented by the Administrator of the +# National Aeronautics and Space Administration. All Rights Reserved. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + + +# -------------------------------------------------------------------------------------------------- + +import os +import yaml + +# -------------------------------------------------------------------------------------------------- + + +def read_settings() -> dict: + ''' + Reads user settings from yaml file under ~/.swell/swell-settings.yaml + + Args: + None + + Returns: + Dictionary of settings specified in file. + ''' + settings_file = os.path.expanduser(os.path.join('~', '.swell', 'swell-settings.yaml')) + + if os.path.exists(settings_file): + with open(settings_file, 'r') as f: + settings_dict = yaml.safe_load(f) + + else: + settings_dict = {} + + return settings_dict + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/shell_commands.py b/src/swell/utilities/shell_commands.py index 20ab6cd65..bd23a4392 100644 --- a/src/swell/utilities/shell_commands.py +++ b/src/swell/utilities/shell_commands.py @@ -10,7 +10,7 @@ import os import stat import subprocess -from typing import Any, Optional, IO, Union +from typing import Any, IO from swell.utilities.logger import Logger @@ -20,8 +20,8 @@ def run_track_log_subprocess( logger: Logger, - command: Union[list[str], str], - output_log: Optional[str] = None, + command: list[str] | str, + output_log: str | None = None, **kwargs ) -> None: @@ -65,7 +65,7 @@ def run_track_log_subprocess( def run_subprocess_dev_null( logger: Logger, - command: Union[list[str], str], + command: list[str] | str | None, **kwargs ) -> None: @@ -77,9 +77,9 @@ def run_subprocess_dev_null( def run_subprocess( logger: Logger, - command: Union[list[str], str], - stdout: Union[int, IO[Any], None] = None, - stderr: Union[int, IO[Any], None] = None, + command: list[str] | str, + stdout: int | IO[Any] | None = None, + stderr: int | IO[Any] | None = None, **kwargs ) -> None: diff --git a/src/swell/utilities/slurm.py b/src/swell/utilities/slurm.py index 787650d81..41a7cfe51 100644 --- a/src/swell/utilities/slurm.py +++ b/src/swell/utilities/slurm.py @@ -9,17 +9,21 @@ import importlib import os import re +from typing import Union +from collections.abc import Mapping from ruamel.yaml import YAML from importlib import resources from swell.utilities.logger import Logger +# -------------------------------------------------------------------------------------------------- + -def prepare_scheduling_dict( +def prepare_slurm_defaults_and_overrides( logger: Logger, - experiment_dict: dict, platform: str, + slurm_overrides: Union[Mapping, str, None], ) -> dict: # Obtain platform-specific SLURM directives and set them as global defaults @@ -36,18 +40,13 @@ def prepare_scheduling_dict( except Exception as err: raise err + global_defaults = {} + global_defaults['slurm_directives_global'] = {} + logger.info(f'Loading SLURM user configuration for the "{platform}" platform') yaml = YAML(typ='safe') with resources.open_text(path_import, 'slurm.yaml') as yaml_file: - global_defaults = yaml.load(yaml_file) - - # Hard-coded SLURM defaults for certain tasks - # ------------------------------------------- - task_defaults = { - "RunJediVariationalExecutable": {"all": {"nodes": 3}}, - "RunJediUfoTestsExecutable": {"all": {"ntasks-per-node": 1}}, - "RunJediConvertStateSoca2ciceExecutable": {"all": {"nodes": 1}} - } + global_defaults['slurm_directives_global'] = yaml.load(yaml_file) # Global SLURM settings stored in $HOME/.swell/swell-slurm.yaml # ---------------------------------------------- @@ -55,145 +54,57 @@ def prepare_scheduling_dict( # See https://github.com/GEOS-ESM/swell/issues/351 user_globals = slurm_global_defaults(logger) - # Global SLURM settings from experiment dict (questionary / overrides YAML) - # ---------------------------------------------- - experiment_globals = {} - if "slurm_directives_global" in experiment_dict: - logger.info(f"Loading additional SLURM globals from experiment dict") - experiment_globals = experiment_dict["slurm_directives_global"] + # Expand experiment dict with SLURM overrides. + # NOTE: This is a bit of a hack. We should really either commit to using a + # separate file and pass it around everywhere, or commit fully to keeping + # everything in `experiment.yaml` and support it through the Questionary + # infrastructure. + # ---------------------------------- + if slurm_overrides is not None: + if isinstance(slurm_overrides, str): + logger.info(f"Reading SLURM directives from {slurm_overrides}.") + try: + with open(slurm_overrides, "r") as slurmfile: + slurm_overrides = yaml.safe_load(slurmfile) + except FileNotFoundError: + raise FileNotFoundError(f"Slurm config {slurm_overrides} not found.") + elif not isinstance(slurm_overrides, Mapping): + raise TypeError("Slurm overrides is not of type Mapping") + + # Ensure that SLURM dict is _only_ used for SLURM directives. + slurm_invalid_keys = set(slurm_overrides.keys()).difference({ + "slurm_directives_global", + "slurm_directives_tasks" + }) + if slurm_invalid_keys: + logger.abort(f'SLURM file contains invalid keys: {slurm_invalid_keys}') - # Task-specific SLURM settings from experiment dict (questionary / overrides YAML) - # ---------------------------------------------- - experiment_task_directives = {} - if "slurm_directives_tasks" in experiment_dict: - logger.info(f"Loading experiment-specific SLURM configs from experiment dict") - experiment_task_directives = experiment_dict["slurm_directives_tasks"] - - # List of tasks using slurm - # ------------------------- - slurm_tasks = { - 'BuildJedi', - 'BuildGeos', - 'EvaObservations', - 'EvaComparisonObservations', - 'EvaTimeseries', - 'GenerateBClimatology', - 'RunGeos', - 'RunJediEnsembleMeanVariance', - 'RunJediConvertStateSoca2ciceExecutable', - 'RunJediFgatExecutable', - 'RunJediHofxEnsembleExecutable', - 'RunJediHofxExecutable', - 'RunJediLocalEnsembleDaExecutable', - 'RunJediObsfiltersExecutable', - 'RunJediUfoTestsExecutable', - 'RunJediVariationalExecutable', - } - - # Throw an error if a user tries to set SLURM directives for a task that - # doesn't use SLURM. - experiment_slurm_tasks = set(experiment_task_directives.keys()) - non_slurm_tasks = experiment_slurm_tasks.difference(slurm_tasks) - assert len(non_slurm_tasks) == 0, \ - f"The following tasks cannot use SLURM: {non_slurm_tasks}" - - model_components = experiment_dict["model_components"] \ - if "model_components" in experiment_dict \ - else [] - - scheduling_dict = {} - for slurm_task in slurm_tasks: - # Priority order (first = highest priority) - # 1. Task-specific directives from experiment - # (experiment_task_directives[slurm_task]["all"]) - # 2. Global directives from experiment (experiment_globals) - # 3. Directives from user config (user_globals) - # 4. Hard-coded task-specific defaults (task_defaults) - # 5. Hard-coded global defaults (global_defaults) - # NOTE: Hard-code "job-name" to SWELL task here but it can be - # overwritten in task-specific directives. - directives = { - "job-name": slurm_task, - **global_defaults, - **user_globals, - **experiment_globals - } - if slurm_task in task_defaults: - if "all" in task_defaults[slurm_task]: - directives = { - **directives, - **task_defaults[slurm_task]["all"] - } - if slurm_task in experiment_task_directives: - if "all" in experiment_task_directives[slurm_task]: - directives = { - **directives, - **experiment_task_directives[slurm_task]["all"] - } - # Set model_agnostic directives - validate_directives(directives) - scheduling_dict[slurm_task] = {"directives": {"all": directives}} - # Now, add model component-specific logic. The inheritance here is more - # complicated: - # - Experiment global defaults (`experiment_globals`) - # - User global defaults (`user_globals`) - # - Task- and model-specific hard-coded defaults - # - Task-specific, model-generic hard-coded defaults - # - Global hard-coded defaults - # Now, for every model component, set the model-generic directives - # (`directives`) but overwrite with model-specific directives if - # present. - for model_component in model_components: - model_directives = { - "job-name": f"{slurm_task}-{model_component}", - **global_defaults - } - if slurm_task in task_defaults: - model_directives = add_directives( - model_directives, - task_defaults[slurm_task], - "all" - ) - model_directives = add_directives( - model_directives, - task_defaults[slurm_task], - model_component - ) - model_directives = { - **model_directives, - **user_globals, - **experiment_globals - } - if slurm_task in experiment_task_directives: - model_directives = add_directives( - model_directives, - experiment_task_directives[slurm_task], - "all" - ) - model_directives = add_directives( - model_directives, - experiment_task_directives[slurm_task], - model_component - ) - validate_directives(model_directives) - scheduling_dict[slurm_task]["directives"][model_component] = model_directives - - # Default execution time limit for everthing is PT1H - x = 'PT1H' - if slurm_task in experiment_task_directives.keys(): - x = experiment_task_directives[slurm_task].get('execution_time_limit', x) - scheduling_dict[slurm_task]['execution_time_limit'] = x - return scheduling_dict - - -def add_directives(target_dict: dict, input_dict: dict, key: str) -> dict: - if key in input_dict: - return { - **target_dict, - **input_dict[key] - } else: - return target_dict + slurm_overrides = {} + + if 'slurm_directives_global' not in slurm_overrides.keys(): + slurm_overrides['slurm_directives_global'] = {} + + if 'slurm_directives_tasks' not in slurm_overrides.keys(): + slurm_overrides['slurm_directives_tasks'] = {} + + slurm_dict = {} + + slurm_dict['slurm_directives_global'] = { + **global_defaults['slurm_directives_global'], + **user_globals, + **slurm_overrides['slurm_directives_global']} + + validate_directives(slurm_dict["slurm_directives_global"]) + + slurm_dict['slurm_directives_tasks'] = slurm_overrides['slurm_directives_tasks'] + + if 'slurm_directives_tasks' in slurm_dict: + for task in slurm_dict["slurm_directives_tasks"].keys(): + validate_directives(slurm_dict["slurm_directives_tasks"][task]) + return slurm_dict + +# -------------------------------------------------------------------------------------------------- def validate_directives(directive_dict: dict) -> None: @@ -204,12 +115,14 @@ def validate_directives(directive_dict: dict) -> None: for s in man_sbatch.split("\n") if re.search(directive_pattern, s) } - # Make sure that everything in `directive_dict` is in `directive_list`; - # i.e., that all entries are valid slurm directives. - invalid_directives = set(directive_dict.keys()).difference(directive_list) - assert \ - len(invalid_directives) == 0, \ - f"The following are invalid SLURM directives: {invalid_directives}" + + for key, item in directive_dict.items(): + if isinstance(item, Mapping): + validate_directives(item) + else: + assert key in directive_list + +# -------------------------------------------------------------------------------------------------- def slurm_global_defaults( @@ -217,14 +130,25 @@ def slurm_global_defaults( yaml_path: str = "~/.swell/swell-slurm.yaml" ) -> dict: yaml_path = os.path.expanduser(yaml_path) + ''' user_globals = {} + user_globals['slurm_directives_global'] = {} + if os.path.exists(yaml_path): logger.info(f"Loading SLURM user configuration from {yaml_path}") yaml = YAML(typ='safe') with open(yaml_path, "r") as yaml_file: + user_globals['slurm_directives_global'] = yaml.safe_load(yaml_file) + ''' + user_globals = {} + if os.path.exists(yaml_path): + yaml = YAML(typ='safe') + with open(yaml_path, 'r') as yaml_file: user_globals = yaml.load(yaml_file) return user_globals +# -------------------------------------------------------------------------------------------------- + man_sbatch = """ Parallel run options: diff --git a/src/swell/utilities/suite_utils.py b/src/swell/utilities/suite_utils.py index 1060a8b32..c2acd812c 100644 --- a/src/swell/utilities/suite_utils.py +++ b/src/swell/utilities/suite_utils.py @@ -8,9 +8,7 @@ # -------------------------------------------------------------------------------------------------- -import glob import os -import importlib from ruamel.yaml import YAML from swell.swell_path import get_swell_path @@ -18,67 +16,30 @@ # -------------------------------------------------------------------------------------------------- -def get_suites() -> list: - # Path to platforms - suites_directory = os.path.join(get_swell_path(), 'suites') +def get_model_components() -> list: - # List of base suites - suites = sorted([sdir for sdir in os.listdir(suites_directory) - if (os.path.isdir(os.path.join(suites_directory, sdir)) - and os.path.exists(os.path.join(suites_directory, sdir, 'flow.cylc')))]) + # Path to model interfaces + interface_directory = os.path.join(get_swell_path(), 'configuration', 'jedi', 'interfaces') - return suites + # Get models + return os.listdir(interface_directory) # -------------------------------------------------------------------------------------------------- -def get_suite_configs() -> list: - - suites = get_suites() - - # List of suites and associated sub-suites - suite_config_list = [] - - for suite in suites: - suite_sub_list = [] - suite_module = importlib.import_module(f'swell.suites.{suite}.suite_config') - suite_configs = getattr(suite_module, 'SuiteConfig') - - [suite_sub_list.append(suite_config[1:] if suite_config[0] == '_' else suite_config) - for suite_config in suite_configs.get_all()] - - suite_config_list.extend(sorted(suite_sub_list)) - - # List all directories in platform_directory - return suite_config_list +def read_override_file(override_path: str | None) -> dict: + if override_path is None: + return {} + else: + yaml = YAML(typ='safe') + with open(override_path, 'r') as f: + return yaml.load(f) # -------------------------------------------------------------------------------------------------- -def get_suite_tests() -> list: - - # Path to platforms - suite_tests_directory = os.path.join(get_swell_path(), 'test', 'suite_tests', '*.yaml') - - # List of tasks - suite_test_files = sorted(glob.glob(suite_tests_directory)) - - # Get just the task name - suite_tests = [] - for suite_test_file in suite_test_files: - suite_tests.append(os.path.basename(suite_test_file)[0:-5]) - - # Sort list alphabetically - suite_tests = sorted(suite_tests) - - # Return list of valid task choices - return suite_tests - - -# -------------------------------------------------------------------------------------------------- - def read_override_file(override_path: str | None) -> dict: if override_path is None: diff --git a/src/swell/utilities/swell_questions.py b/src/swell/utilities/swell_questions.py index 1d0e2f45c..f39708790 100644 --- a/src/swell/utilities/swell_questions.py +++ b/src/swell/utilities/swell_questions.py @@ -9,12 +9,20 @@ import os -from dataclasses import dataclass, asdict, field -from typing import List, Optional, Self, Union, Literal -from enum import Enum +from dataclasses import dataclass, asdict +from typing import Self, Literal +from enum import Enum, StrEnum from swell.utilities.datetime_util import is_datetime, is_duration from swell.swell_path import get_swell_path +from swell.utilities.dataclass_utils import mutable_field + +# -------------------------------------------------------------------------------------------------- + + +class QuestionType(StrEnum): + SUITE = 'suite' + TASK = 'task' # -------------------------------------------------------------------------------------------------- @@ -23,6 +31,7 @@ class WidgetType(Enum): STRING = "string" STRING_CHECK_LIST = "string-check-list" STRING_DROP_LIST = "string-drop-list" + STRING_LIST = "string-list" BOOLEAN = "boolean" ISO_DURATION = "iso-duration" ISO_DATETIME = "iso-datetime" @@ -53,6 +62,8 @@ def base_type(self) -> type: if 'iso-' in self.value: return str + raise TypeError('Could not deduce widget type') + def validate_value(self, value) -> bool: """ Validate that the value matches the type and format of the widget type. """ base_type = self.base_type() @@ -90,27 +101,14 @@ def validate_value(self, value) -> bool: @dataclass class SwellQuestion: """Basic dataclass for defining Swell questions for suites and tasks""" - default_value: str + default_value: any question_name: str widget_type: WidgetType prompt: str + models: list | None = None question_type: str = None ask_question: bool = False - options: Optional[str] = None - -# -------------------------------------------------------------------------------------------------- - - -class QuestionContainer: - """ Class to extend question lists for suites and tasks, use with Enum """ - - def __init__(self, *args): - arg_dict = asdict(args[0]) - setattr(self, arg_dict['list_name'], args[0]) - - @classmethod - def get_all(cls): - return cls._member_names_ + options: str | list | None = None # -------------------------------------------------------------------------------------------------- @@ -118,16 +116,15 @@ def get_all(cls): @dataclass class QuestionList: """Basic dataclass containing a list of questions for each model, suite, task""" - list_name: str - questions: List[Union[SwellQuestion, Self]] + questions: list[SwellQuestion | Self] = mutable_field([]) - geos_atmosphere: list = field(default_factory=lambda: []) - geos_cf: list = field(default_factory=lambda: []) - geos_marine: list = field(default_factory=lambda: []) + geos_atmosphere: list = mutable_field([]) + geos_cf: list = mutable_field([]) + geos_marine: list = mutable_field([]) # -------------------------------------------------------------------------------------------------- - def get_all_question_names(self, suite_task: Optional[Literal['suite', 'task']] = None) -> None: + def get_all_question_names(self, suite_task: Literal['suite', 'task'] | None = None) -> list: question_list = [] for model in [None] + os.listdir(os.path.join(get_swell_path(), 'configuration', 'jedi', 'interfaces')): @@ -143,7 +140,7 @@ def get_all_question_names(self, suite_task: Optional[Literal['suite', 'task']] # -------------------------------------------------------------------------------------------------- - def expand_question_list(self, model: Optional[str] = None): + def expand_question_list(self, model: str | None = None): question_list = [] # Loop through the items in the questions list @@ -157,7 +154,7 @@ def expand_question_list(self, model: Optional[str] = None): question = asdict(question_obj) # If the item is a question list, expand its contents - if 'list_name' in question.keys(): + if 'questions' in question.keys(): question_list.extend(question_obj.expand_question_list(model)) elif model is None: # Add to the model_independent question list @@ -171,7 +168,7 @@ def expand_question_list(self, model: Optional[str] = None): question_obj = question_obj.value question = asdict(question_obj) - if 'list_name' in question.keys(): + if 'questions' in question.keys(): question_list.extend(question_obj.expand_question_list(model)) else: question_list.append(question) @@ -183,14 +180,14 @@ def expand_question_list(self, model: Optional[str] = None): @dataclass class SuiteQuestion(SwellQuestion): - question_type: str = "suite" + question_type: QuestionType = QuestionType.SUITE # -------------------------------------------------------------------------------------------------- @dataclass class TaskQuestion(SwellQuestion): - question_type: str = "task" + question_type: QuestionType = QuestionType.TASK # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/test_cache.py b/src/swell/utilities/test_cache.py index 6b8988af5..5f03a9a41 100644 --- a/src/swell/utilities/test_cache.py +++ b/src/swell/utilities/test_cache.py @@ -7,15 +7,13 @@ # -------------------------------------------------------------------------------------------------- -from typing import Optional - import os from ruamel.yaml import YAML # -------------------------------------------------------------------------------------------------- -def get_test_cache() -> Optional[str]: +def get_test_cache() -> str | None: test_settings_file = os.path.expanduser('~/.swell/swell-test.yaml') if os.path.exists(test_settings_file): yaml = YAML(typ='safe')