diff --git a/docs/adding_a_suite.md b/docs/adding_a_suite.md index b3d255f24..8a15b3bcc 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,73 @@ 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` (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 -class SuiteConfig(QuestionContainer, Enum): - _3dvar_base = QuestionList( - list_name="3dvar", - questions=[ - sq.marine - ] - ) - - # -------------------------------------------------------------------------------------------------- - - _3dvar_tier1 = QuestionList( - list_name="3dvar", - 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']), - ] - ) +from swell.suites.base.all_suites import suite_configs +from swell.suites.base.suite_questions import marine + +_3dvar_base = QuestionList( + list_name="3dvar", + questions=[ + marine + ] +) + +suite_configs.register('3dvar', '3dvar_base', _3dvar_base) + +# -------------------------------------------------------------------------------------------------- + +_3dvar_tier1 = QuestionList( + list_name="3dvar", + 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', '3dvar_tier1', _3dvar_tier1) ``` 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. -`_3dvar_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_tier1` sets different default values which override the question defaults. If desired, other configurations can then inherit question defaults from `_3dvar_tier1`, and set their own defaults on top of the existing ones. +`_3dvar_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_tier1` sets different default values which override the question defaults. If desired, other configurations can then inherit question defaults from `_3dvar_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/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml b/src/swell/configuration/jedi/interfaces/geos_atmosphere/suite_questions.yaml index 3fcd0cf65..5f337d152 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,12 @@ 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_atmosphere/task_questions.yaml b/src/swell/configuration/jedi/interfaces/geos_atmosphere/task_questions.yaml index 636e49e1c..06a4c4927 100644 --- a/src/swell/configuration/jedi/interfaces/geos_atmosphere/task_questions.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_atmosphere/task_questions.yaml @@ -105,12 +105,6 @@ clean_patterns: - '*.txt' - logfile.*.out -cycling_varbc: - default_value: false - options: - - true - - false - ensemble_hofx_packets: default_value: 2 options: None 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 e2474eb4d..cd68810e9 100644 --- a/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml +++ b/src/swell/configuration/jedi/interfaces/geos_marine/suite_questions.yaml @@ -10,3 +10,13 @@ ensemble_hofx_packets: 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..5eef291e9 --- /dev/null +++ b/src/swell/configuration/question_defaults.py @@ -0,0 +1,1613 @@ +# (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, Union, 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 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 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 runahead_limit(SuiteQuestion): + default_value: str = "P4" + question_name: str = "runahead_limit" + ask_question: bool = True + prompt: str = ("Since this suite is non-cycling choose how " + "many hours the workflow can run ahead?") + 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 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 + +# -------------------------------------------------------------------------------------------------- + + +@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 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 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_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" + ask_question: bool = True + prompt: str = "Which GEOS tag do you wish to clone?" + 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 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 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_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 r2d2_local_path(TaskQuestion): + default_value: str = "defer_to_platform" + question_name: str = "r2d2_local_path" + prompt: str = "What is the path to the R2D2 local directory?" + widget_type: WType = WType.STRING + +# -------------------------------------------------------------------------------------------------- + + +@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 set_obs_as_local(TaskQuestion): + default_value: bool = False + question_name: str = "set_obs_as_local" + options: List[bool] = mutable_field([ + True, + False + ]) + models: List[str] = mutable_field([ + 'all_models' + ]) + prompt: str = "Treat observations as 'local' to the directory?" + 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" + 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: Union[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/deployment/create_experiment.py b/src/swell/deployment/create_experiment.py index 1c2105e9f..c69f8a843 100644 --- a/src/swell/deployment/create_experiment.py +++ b/src/swell/deployment/create_experiment.py @@ -9,7 +9,6 @@ import copy -import datetime import io import os import shutil @@ -17,14 +16,14 @@ from ruamel.yaml import YAML from typing import Union, 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 +37,7 @@ def clone_config( platform: str, advanced: bool ) -> str: + # Create a logger logger = get_logger('SwellCloneExperiment') @@ -98,40 +98,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,29 +121,78 @@ 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() # Expand all environment vars in the dictionary # --------------------------------------------- @@ -183,7 +214,7 @@ def prepare_config( # Return path to dictionary file # ------------------------------ - return experiment_dict_string_comments + return experiment_dict_string_comments, workflow_string # -------------------------------------------------------------------------------------------------- @@ -200,7 +231,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 # --------------- @@ -208,8 +239,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 # -------------------------- @@ -237,12 +268,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) + 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 # ----------------------------------------------------------- @@ -376,7 +403,6 @@ def template_modules_file( with open(modules_file, 'w') as modules_file_open: modules_file_open.write(modules_file_str) - # -------------------------------------------------------------------------------------------------- @@ -429,116 +455,3 @@ def create_modules_csh( # -------------------------------------------------------------------------------------------------- - - -def prepare_cylc_suite_jinja2( - logger: Logger, - swell_suite_path: str, - exp_suite_path: str, - experiment_dict: dict, - platform: str -) -> None: - - # Open suite file from swell - # -------------------------- - with open(os.path.join(swell_suite_path, 'flow.cylc'), 'r') as file: - suite_file = file.read() - - # Copy the experiment dictionary to the rendering dictionary - # ---------------------------------------------------------- - render_dictionary = copy.deepcopy(experiment_dict) - - # Get unique list of cycle times with model flags to render dictionary - # -------------------------------------------------------------------- - - # Convenience - fetch model_components prior to search for 'cycle_times' and 'ensemble_*' - model_components = dict_get(logger, experiment_dict, 'model_components', []) - - # Check if 'cycle_times' appears anywhere in the suite_file - if 'cycle_times' in suite_file: - - # 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 = [] - 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)) - 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 experiment_dict['models'][model_component]['cycle_times']: - cycle_time_dict[model_component] = True - cycle_times_dict_list.append(cycle_time_dict) - - render_dictionary['cycle_times'] = cycle_times_dict_list - - # Otherwise check that experiment_dict has cycle_times - elif 'cycle_times' in experiment_dict: - - cycle_times = list(set(experiment_dict['cycle_times'])) - cycle_times.sort() - render_dictionary['cycle_times'] = cycle_times - - else: - - # Otherwise use logger to abort - logger.abort('The suite file required cycle_times but there are no model components ' + - 'to gather them from or they are not provided in the experiment ' + - 'dictionary.') - - # Check if 'ensemble_hofx_strategy' appears anywhere in suite_file - ensemble_list = ['ensemble_'+s for s in ['num_members', 'hofx_strategy', 'hofx_packets']] - ensemble_list = ensemble_list + ['skip_ensemble_hofx'] - for ensemble_aspect in ensemble_list: - if ensemble_aspect in suite_file: - if len(model_components) == 0: - logger.abort(f'The suite file required {ensemble_aspect} ' + - 'there are no model components to gather them from or ' + - 'they are not provided in the experiment dictionary.') - for model_component in model_components: - render_dictionary[ensemble_aspect] = \ - experiment_dict['models'][model_component][ensemble_aspect] - - # Cycling VarBC exception (only for geos_atmosphere) - if 'cycling_varbc' in suite_file: - if 'geos_atmosphere' in model_components: - render_dictionary['cycling_varbc'] = \ - experiment_dict['models']['geos_atmosphere']['cycling_varbc'] - else: - logger.abort('The suite file required cycling_varbc but ' + - 'geos_atmosphere is not in the model components.') - - # Marine model toggles (only for geos_marine) - if 'marine_models' in suite_file: - if 'geos_marine' in model_components: - render_dictionary['marine_models'] = \ - experiment_dict['models']['geos_marine']['marine_models'] - else: - logger.abort('The suite file required marine_models but ' + - 'geos_marine is not in the model components.') - - render_dictionary['scheduling'] = prepare_scheduling_dict(logger, experiment_dict, platform) - - # Set some specific values for: - # ------------------------------ - # run time (note: these overwrite defaults above) - render_dictionary['scheduling']['BuildJedi']['execution_time_limit'] = 'PT3H' - render_dictionary['scheduling']['EvaObservations']['execution_time_limit'] = 'PT30M' - - # Render the template - # ------------------- - new_suite_file = template_string_jinja2(logger, suite_file, render_dictionary, False) - - # Write suite file to experiment - # ------------------------------ - with open(os.path.join(exp_suite_path, 'flow.cylc'), 'w') as file: - file.write(new_suite_file) - - -# -------------------------------------------------------------------------------------------------- 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 5d4c6a6f4..876c45802 100644 --- a/src/swell/deployment/launch_experiment.py +++ b/src/swell/deployment/launch_experiment.py @@ -88,6 +88,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('Launching the TUI, press \'q\' at any time to exit the TUI') input() @@ -104,7 +110,9 @@ def cylc_run_experiment(self) -> None: # NB: Could be a factory based on workfl def launch_experiment( suite_path: str, no_detach: bool, - log_path: str + log_path: str, + send_cylc_messages: bool = False, + allow_pause: bool = False ) -> None: # Get the path to where the suite files are located @@ -127,6 +135,20 @@ def launch_experiment( 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 7ba60dd75..0837e7492 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,21 @@ # -------------------------------------------------------------------------------------------------- -import copy import os from ruamel.yaml import YAML from collections.abc import Mapping -from typing import Union, Tuple, Optional +from typing import Union, 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 update_dict, add_dict +from swell.suites.base.suite_attributes import suite_configs +from swell.utilities.swell_questions import QuestionType # -------------------------------------------------------------------------------------------------- @@ -78,213 +78,198 @@ 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.comment_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}}) + + self.suite_needs_model_components = True + if 'model_components' not in question_dictionary_model_ind.keys(): + self.suite_needs_model_components = False - 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 + for question in suite_question_list: + if question['question_name'] == 'cycle_times': + question['models'] = None + question_dictionary_model_ind['cycle_times'] = question - self.questions_per_task[task] = [question['question_name'] - for question in question_list] - else: - self.questions_per_task[task] = [] + self.question_dictionary_model_ind = question_dictionary_model_ind + self.question_dictionary_model_dep = question_dictionary_model_dep - # 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 + # ---------------------------------------------------------------------------------------------- - # 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 + def prepare_task_question_dictionary(self): + # Fill in the question dictionaries with questions from the tasks - # 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 + # Track all possible tasks + task_options = [] - # Iterate through the task questions - # ---------------------------------- - for key, value in question_dictionary_tasks.items(): + # Model components used by the experiment + model_components = [] + if 'model_components' in self.experiment_dict.keys(): + model_components = self.experiment_dict['model_components'] - # If a question has no counterpart specified in suite questions, merge it - # ----------------------------------------------------------------------- - if key not in question_dictionary: - question_dictionary[key] = value - else: + # 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() - # 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 + for question in question_list: + question_dict = {question['question_name']: question} - # 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') - 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]) + if question['models'] is not None: + model_dict = {} - # 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 + 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 - for key in keys_to_remove: - del self.question_dictionary_model_dep[model][key] + 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) + + 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 key, val in self.question_dictionary_model_ind.items(): - if key in platform_defaults.keys(): - self.question_dictionary_model_ind[key].update(platform_defaults[key]) + for question_name, question in self.question_dictionary_model_ind.items(): + 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 # ---------------------------------------------------- @@ -293,51 +278,55 @@ 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': - 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}' + 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}' # ---------------------------------------------------------------------------------------------- - def override_with_external(self) -> None: + def override_with_external(self, suite_task: QuestionType) -> None: # Append with any user provide overrides if self.override is not None: @@ -346,7 +335,7 @@ def override_with_external(self) -> None: override_dict = {} if isinstance(self.override, Mapping): - override_dict.update_dict(override_dict, self.override) + override_dict = update_dict(override_dict, self.override) elif isinstance(self.override, str): yaml = YAML(typ='safe') @@ -361,173 +350,70 @@ 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 override_dict: - val['default_value'] = override_dict[key] + for question_name, question in self.question_dictionary_model_ind.items(): + if question['question_type'] == suite_task: + if question_name in override_dict: + question['default_value'] = override_dict[question_name] # Iterate over the model_dep dictionary and override # -------------------------------------------------- if self.suite_needs_model_components and 'models' in override_dict.keys(): for model, model_dict in self.question_dictionary_model_dep.items(): - for key, val in model_dict.items(): - if model in override_dict['models']: - if key in override_dict['models'][model]: - val['default_value'] = override_dict['models'][model][key] + for question_name, question in model_dict.items(): + if question['question_type'] == suite_task: + if model in override_dict['models']: + if question_name in override_dict['models'][model]: + question['default_value'] = override_dict[ + '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. - - 1. Ask the model independent suite questions. - - 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. - - 3. Get a list of tasks that do not depend on the model component. - - 4. Ask the model independent task questions. + def get_questions_of_type(self, + suite_task: QuestionType, + question_dictionary: Mapping + ) -> Mapping: - 5. Check that the suite in question has model_components + # Get all questions of a certain type + out_dict = {} - 6. Ask the model dependent suite questions. + 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]) - 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. + else: + for question_name, question in question_dictionary.items(): + if question['question_type'] == suite_task: + out_dict[question['question_name']] = question - 8. Ask the new task questions that do not actually depend on the model.. + return out_dict - 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) - - # 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 + 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) - 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, @@ -544,10 +430,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 @@ -574,11 +460,11 @@ 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'] # ---------------------------------------------------------------------------------------------- @@ -595,98 +481,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..f253fc61a 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 @@ -27,6 +27,9 @@ def get_answer(self, logger: Logger, key: str, val: dict, model: Optional[str] = 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/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 b4d238308..cb8f3573f 100644 --- a/src/swell/suites/3dfgat_atmos/flow.cylc +++ b/src/swell/suites/3dfgat_atmos/workflow.py @@ -4,6 +4,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 @@ -72,14 +83,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}} @@ -108,88 +122,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 %} +''' # 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() + 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_cycle/suite_config.py b/src/swell/suites/3dfgat_cycle/suite_config.py index dbac95e62..33269dedf 100644 --- a/src/swell/suites/3dfgat_cycle/suite_config.py +++ b/src/swell/suites/3dfgat_cycle/suite_config.py @@ -8,89 +8,88 @@ # -------------------------------------------------------------------------------------------------- -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 = '3dfgat_cycle' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +_3dfgat_cycle_tier1 = QuestionList( + questions=[ + marine, + qd.cycling_varbc(), + 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([ + "*.txt", + "*.rc", + "*.bin" + ]), + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dfgat_cycle_tier1', _3dfgat_cycle_tier1) - _3dfgat_cycle_tier1 = QuestionList( - list_name="3dfgat_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([ - "*.txt", - "*.rc", - "*.bin" - ]), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +_3dfgat_cycle = QuestionList( + questions=[ + _3dfgat_cycle_tier1 + ] +) - _3dfgat_cycle = QuestionList( - list_name="3dfgat_cycle", - questions=[ - _3dfgat_cycle_tier1 - ] - ) +suite_configs.register(suite_name, '3dfgat_cycle', _3dfgat_cycle) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dfgat_cycle/flow.cylc b/src/swell/suites/3dfgat_cycle/workflow.py similarity index 50% rename from src/swell/suites/3dfgat_cycle/flow.cylc rename to src/swell/suites/3dfgat_cycle/workflow.py index db6b339ba..2b601e50d 100644 --- a/src/swell/suites/3dfgat_cycle/flow.cylc +++ b/src/swell/suites/3dfgat_cycle/workflow.py @@ -1,9 +1,20 @@ -# (C) Copyright 2023 United States Government as represented by the Administrator of the +# (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 executing Geos forecast @@ -121,148 +132,67 @@ {% 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 %} - - [[RunGeosExecutable]] - script = "swell task RunGeosExecutable $config -d $datetime" - platform = {{platform}} - execution time limit = {{scheduling["RunGeosExecutable"]["execution_time_limit"]}} - execution retry delays = 2*PT1M - [[[directives]]] - {%- for key, value in scheduling["RunGeosExecutable"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PrepGeosRunDir]] - script = "swell task PrepGeosRunDir $config -d $datetime" - - [[RemoveForecastDir]] - script = "swell task RemoveForecastDir $config -d $datetime" - - [[GetGeosRestart]] - script = "swell task GetGeosRestart $config -d $datetime" - - {% for model_component in model_components %} - - [[LinkGeosOutput-{{model_component}}]] - script = "swell task LinkGeosOutput $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"]}} - execution retry delays = 2*PT1M - [[[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"]}} - [[[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_cycle') +class Workflow_3dfgat_cycle(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) + + 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.GetGeosRestart()) + self.tasks.append(ta.PrepGeosRunDir()) + self.tasks.append(ta.RunGeosExecutable()) + + 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.LinkGeosOutput(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.RemoveForecastDir(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..d02359e0e --- /dev/null +++ b/src/swell/suites/3dvar/__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/suite_config.py b/src/swell/suites/3dvar/suite_config.py index b385a0554..c607add1c 100644 --- a/src/swell/suites/3dvar/suite_config.py +++ b/src/swell/suites/3dvar/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' - # -------------------------------------------------------------------------------------------------- +_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_tier1 = QuestionList( - list_name="3dvar", - 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_tier1', _3dvar_tier1) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- - _3dvar = QuestionList( - list_name="3dvar", - questions=[ - _3dvar_tier1 - ] - ) +_3dvar = QuestionList( + questions=[ + _3dvar_tier1 + ] +) - # -------------------------------------------------------------------------------------------------- +suite_configs.register(suite_name, '3dvar', _3dvar) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar/flow.cylc b/src/swell/suites/3dvar/workflow.py similarity index 53% rename from src/swell/suites/3dvar/flow.cylc rename to src/swell/suites/3dvar/workflow.py index 9b1d063fd..6860f0a73 100644 --- a/src/swell/suites/3dvar/flow.cylc +++ b/src/swell/suites/3dvar/workflow.py @@ -4,6 +4,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 @@ -45,6 +56,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 @@ -79,7 +91,6 @@ EvaJediLog-{{model_component}} & EvaIncrement-{{model_component}} & EvaObservations-{{model_component}} & SaveObsDiags-{{model_component}} => CleanCycle-{{model_component}} - {% endif %} {% endfor %} """ @@ -91,85 +102,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 - - # 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') +class Workflow_3dvar(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) + + 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_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 e0861d645..4a3e4f5f4 100644 --- a/src/swell/suites/3dvar_atmos/flow.cylc +++ b/src/swell/suites/3dvar_atmos/workflow.py @@ -4,6 +4,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 @@ -105,85 +116,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 %} - - [[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() + 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_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_cycle/flow.cylc b/src/swell/suites/3dvar_cycle/flow.cylc deleted file mode 100644 index 8db2144fd..000000000 --- a/src/swell/suites/3dvar_cycle/flow.cylc +++ /dev/null @@ -1,270 +0,0 @@ -# (C) Copyright 2023 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 Geos forecast - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -# -------------------------------------------------------------------------------------------------- - -[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 - - # Clone JEDI source code - CloneJedi - - # Build Geos source code by linking - CloneGeos => BuildGeosByLinking? - - # Build JEDI source code by linking - CloneJedi => BuildJediByLinking? - - # If not able to link to build create the build - BuildGeosByLinking:fail? => BuildGeos - - # If not able to link to build create the build - BuildJediByLinking:fail? => BuildJedi - - # Need first set of restarts to run model - GetGeosRestart => PrepGeosRunDir - - # Model cannot run without code - BuildGeosByLinking? | BuildGeos => RunGeosExecutable - - {% for model_component in model_components %} - - # JEDI cannot run without code - BuildJediByLinking? | BuildJedi => RunJediVariationalExecutable-{{model_component}} - - # Stage JEDI static files - CloneJedi => StageJedi-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - - {% endfor %} - """ - - {% for cycle_time in cycle_times %} - {{cycle_time.cycle_time}} = """ - {% for model_component in model_components %} - - # Model things - # Run the forecast through two windows (need to output restarts at the end of the - # first window and backgrounds for the second window) - MoveDaRestart-{{model_component}}[-{{models[model_component]["window_length"]}}] => PrepGeosRunDir - PrepGeosRunDir => RunGeosExecutable - - # Run the analysis - # RunGeosExecutable => StageJediCycle-{{model_component}} - RunGeosExecutable => LinkGeosOutput-{{model_component}} - LinkGeosOutput-{{model_component}} => GenerateBClimatology-{{model_component}} - - # Data assimilation things - StageJediCycle-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - - GenerateBClimatology-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} - RenderJediObservations-{{model_component}} => RunJediVariationalExecutable-{{model_component}} - - # Run analysis diagnostics - RunJediVariationalExecutable-{{model_component}} => EvaObservations-{{model_component}} - RunJediVariationalExecutable-{{model_component}} => EvaJediLog-{{model_component}} - RunJediVariationalExecutable-{{model_component}} => EvaIncrement-{{model_component}} - - # Prepare analysis for next forecast - EvaIncrement-{{model_component}} => PrepareAnalysis-{{model_component}} - {% if 'cice6' in models[model_component]["marine_models"] %} - PrepareAnalysis-{{model_component}} => RunJediConvertStateSoca2ciceExecutable-{{model_component}} - # RunJediConvertStateSoca2ciceExecutable-{{model_component}} => SaveRestart-{{model_component}} - RunJediConvertStateSoca2ciceExecutable-{{model_component}} => MoveDaRestart-{{model_component}} - RunJediConvertStateSoca2ciceExecutable-{{model_component}} => CleanCycle-{{model_component}} - {% else %} - # PrepareAnalysis-{{model_component}} => SaveRestart-{{model_component}} - PrepareAnalysis-{{model_component}} => MoveDaRestart-{{model_component}} - {% endif %} - - # Move restart to next cycle - # SaveRestart-{{model_component}} => MoveDaRestart-{{model_component}} - - # Save analysis output - # RunJediVariationalExecutable-{{model_component}} => SaveAnalysis-{{model_component}} - RunJediVariationalExecutable-{{model_component}} => SaveObsDiags-{{model_component}} - - # Save model output - # MoveBackground-{{model_component}} => StoreBackground-{{model_component}} - - # Remove Run Directory - # MoveDaRestart-{{model_component}} & MoveBackground-{{model_component}} => RemoveForecastDir - MoveDaRestart-{{model_component}} => RemoveForecastDir - - # Clean up large files - # EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & SaveObsDiags-{{model_component}} & RemoveForecastDir => - EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & EvaIncrement-{{model_component}} & SaveObsDiags-{{model_component}} => - CleanCycle-{{model_component}} - {% 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 %} - - [[RunGeosExecutable]] - script = "swell task RunGeosExecutable $config -d $datetime" - platform = {{platform}} - execution time limit = {{scheduling["RunGeosExecutable"]["execution_time_limit"]}} - execution retry delays = 2*PT1M - [[[directives]]] - {%- for key, value in scheduling["RunGeosExecutable"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[PrepGeosRunDir]] - script = "swell task PrepGeosRunDir $config -d $datetime" - - [[RemoveForecastDir]] - script = "swell task RemoveForecastDir $config -d $datetime" - - [[GetGeosRestart]] - script = "swell task GetGeosRestart $config -d $datetime" - - {% for model_component in model_components %} - - [[LinkGeosOutput-{{model_component}}]] - script = "swell task LinkGeosOutput $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"]}} - execution retry delays = 2*PT1M - [[[directives]]] - {%- for key, value in scheduling["GenerateBClimatology"]["directives"][model_component].items() %} - --{{key}} = {{value}} - {%- endfor %} - - [[GenerateBClimatologyByLinking-{{model_component}}]] - script = "swell task GenerateBClimatologyByLinking $config -d $datetime -m {{model_component}}" - - {% 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 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 %} - -# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cycle/suite_config.py b/src/swell/suites/3dvar_cycle/suite_config.py index ef5089d02..89e6bfaa6 100644 --- a/src/swell/suites/3dvar_cycle/suite_config.py +++ b/src/swell/suites/3dvar_cycle/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 marine +from swell.suites.base.suite_attributes import suite_configs +suite_name = '3dvar_cycle' # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): +_3dvar_cycle_tier1 = QuestionList( + questions=[ + marine, + qd.cycling_varbc(), + 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']), + ], + 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_cycle_tier1', _3dvar_cycle_tier1) - _3dvar_cycle_tier1 = QuestionList( - list_name="3dvar_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']), - ], - 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_cycle = QuestionList( + questions=[ + _3dvar_cycle_tier1 + ] +) - _3dvar_cycle = QuestionList( - list_name="3dvar_cycle", - questions=[ - _3dvar_cycle_tier1 - ] - ) +suite_configs.register(suite_name, '3dvar_cycle', _3dvar_cycle) - # -------------------------------------------------------------------------------------------------- +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/3dvar_cycle/workflow.py b/src/swell/suites/3dvar_cycle/workflow.py new file mode 100644 index 000000000..0e7429a45 --- /dev/null +++ b/src/swell/suites/3dvar_cycle/workflow.py @@ -0,0 +1,193 @@ +# (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 executing Geos forecast + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + UTC mode = True + allow implicit tasks = False + +# -------------------------------------------------------------------------------------------------- + +[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 + + # Clone JEDI source code + CloneJedi + + # Build Geos source code by linking + CloneGeos => BuildGeosByLinking? + + # Build JEDI source code by linking + CloneJedi => BuildJediByLinking? + + # If not able to link to build create the build + BuildGeosByLinking:fail? => BuildGeos + + # If not able to link to build create the build + BuildJediByLinking:fail? => BuildJedi + + # Need first set of restarts to run model + GetGeosRestart => PrepGeosRunDir + + # Model cannot run without code + BuildGeosByLinking? | BuildGeos => RunGeosExecutable + + {% for model_component in model_components %} + + # JEDI cannot run without code + BuildJediByLinking? | BuildJedi => RunJediVariationalExecutable-{{model_component}} + + # Stage JEDI static files + CloneJedi => StageJedi-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + + {% endfor %} + """ + + {% for cycle_time in cycle_times %} + {{cycle_time.cycle_time}} = """ + {% for model_component in model_components %} + + # Model things + # Run the forecast through two windows (need to output restarts at the end of the + # first window and backgrounds for the second window) + MoveDaRestart-{{model_component}}[-{{models[model_component]["window_length"]}}] => PrepGeosRunDir + PrepGeosRunDir => RunGeosExecutable + + # Run the analysis + # RunGeosExecutable => StageJediCycle-{{model_component}} + RunGeosExecutable => LinkGeosOutput-{{model_component}} + LinkGeosOutput-{{model_component}} => GenerateBClimatology-{{model_component}} + + # Data assimilation things + StageJediCycle-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + + GenerateBClimatology-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + GetObservations-{{model_component}} => RenderJediObservations-{{model_component}} + RenderJediObservations-{{model_component}} => RunJediVariationalExecutable-{{model_component}} + + # Run analysis diagnostics + RunJediVariationalExecutable-{{model_component}} => EvaObservations-{{model_component}} + RunJediVariationalExecutable-{{model_component}} => EvaJediLog-{{model_component}} + RunJediVariationalExecutable-{{model_component}} => EvaIncrement-{{model_component}} + + # Prepare analysis for next forecast + EvaIncrement-{{model_component}} => PrepareAnalysis-{{model_component}} + {% if 'cice6' in models[model_component]["marine_models"] %} + PrepareAnalysis-{{model_component}} => RunJediConvertStateSoca2ciceExecutable-{{model_component}} + # RunJediConvertStateSoca2ciceExecutable-{{model_component}} => SaveRestart-{{model_component}} + RunJediConvertStateSoca2ciceExecutable-{{model_component}} => MoveDaRestart-{{model_component}} + RunJediConvertStateSoca2ciceExecutable-{{model_component}} => CleanCycle-{{model_component}} + {% else %} + # PrepareAnalysis-{{model_component}} => SaveRestart-{{model_component}} + PrepareAnalysis-{{model_component}} => MoveDaRestart-{{model_component}} + {% endif %} + + # Move restart to next cycle + # SaveRestart-{{model_component}} => MoveDaRestart-{{model_component}} + + # Save analysis output + # RunJediVariationalExecutable-{{model_component}} => SaveAnalysis-{{model_component}} + RunJediVariationalExecutable-{{model_component}} => SaveObsDiags-{{model_component}} + + # Save model output + # MoveBackground-{{model_component}} => StoreBackground-{{model_component}} + + # Remove Run Directory + # MoveDaRestart-{{model_component}} & MoveBackground-{{model_component}} => RemoveForecastDir + MoveDaRestart-{{model_component}} => RemoveForecastDir + + # Clean up large files + # EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & SaveObsDiags-{{model_component}} & RemoveForecastDir => + EvaObservations-{{model_component}} & EvaJediLog-{{model_component}} & EvaIncrement-{{model_component}} & SaveObsDiags-{{model_component}} => + CleanCycle-{{model_component}} + {% endfor %} + """ + {% endfor %} +# -------------------------------------------------------------------------------------------------- + +[runtime] + + # Task defaults + # ------------- + +''' # noqa + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('3dvar_cycle') +class Workflow_3dvar_cycle(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) + + 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.GetGeosRestart()) + self.tasks.append(ta.PrepGeosRunDir()) + self.tasks.append(ta.RunGeosExecutable()) + + 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.LinkGeosOutput(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.RemoveForecastDir(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/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..0d4c99e86 --- /dev/null +++ b/src/swell/suites/base/suite_questions.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. + + +# -------------------------------------------------------------------------------------------------- + +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() + ] +) + +# -------------------------------------------------------------------------------------------------- + +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 dce2ce3d2..000000000 --- a/src/swell/suites/build_geos/flow.cylc +++ /dev/null @@ -1,56 +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. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for building the GEOS model - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - allow implicit tasks = False - -# -------------------------------------------------------------------------------------------------- - -[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..a97f4c544 --- /dev/null +++ b/src/swell/suites/build_geos/workflow.py @@ -0,0 +1,73 @@ +# (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 + +# -------------------------------------------------------------------------------------------------- + +[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() + 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 b7e347dee..000000000 --- a/src/swell/suites/build_jedi/flow.cylc +++ /dev/null @@ -1,56 +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. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for building the JEDI code - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - allow implicit tasks = False - -# -------------------------------------------------------------------------------------------------- - -[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..6850c9715 --- /dev/null +++ b/src/swell/suites/build_jedi/workflow.py @@ -0,0 +1,73 @@ +# (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 + +# -------------------------------------------------------------------------------------------------- + +[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() + 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 7df94f8c8..000000000 --- a/src/swell/suites/compare/flow.cylc +++ /dev/null @@ -1,96 +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. - - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for running comparison tests on completed experiments - - - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -# -------------------------------------------------------------------------------------------------- - -[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}} - JediLogComparison-{{model_component}}:fail? => EvaComparisonJediLog-{{model_component}} - JediLogComparison-{{model_component}}:fail? => EvaComparisonObservations-{{model_component}} => 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 %} - - {% 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 9f8c4807e..e445768d8 100644 --- a/src/swell/suites/compare/suite_config.py +++ b/src/swell/suites/compare/suite_config.py @@ -7,63 +7,63 @@ # # -------------------------------------------------------------------------------------------------- -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 enum import Enum +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 # -------------------------------------------------------------------------------------------------- +suite_name = 'compare' -class SuiteConfig(QuestionContainer, Enum): +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 = 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( + questions=[ + compare, + qd.comparison_log_type('variational'), + qd.model_components(['geos_marine']), + ] +) - compare_variational_marine = QuestionList( - list_name="compare_variational_marine", - 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( - list_name="compare_variational_atmosphere", - questions=[ - compare, - qd.comparison_log_type('variational'), - qd.model_components(['geos_atmosphere']), - ] - ) +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_fgat_marine = QuestionList( - list_name="compare_fgat_marine", - questions=[ - compare, - qd.comparison_log_type('fgat'), - qd.model_components(['geos_marine']), - ] - ) +# -------------------------------------------------------------------------------------------------- - # -------------------------------------------------------------------------------------------------- +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..fb4348bb8 --- /dev/null +++ b/src/swell/suites/compare/workflow.py @@ -0,0 +1,112 @@ +# (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 + +# -------------------------------------------------------------------------------------------------- + +[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}} + JediLogComparison-{{model_component}}:fail? => EvaComparisonJediLog-{{model_component}} + JediLogComparison-{{model_component}}:fail? => EvaComparisonObservations-{{model_component}} => 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() + 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()) + + 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)) + + 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 ce1e0f4cc..9a3f55a50 100644 --- a/src/swell/suites/convert_bufr/suite_config.py +++ b/src/swell/suites/convert_bufr/suite_config.py @@ -8,39 +8,37 @@ # -------------------------------------------------------------------------------------------------- -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): - - # -------------------------------------------------------------------------------------------------- - - 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", - ]), - qd.bufr_obs_classes([ - "ncep_1bamua_bufr", - "ncep_mtiasi_bufr", - ]), - ] - ) +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", + ]), + qd.bufr_obs_classes([ + "ncep_1bamua_bufr", + "ncep_mtiasi_bufr", + ]), + ] +) + +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 60% rename from src/swell/suites/convert_bufr/flow.cylc rename to src/swell/suites/convert_bufr/workflow.py index 8a5e5eeac..e999d13c1 100644 --- a/src/swell/suites/convert_bufr/flow.cylc +++ b/src/swell/suites/convert_bufr/workflow.py @@ -4,6 +4,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 @@ -71,47 +82,40 @@ # 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" +# -------------------------------------------------------------------------------------------------- - [[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 %} +@workflows.register('convert_bufr') +class Workflow_convert_bufr(CylcWorkflow): - {% for model_component in model_components %} + 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) - [[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 55% rename from src/swell/suites/convert_ncdiags/flow.cylc rename to src/swell/suites/convert_ncdiags/workflow.py index c43986dcd..9118f5e8b 100644 --- a/src/swell/suites/convert_ncdiags/flow.cylc +++ b/src/swell/suites/convert_ncdiags/workflow.py @@ -4,6 +4,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 @@ -60,43 +71,38 @@ # 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 %} +@workflows.register('convert_ncdiags') +class Workflow_convert_ncdiags(CylcWorkflow): - [[ GetGsiBc ]] - script = "swell task GetGsiBc $config -d $datetime -m geos_atmosphere" + 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) - [[ 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 56% rename from src/swell/suites/eva_capabilities/flow.cylc rename to src/swell/suites/eva_capabilities/workflow.py index ca0f7547a..4beae3288 100644 --- a/src/swell/suites/eva_capabilities/flow.cylc +++ b/src/swell/suites/eva_capabilities/workflow.py @@ -4,6 +4,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 @@ -56,37 +67,37 @@ # 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() + 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_geos/__init__.py b/src/swell/suites/forecast_geos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/swell/suites/forecast_geos/suite_config.py b/src/swell/suites/forecast_geos/suite_config.py index 4fb2db306..635fc974a 100644 --- a/src/swell/suites/forecast_geos/suite_config.py +++ b/src/swell/suites/forecast_geos/suite_config.py @@ -8,46 +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_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_geos_tier1', forecast_geos_tier1) # -------------------------------------------------------------------------------------------------- -class SuiteConfig(QuestionContainer, Enum): - - # -------------------------------------------------------------------------------------------------- - - forecast_geos_tier1 = QuestionList( - list_name="forecast_geos", - questions=[ - sq.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"), - ], - ) - - # -------------------------------------------------------------------------------------------------- - - forecast_geos = QuestionList( - list_name="forecast_geos", - questions=[ - forecast_geos_tier1 - ] - ) - - # -------------------------------------------------------------------------------------------------- +forecast_geos = QuestionList( + questions=[ + forecast_geos_tier1 + ] +) + +suite_configs.register(suite_name, 'forecast_geos', forecast_geos) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/forecast_geos/flow.cylc b/src/swell/suites/forecast_geos/workflow.py similarity index 56% rename from src/swell/suites/forecast_geos/flow.cylc rename to src/swell/suites/forecast_geos/workflow.py index cc8d74dea..566d5c097 100644 --- a/src/swell/suites/forecast_geos/flow.cylc +++ b/src/swell/suites/forecast_geos/workflow.py @@ -4,6 +4,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 Geos forecast without DA @@ -65,52 +76,39 @@ # 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 %} - - [[PrepGeosRunDir]] - script = "swell task PrepGeosRunDir $config -d $datetime" - - [[RemoveForecastDir]] - script = "swell task RemoveForecastDir $config -d $datetime" - - [[GetGeosRestart]] - script = "swell task GetGeosRestart $config -d $datetime" - - [[MoveForecastRestart]] - script = "swell task MoveForecastRestart $config -d $datetime" - - [[SaveRestart]] - script = "swell task SaveRestart $config -d $datetime" - - [[RunGeosExecutable]] - script = "swell task RunGeosExecutable $config -d $datetime" - platform = {{platform}} - execution time limit = {{scheduling["RunGeosExecutable"]["execution_time_limit"]}} - [[[directives]]] - {%- for key, value in scheduling["RunGeosExecutable"]["directives"]["all"].items() %} - --{{key}} = {{value}} - {%- endfor %} + +''' + +# -------------------------------------------------------------------------------------------------- + + +@workflows.register('forecast_geos') +class Workflow_forecast_geos(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) + + 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.GetGeosRestart()) + self.tasks.append(ta.PrepGeosRunDir()) + self.tasks.append(ta.RunGeosExecutable()) + self.tasks.append(ta.MoveForecastRestart()) + self.tasks.append(ta.SaveRestart()) + self.tasks.append(ta.RemoveForecastDir()) # -------------------------------------------------------------------------------------------------- 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 53% rename from src/swell/suites/geosadas/flow.cylc rename to src/swell/suites/geosadas/workflow.py index 9bd6b7936..ae1af9d42 100644 --- a/src/swell/suites/geosadas/flow.cylc +++ b/src/swell/suites/geosadas/workflow.py @@ -4,10 +4,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. + # -------------------------------------------------------------------------------------------------- -# 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] @@ -68,58 +75,44 @@ # 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" - - [[ 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" - - [[ GetGeosAdasBackground ]] - script = "swell task GetGeosAdasBackground $config -d $datetime -m geos_atmosphere" - - [[RenderJediObservations-{{model_component}}]] - script = "swell task RenderJediObservations $config -d $datetime -m geos_atmosphere" +# -------------------------------------------------------------------------------------------------- - [[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 %} - [[CleanCycle]] - script = "swell task CleanCycle $config -d $datetime -m geos_atmosphere" +@workflows.register('geosadas') +class Workflow_geosadas(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) + + 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.CloneGeosMksi()) + + 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..52c193943 100644 --- a/src/swell/suites/hofx/suite_config.py +++ b/src/swell/suites/hofx/suite_config.py @@ -8,81 +8,81 @@ # -------------------------------------------------------------------------------------------------- -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.cycling_varbc(), + 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([]), + ] +) - # -------------------------------------------------------------------------------------------------- +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 55% rename from src/swell/suites/hofx/flow.cylc rename to src/swell/suites/hofx/workflow.py index 666399b9d..e363e1b91 100644 --- a/src/swell/suites/hofx/flow.cylc +++ b/src/swell/suites/hofx/workflow.py @@ -4,6 +4,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) @@ -90,79 +101,48 @@ # 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() + 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 3af9c5e6e..c1748c95f 100644 --- a/src/swell/suites/hofx_cf/suite_config.py +++ b/src/swell/suites/hofx_cf/suite_config.py @@ -8,37 +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.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.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 562482ecf..e687330ae 100644 --- a/src/swell/suites/hofx_cf/flow.cylc +++ b/src/swell/suites/hofx_cf/workflow.py @@ -4,6 +4,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) @@ -83,67 +94,43 @@ # 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() + 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 dc999598f..000000000 --- a/src/swell/suites/ingest_obs/flow.cylc +++ /dev/null @@ -1,32 +0,0 @@ -[scheduler] - UTC mode = True - allow implicit tasks = False - -[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] - [[root]] - pre-script = "source $CYLC_SUITE_DEF_PATH/modules" - - [[[environment]]] - datetime = $CYLC_TASK_CYCLE_POINT - config = $CYLC_SUITE_DEF_PATH/experiment.yaml - - {% for model_component in model_components %} - [[IngestObs-{{model_component}}]] - script = "swell task IngestObs $config -d $datetime -m {{model_component}}" - execution time limit = PT10M - - {% endfor %} \ No newline at end of file diff --git a/src/swell/suites/ingest_obs/suite_config.py b/src/swell/suites/ingest_obs/suite_config.py index 6d9f1655f..fc0bd76d7 100644 --- a/src/swell/suites/ingest_obs/suite_config.py +++ b/src/swell/suites/ingest_obs/suite_config.py @@ -5,36 +5,47 @@ """ -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, - ], - ) - # 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("2021-07-02T06:00:00Z"), - qd.final_cycle_point("2021-07-03T06: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']), # List of obs names - qd.dry_run(True), - ] - ) +# -------------------------------------------------------------------------------------------------- + +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, + ], +) + +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("2021-07-02T06:00:00Z"), + qd.final_cycle_point("2021-07-03T06: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']), # List of obs names + qd.dry_run(True), + ] +) + +suite_configs.register(suite_name, 'ingest_obs_marine', ingest_obs_marine) + +# -------------------------------------------------------------------------------------------------- diff --git a/src/swell/suites/ingest_obs/workflow.py b/src/swell/suites/ingest_obs/workflow.py new file mode 100644 index 000000000..1ba080a8c --- /dev/null +++ b/src/swell/suites/ingest_obs/workflow.py @@ -0,0 +1,67 @@ +# (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 + +[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() + 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 8675bccba..000000000 --- a/src/swell/suites/localensembleda/flow.cylc +++ /dev/null @@ -1,238 +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. - -# -------------------------------------------------------------------------------------------------- - -# Cylc suite for executing JEDI-based LocalEnsembleDA Algorithm - -# -------------------------------------------------------------------------------------------------- - -[scheduler] - UTC mode = True - allow implicit tasks = False - -# -------------------------------------------------------------------------------------------------- - -[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 437dd35ab..5f2536586 100644 --- a/src/swell/suites/localensembleda/suite_config.py +++ b/src/swell/suites/localensembleda/suite_config.py @@ -8,131 +8,133 @@ # -------------------------------------------------------------------------------------------------- -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.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("4D"), + 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.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("4D"), + 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.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("4D"), - 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.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("4D"), - 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..b70b39444 --- /dev/null +++ b/src/swell/suites/localensembleda/workflow.py @@ -0,0 +1,169 @@ +# (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 executing JEDI-based LocalEnsembleDA Algorithm + +# -------------------------------------------------------------------------------------------------- + +[scheduler] + UTC mode = True + allow implicit tasks = False + +# -------------------------------------------------------------------------------------------------- + +[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) + + 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 247bee869..000000000 --- a/src/swell/suites/suite_questions.py +++ /dev/null @@ -1,57 +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() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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 5a53eec77..672d25f24 100644 --- a/src/swell/suites/ufo_testing/flow.cylc +++ b/src/swell/suites/ufo_testing/workflow.py @@ -4,6 +4,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 @@ -81,73 +92,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" - - [[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() + 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 944a43779..78d5af12e 100644 --- a/src/swell/swell.py +++ b/src/swell/swell.py @@ -9,6 +9,7 @@ import click +from ruamel.yaml import YAML from typing import Union, Optional, Literal from swell.deployment.platforms.platforms import get_platforms @@ -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 # -------------------------------------------------------------------------------------------------- @@ -82,12 +84,23 @@ def swell_driver() -> None: or for task-model combinations. """ +test_iteration_help = """ +(For diagnostic tasks only) - Set the number that is associated with the particular experiment +that is being compared. """ + +test_output_help = """ +(For diagnostic tasks only) - Define the output directory that diagnostics tests will send +their results. Used in comparison suites. """ + +cwd_help = """ +For task configs, set flag to create directory at the user's cwd, otherwise directory will be +created in default experiment_root.""" # -------------------------------------------------------------------------------------------------- @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', @@ -115,6 +128,46 @@ def create( # Create the experiment directory create_experiment_directory(suite, input_method, platform, override, advanced, slurm) +# -------------------------------------------------------------------------------------------------- + + +@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: Optional[str], + model: Optional[str], + input_method: str, + override: Optional[str], + slurm: Optional[str], + 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) # -------------------------------------------------------------------------------------------------- @@ -158,10 +211,14 @@ 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) def launch( suite_path: str, no_detach: bool, - log_path: str + log_path: str, + send_messages: bool, + pause_workflow: bool ) -> None: """ Launch an experiment with the cylc workflow manager @@ -172,7 +229,7 @@ def launch( suite_path (str): Path to where the flow.cylc and associated suite files are located. \n """ - launch_experiment(suite_path, no_detach, log_path) + launch_experiment(suite_path, no_detach, log_path, send_messages, pause_workflow) # -------------------------------------------------------------------------------------------------- @@ -189,7 +246,7 @@ def task( config: str, datetime: Optional[str], model: Optional[str], - ensemblePacket: Optional[str] + ensemblePacket: Optional[str], ) -> None: """ Run a workflow task @@ -224,7 +281,6 @@ def utility(utility: str) -> None: # -------------------------------------------------------------------------------------------------- - @swell_driver.command() @click.argument('test', type=click.Choice(valid_tests)) def test(test: str) -> None: 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 f28de8466..c71339287 100644 --- a/src/swell/tasks/base/task_base.py +++ b/src/swell/tasks/base/task_base.py @@ -21,7 +21,6 @@ # 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 @@ -48,6 +47,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:') @@ -191,9 +192,14 @@ 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 + 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.__datetime__.string_directory(), self.__model__) + else: + return self.experiment_path() # Return return cycle_dir @@ -315,7 +321,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) # -------------------------------------------------------------------------------------------------- @@ -332,7 +339,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 @@ -353,6 +360,7 @@ def task_wrapper( 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..1d5996cf4 --- /dev/null +++ b/src/swell/tasks/base/task_setup.py @@ -0,0 +1,419 @@ +# (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 + +# -------------------------------------------------------------------------------------------------- + +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 = [] + + # 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: + self.script = self.script.format(model=self.model) + 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 0723a3281..8ca95abfa 100644 --- a/src/swell/tasks/bufr_to_ioda.py +++ b/src/swell/tasks/bufr_to_ioda.py @@ -14,6 +14,8 @@ from ruamel.yaml import YAML, YAMLError 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 # -------------------------------------------------------------------------------------------------- @@ -34,6 +36,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 30221edf8..bcbaba933 100644 --- a/src/swell/tasks/clean_cycle.py +++ b/src/swell/tasks/clean_cycle.py @@ -9,12 +9,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.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/eva_comparison_increment.py b/src/swell/tasks/eva_comparison_increment.py index 8cfc16918..d791338e8 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 diff --git a/src/swell/tasks/eva_comparison_jedi_log.py b/src/swell/tasks/eva_comparison_jedi_log.py index 7c96a4155..6f8688633 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() 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 1f1b9a459..88dc73aa1 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() 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 b186a6a38..7113e23b9 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 59d936d20..068c3e817 100644 --- a/src/swell/tasks/get_background.py +++ b/src/swell/tasks/get_background.py @@ -9,11 +9,12 @@ from swell.tasks.base.task_base import taskBase -from swell.utilities.r2d2 import create_r2d2_config +from swell.tasks.base.task_setup import TaskSetup +from swell.tasks.base.task_attributes import task_attributes +import swell.configuration.question_defaults as qd import isodate import os -import r2d2 # -------------------------------------------------------------------------------------------------- @@ -26,6 +27,29 @@ # -------------------------------------------------------------------------------------------------- +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(), + qd.r2d2_local_path(), + ] + +# -------------------------------------------------------------------------------------------------- + + class GetBackground(taskBase): def execute(self) -> None: @@ -37,6 +61,10 @@ def execute(self) -> None: See the taskBase constructor for more information. """ + # Local import because module is not loaded until experiment launch + from swell.utilities.r2d2 import create_r2d2_config + import r2d2 + # Get duration into forecast for first background file # ---------------------------------------------------- bkg_steps = [] 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_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_geos_restart.py b/src/swell/tasks/get_geos_restart.py index db9175636..416f6783d 100644 --- a/src/swell/tasks/get_geos_restart.py +++ b/src/swell/tasks/get_geos_restart.py @@ -11,10 +11,29 @@ 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 copy_to_dst_dir, check_if_files_exist_in_path # -------------------------------------------------------------------------------------------------- +task_name = 'GetGeosRestart' + + +@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_restarts_directory() + ] + +# -------------------------------------------------------------------------------------------------- + class GetGeosRestart(taskBase): diff --git a/src/swell/tasks/get_geovals.py b/src/swell/tasks/get_geovals.py index a11b46221..8d110c1e4 100644 --- a/src/swell/tasks/get_geovals.py +++ b/src/swell/tasks/get_geovals.py @@ -11,16 +11,43 @@ 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.r2d2 import create_r2d2_config -from r2d2 import fetch # -------------------------------------------------------------------------------------------------- +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.r2d2_local_path(), + qd.window_length(), + ] + +# -------------------------------------------------------------------------------------------------- + + class GetGeovals(taskBase): def execute(self) -> None: + from r2d2 import fetch + # Parse config # ------------ geovals_experiment = self.config.geovals_experiment() 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 7178518a1..7d12c3b78 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 create_r2d2_config +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 = '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.r2d2_local_path(), + qd.window_length(), + ] # -------------------------------------------------------------------------------------------------- @@ -23,6 +46,11 @@ class GetNcdiags(taskBase): def execute(self) -> None: + # Import modules + # -------------- + from r2d2 import fetch + from swell.utilities.r2d2 import create_r2d2_config + # Parse config # ------------ ncdiag_experiments = self.config.ncdiag_experiments() 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 f8c561450..e26b309fb 100644 --- a/src/swell/tasks/get_observations.py +++ b/src/swell/tasks/get_observations.py @@ -8,15 +8,16 @@ # -------------------------------------------------------------------------------------------------- import isodate -import netCDF4 as nc import numpy as np import os -import r2d2 import shutil from typing import Union from datetime import timedelta, datetime as dt 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.r2d2 import create_r2d2_config from swell.utilities.datetime_util import datetime_formats from swell.utilities.observations import get_ioda_names_list, get_provider_for_observation @@ -31,6 +32,29 @@ # -------------------------------------------------------------------------------------------------- +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.r2d2_local_path(), + qd.window_length(), + ] + +# -------------------------------------------------------------------------------------------------- + class GetObservations(taskBase): @@ -98,6 +122,10 @@ def execute(self) -> None: "tlapse" files need to be fetched. """ + # # Local import because module is not loaded until experiment launch + # -------------- + import r2d2 + # Parse config # ------------ obs_experiment = self.config.obs_experiment() @@ -426,6 +454,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][:] @@ -451,6 +480,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/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 f7b163fc7..07930c39b 100644 --- a/src/swell/tasks/ingest_obs.py +++ b/src/swell/tasks/ingest_obs.py @@ -18,10 +18,34 @@ 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 +import swell.configuration.question_defaults as qd from swell.utilities.r2d2 import create_r2d2_config 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.r2d2_local_path(), + qd.window_length(), + # qd.window_offset(), + ] + +# -------------------------------------------------------------------------------------------------- + class IngestObs(taskBase): """Ingest observation files into R2D2 v3. 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_geos_output.py b/src/swell/tasks/link_geos_output.py index f2853728a..6f0cb3d28 100644 --- a/src/swell/tasks/link_geos_output.py +++ b/src/swell/tasks/link_geos_output.py @@ -12,11 +12,31 @@ 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() + ] # -------------------------------------------------------------------------------------------------- @@ -188,6 +208,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) @@ -210,6 +232,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 31851a6ef..953b907c5 100644 --- a/src/swell/tasks/move_da_restart.py +++ b/src/swell/tasks/move_da_restart.py @@ -13,10 +13,29 @@ from typing import Union 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 d4e297539..e42a09675 100644 --- a/src/swell/tasks/move_forecast_restart.py +++ b/src/swell/tasks/move_forecast_restart.py @@ -12,10 +12,27 @@ from typing import Union 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_geos_run_dir.py b/src/swell/tasks/prep_geos_run_dir.py index bd31ab729..d845088d4 100644 --- a/src/swell/tasks/prep_geos_run_dir.py +++ b/src/swell/tasks/prep_geos_run_dir.py @@ -15,10 +15,32 @@ from datetime import datetime as dt 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 copy_to_dst_dir, check_if_files_exist_in_path # -------------------------------------------------------------------------------------------------- +task_name = 'PrepGeosRunDir' + + +@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.existing_geos_gcm_build_path(), + qd.forecast_duration(), + qd.geos_experiment_directory(), + qd.mom6_iau_nhours() + ] + +# -------------------------------------------------------------------------------------------------- + class PrepGeosRunDir(taskBase): diff --git a/src/swell/tasks/prepare_analysis.py b/src/swell/tasks/prepare_analysis.py index 83c56861b..520c30ba5 100644 --- a/src/swell/tasks/prepare_analysis.py +++ b/src/swell/tasks/prepare_analysis.py @@ -15,6 +15,27 @@ 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(), + ] # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/remove_forecast_dir.py b/src/swell/tasks/remove_forecast_dir.py index a527e680d..05e968dc4 100644 --- a/src/swell/tasks/remove_forecast_dir.py +++ b/src/swell/tasks/remove_forecast_dir.py @@ -10,7 +10,19 @@ 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 +# -------------------------------------------------------------------------------------------------- + +task_name = 'RemoveForecastDir' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/tasks/render_jedi_observations.py b/src/swell/tasks/render_jedi_observations.py index 5808e1dfe..20763799d 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.set_obs_as_local(), + qd.window_length() + ] + +# -------------------------------------------------------------------------------------------------- + class RenderJediObservations(taskBase): diff --git a/src/swell/tasks/run_geos_executable.py b/src/swell/tasks/run_geos_executable.py index 88489986f..a2b84f64b 100644 --- a/src/swell/tasks/run_geos_executable.py +++ b/src/swell/tasks/run_geos_executable.py @@ -11,10 +11,23 @@ from typing import Optional 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.shell_commands import run_track_log_subprocess # -------------------------------------------------------------------------------------------------- +task_name = 'RunGeosExecutable' + + +@task_attributes.register(task_name) +class Setup(TaskSetup): + def set_defaults(self): + self.base_name = task_name + self.is_cycling = True + +# -------------------------------------------------------------------------------------------------- + class RunGeosExecutable(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 104a9a5dc..62338d55d 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,41 @@ 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'), + ] -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 3a398b454..7bc8f59de 100644 --- a/src/swell/tasks/run_jedi_ensemble_mean_variance.py +++ b/src/swell/tasks/run_jedi_ensemble_mean_variance.py @@ -12,11 +12,45 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediEnsembleMeanVariance(taskBase): diff --git a/src/swell/tasks/run_jedi_fgat_executable.py b/src/swell/tasks/run_jedi_fgat_executable.py index f65297508..d2bee09cc 100644 --- a/src/swell/tasks/run_jedi_fgat_executable.py +++ b/src/swell/tasks/run_jedi_fgat_executable.py @@ -11,11 +11,55 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + 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 8e9aee39e..76ac4208c 100644 --- a/src/swell/tasks/run_jedi_hofx_ensemble_executable.py +++ b/src/swell/tasks/run_jedi_hofx_ensemble_executable.py @@ -12,11 +12,50 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + 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 6d63cf5fc..dcc52ec6e 100644 --- a/src/swell/tasks/run_jedi_hofx_executable.py +++ b/src/swell/tasks/run_jedi_hofx_executable.py @@ -14,12 +14,48 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediHofxExecutable(taskBase): @@ -31,6 +67,8 @@ def execute(self, ensemble_members: Optional[list] = None) -> None: # --------------------- jedi_application = 'hofx' + from swell.utilities.netcdf_files import combine_files_without_groups + # Parse configuration # ------------------- window_type = self.config.window_type() 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 b0d67a5b8..d3ab510f2 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,66 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediLocalEnsembleDaExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_obsfilters_executable.py b/src/swell/tasks/run_jedi_obsfilters_executable.py index 7af6e4867..994f2be08 100644 --- a/src/swell/tasks/run_jedi_obsfilters_executable.py +++ b/src/swell/tasks/run_jedi_obsfilters_executable.py @@ -13,10 +13,50 @@ 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') + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediObsfiltersExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_ufo_tests_executable.py b/src/swell/tasks/run_jedi_ufo_tests_executable.py index 0613c5990..2882ae7fd 100644 --- a/src/swell/tasks/run_jedi_ufo_tests_executable.py +++ b/src/swell/tasks/run_jedi_ufo_tests_executable.py @@ -13,12 +13,39 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediUfoTestsExecutable(taskBase): diff --git a/src/swell/tasks/run_jedi_variational_executable.py b/src/swell/tasks/run_jedi_variational_executable.py index dc634cbd7..c03e6f263 100644 --- a/src/swell/tasks/run_jedi_variational_executable.py +++ b/src/swell/tasks/run_jedi_variational_executable.py @@ -11,11 +11,55 @@ 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'), + ] + +# -------------------------------------------------------------------------------------------------- + class RunJediVariationalExecutable(taskBase): diff --git a/src/swell/tasks/save_obs_diags.py b/src/swell/tasks/save_obs_diags.py index d03c6ba0c..9b80641cd 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.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 import create_r2d2_config 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.r2d2_local_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 + # Parse config # ------------ background_time_offset = self.config.background_time_offset() diff --git a/src/swell/tasks/save_restart.py b/src/swell/tasks/save_restart.py index c251cebe1..4abd0ea50 100644 --- a/src/swell/tasks/save_restart.py +++ b/src/swell/tasks/save_restart.py @@ -10,12 +10,34 @@ from datetime import datetime as dt import isodate import os -from r2d2 import store 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.file_system_operations import copy_to_dst_dir -from swell.utilities.r2d2 import create_r2d2_config + +# -------------------------------------------------------------------------------------------------- + +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 + self.questions = [ + qd.window_length(), + qd.window_type(), + qd.background_time_offset(), + qd.forecast_duration(), + qd.horizontal_resolution(), + qd.marine_models(), + qd.r2d2_local_path() + ] # -------------------------------------------------------------------------------------------------- @@ -30,6 +52,9 @@ def execute(self): Does not handle 4d backgrounds properly """ + from swell.utilities.r2d2 import create_r2d2_config + from r2d2 import store + # Parse config window_type = self.config.window_type() window_length = self.config.window_length() diff --git a/src/swell/tasks/stage_jedi.py b/src/swell/tasks/stage_jedi.py index a9baa19ab..265ec80e4 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,36 @@ # -------------------------------------------------------------------------------------------------- +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.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/store_background.py b/src/swell/tasks/store_background.py index 43d9095e4..34cad4abb 100644 --- a/src/swell/tasks/store_background.py +++ b/src/swell/tasks/store_background.py @@ -11,16 +11,37 @@ from datetime import datetime as dt import isodate import os -from r2d2 import store 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.r2d2 import create_r2d2_config # -------------------------------------------------------------------------------------------------- +task_name = 'StoreBackground' + + +@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_experiment(), + qd.background_frequency(), + qd.horizontal_resolution(), + qd.r2d2_local_path(), + ] + +# -------------------------------------------------------------------------------------------------- + class StoreBackground(taskBase): @@ -34,6 +55,9 @@ def execute(self) -> None: See the taskBase constructor for more information. """ + from r2d2 import store + from swell.utilities.r2d2 import create_r2d2_config + # Current cycle time object # ------------------------- current_cycle_dto = dt.strptime(self.cycle_time(), datetime_formats['iso_format']) diff --git a/src/swell/tasks/task_questions.py b/src/swell/tasks/task_questions.py deleted file mode 100644 index 09868a61e..000000000 --- a/src/swell/tasks/task_questions.py +++ /dev/null @@ -1,772 +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() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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(), - qd.r2d2_local_path(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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.r2d2_local_path(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGeosAdasBackground = QuestionList( - list_name="GetGeosAdasBackground", - questions=[ - qd.path_to_geos_adas_background() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetGeosRestart = QuestionList( - list_name="GetGeosRestart", - questions=[ - swell_static_file_questions, - qd.geos_restarts_directory() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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.r2d2_local_path(), - qd.window_length(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - GetObservations = QuestionList( - list_name="GetObservations", - questions=[ - background_crtm_obs, - qd.cycling_varbc(), - qd.obs_experiment(), - qd.observing_system_records_path(), - qd.r2d2_local_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.r2d2_local_path(), - qd.window_length(), - # qd.window_offset(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - JediLogComparison = QuestionList( - list_name="JediComparisonLog", - questions=[ - qd.comparison_log_type(), - qd.number_of_iterations() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - JediOopsLogParser = QuestionList( - list_name="JediOopsLogParser", - questions=[ - qd.comparison_log_type(), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - LinkGeosOutput = QuestionList( - list_name="LinkGeosOutput", - questions=[ - window_questions, - qd.background_frequency(), - qd.marine_models() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - MoveDaRestart = QuestionList( - list_name="MoveDaRestart", - questions=[ - qd.mom6_iau(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - MoveForecastRestart = QuestionList( - list_name="MoveForecastRestart", - questions=[ - qd.forecast_duration() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PrepareAnalysis = QuestionList( - list_name="PrepareAnalysis", - questions=[ - qd.analysis_variables(), - qd.mom6_iau(), - qd.total_processors(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - PrepGeosRunDir = QuestionList( - list_name="PrepGeosRunDir", - questions=[ - swell_static_file_questions, - qd.existing_geos_gcm_build_path(), - qd.forecast_duration(), - qd.geos_experiment_directory(), - 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.set_obs_as_local(), - qd.window_length() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediFgatExecutable = QuestionList( - list_name="RunJediFgatExecutable", - questions=[ - run_jedi_executable, - qd.marine_models(), - qd.comparison_log_type('fgat') - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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') - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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') - ] - ) - - # -------------------------------------------------------------------------------------------------- - - 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'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - RunJediVariationalExecutable = QuestionList( - list_name="RunJediVariationalExecutable", - questions=[ - run_jedi_executable, - qd.perhost(), - qd.comparison_log_type('variational'), - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveObsDiags = QuestionList( - list_name="SaveObsDiags", - questions=[ - background_crtm_obs, - qd.window_length(), - qd.r2d2_local_path(), - qd.marine_models() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - SaveRestart = QuestionList( - list_name="SaveRestart", - questions=[ - window_questions, - qd.background_time_offset(), - qd.forecast_duration(), - qd.horizontal_resolution(), - qd.marine_models(), - qd.r2d2_local_path() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - StageJedi = QuestionList( - list_name="StageJedi", - questions=[ - swell_static_file_questions, - qd.gsibec_configuration(), - qd.gsibec_nlats(), - qd.gsibec_nlons(), - qd.horizontal_resolution(), - qd.vertical_resolution() - ] - ) - - # -------------------------------------------------------------------------------------------------- - - StoreBackground = QuestionList( - list_name="StoreBackground", - questions=[ - window_questions, - qd.background_experiment(), - qd.background_frequency(), - qd.horizontal_resolution(), - qd.r2d2_local_path(), - ] - ) - - # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/test/code_tests/code_tests.py b/src/swell/test/code_tests/code_tests.py index ff3e4e4c2..c77c6766d 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 # -------------------------------------------------------------------------------------------------- @@ -40,8 +40,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/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/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/dictionary.py b/src/swell/utilities/dictionary.py index e5d50f324..594fd8500 100644 --- a/src/swell/utilities/dictionary.py +++ b/src/swell/utilities/dictionary.py @@ -7,9 +7,9 @@ # -------------------------------------------------------------------------------------------------- +from collections.abc import Hashable, Mapping import io from ruamel.yaml import YAML -from collections.abc import Hashable from typing import Union from swell.utilities.logger import Logger @@ -180,3 +180,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/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 bf11de750..000000000 --- a/src/swell/utilities/question_defaults.py +++ /dev/null @@ -1,1464 +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 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 runahead_limit(SuiteQuestion): - default_value: str = "P4" - question_name: str = "runahead_limit" - ask_question: bool = True - prompt: str = ("Since this suite is non-cycling choose how " - "many hours the workflow can run ahead?") - 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 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 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 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_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" - ask_question: bool = True - prompt: str = "Which GEOS tag do you wish to clone?" - 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 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 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_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 r2d2_local_path(TaskQuestion): - default_value: str = "defer_to_platform" - question_name: str = "r2d2_local_path" - prompt: str = "What is the path to the R2D2 local directory?" - widget_type: WType = WType.STRING - - # -------------------------------------------------------------------------------------------------- - - @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 set_obs_as_local(TaskQuestion): - default_value: bool = False - question_name: str = "set_obs_as_local" - options: List[bool] = mutable_field([ - True, - False - ]) - models: List[str] = mutable_field([ - 'all_models' - ]) - prompt: str = "Treat observations as 'local' to the directory?" - 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" - 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 - -# -------------------------------------------------------------------------------------------------- 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/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/slurm.py b/src/swell/utilities/slurm.py index 3853e223d..d9853a0e1 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,147 +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', - 'RunJediEnsembleMeanVariance', - 'RunJediConvertStateSoca2ciceExecutable', - 'RunJediFgatExecutable', - 'RunJediHofxEnsembleExecutable', - 'RunJediHofxExecutable', - 'RunJediLocalEnsembleDaExecutable', - 'RunJediObsfiltersExecutable', - 'RunJediUfoTestsExecutable', - 'RunJediVariationalExecutable', - 'RunGeosExecutable' - } - - # 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: @@ -206,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( @@ -219,14 +130,23 @@ 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 = yaml.load(yaml_file) + user_globals['slurm_directives_global'] = yaml.safe_load(yaml_file) + ''' + 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 4e9c8a144..5520a4abb 100644 --- a/src/swell/utilities/suite_utils.py +++ b/src/swell/utilities/suite_utils.py @@ -8,72 +8,20 @@ # -------------------------------------------------------------------------------------------------- -import glob import os -import importlib from swell.swell_path import get_swell_path # -------------------------------------------------------------------------------------------------- -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')))]) - - return suites - -# -------------------------------------------------------------------------------------------------- - - -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 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 + # Path to model interfaces + interface_directory = os.path.join(get_swell_path(), 'configuration', 'jedi', 'interfaces') + # Get models + return os.listdir(interface_directory) # -------------------------------------------------------------------------------------------------- diff --git a/src/swell/utilities/swell_questions.py b/src/swell/utilities/swell_questions.py index ec1b5728a..049f9a639 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 dataclasses import dataclass, asdict from typing import List, Optional, Self, Union, Literal -from enum import Enum +from enum import Enum, StrEnum from isodate import parse_datetime, parse_duration, ISO8601Error 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" @@ -100,6 +109,7 @@ class SwellQuestion: question_name: str widget_type: WidgetType prompt: str + models: Optional[list] = None question_type: str = None ask_question: bool = False options: Optional[str] = None @@ -107,29 +117,14 @@ class SwellQuestion: # -------------------------------------------------------------------------------------------------- -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_ - -# -------------------------------------------------------------------------------------------------- - - @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[Union[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([]) # -------------------------------------------------------------------------------------------------- @@ -163,7 +158,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 @@ -177,7 +172,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) @@ -189,14 +184,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 # --------------------------------------------------------------------------------------------------