diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index f7bc146b..a76db4e7 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -66,6 +66,11 @@ For the ``mpas_analysis`` task: * ``reference_data_path`` and ``test_data_path`` are optional and are only used for model-vs-model comparisons. If provided, ``zppy`` uses them to locate the MPAS-Analysis config files from a *previous* MPAS-Analysis run and passes those through to MPAS-Analysis as ``controlRunConfigFile`` (reference) and ``mainRunConfigFile`` (test). + The comparison type of the *current* MPAS-Analysis run is inferred implicitly: + if ``reference_data_path`` is set, the run is treated as model-vs-model (``mvm``); + otherwise it is treated as model-vs-observations (``mvo``). Users normally do + not need to set a comparison type for the current run. + .. note:: These parameter names are intentionally consistent with the terminology used by ``e3sm_diags`` for model-vs-model runs: in both cases, ``reference_data_path`` identifies the *reference simulation's zppy-generated outputs*. @@ -74,7 +79,16 @@ For the ``mpas_analysis`` task: For MPAS-Analysis, ``zppy`` resolves the config file when ``reference_data_path`` points to the prior run's zppy output directory (the one containing ``post/``). ``reference_data_path`` is intended to point to the prior run's zppy output directory (the one containing ``post/``). ``zppy`` will then use: - ``/post/analysis/mpas_analysis/cfg/mpas_analysis_.cfg`` (or ``mpas_analysis_mvm`` if the referenced run was MVM). + ``/post/analysis/mpas_analysis//cfg/mpas_analysis_.cfg`` + where ```` is ``mvo`` or ``mvm``. + + For referenced prior runs, ``reference_comparison_type`` and + ``test_comparison_type`` can be set to ``"auto"``, ``"mvo"``, or ``"mvm"``. + The default is ``"auto"``. In auto mode: + + * if the path points to ``[[subsection]]``, zppy uses the referenced subsection's actual comparison type + * if the path points to an external zppy output directory, zppy looks for the matching cfg under ``mvo`` and ``mvm`` + * if both exist for the same identifier, zppy raises an error and the user should set ``reference_comparison_type`` or ``test_comparison_type`` explicitly When ``reference_data_path`` is set to a non-subsection path, ``reference_case`` is required so the MVM output directory can include the reference case name. If ``reference_data_path`` is set to ``[[subsection]]``, ``reference_case`` is inferred to be the same as the current ``case``. @@ -122,4 +136,4 @@ In ``e3sm_diags.py``: * ``check_mvm_only_parameters_for_bash``: similar, but these are specifically parameters used for model-vs-model runs. Uses ``check_parameter_defined`` in addition to ``check_set_specific_parameter``. * ``check_and_define_parameters``: make sure all parameters are defined, using ``utils.py get_value_from_parameter``, ``utils.py set_value_of_parameter_if_undefined``, and ``check_mvm_only_parameters_for_bash``. -``check_parameters_for_bash`` can be run immediately for each subtask because it has very few conditions. Other checks are included in ``check_and_define_parameters`` later on in the code. \ No newline at end of file +``check_parameters_for_bash`` can be run immediately for each subtask because it has very few conditions. Other checks are included in ``check_and_define_parameters`` later on in the code. diff --git a/docs/source/post.mpas_analysis_model_vs_model.cfg b/docs/source/post.mpas_analysis_model_vs_model.cfg index fe9569cc..07f37803 100644 --- a/docs/source/post.mpas_analysis_model_vs_model.cfg +++ b/docs/source/post.mpas_analysis_model_vs_model.cfg @@ -22,9 +22,14 @@ enso_years = "1850-2014", # Point at a *previous zppy run output directory* for the reference simulation. # zppy will locate the matching MPAS-Analysis cfg file under: -# /post/analysis/mpas_analysis/cfg/ +# /post/analysis/mpas_analysis//cfg/ reference_data_path = +# Optional: override how zppy resolves the referenced prior run. +# Usually "auto" is sufficient. Set this only if both mvo and mvm outputs exist +# for the same year range under the referenced run. +# reference_comparison_type = "auto" + # Required when reference_data_path is not a [[subsection]]. # Used to build the MVM output directory name. reference_case = @@ -34,6 +39,7 @@ reference_case = # If set to [[subsection]], zppy will use that subsection's year ranges # when ts_years/climo_years/enso_years are not provided. # test_data_path = +# test_comparison_type = "auto" # Optional: override the reference year ranges (defaults to the test ranges). # If reference_data_path points to a prior [mpas_analysis] subsection using diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 536dfc1c..e4144f24 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -74,6 +74,10 @@ Unlike ``e3sm_diags`` (where ``run_type = "model_vs_model"`` and ``reference_dat points directly at reference climatology output), MPAS-Analysis comparisons are driven by MPAS-Analysis config files. For model-vs-model mode, ``zppy`` locates the matching config file(s) from prior MPAS-Analysis output and passes them to MPAS-Analysis. +The current run's type is inferred automatically: setting ``reference_data_path`` +makes it an ``mvm`` run, otherwise it is an ``mvo`` run. If a referenced prior +run could resolve to either ``mvo`` or ``mvm``, use ``reference_comparison_type`` +or ``test_comparison_type`` to disambiguate. .. literalinclude:: post.mpas_analysis_model_vs_model.cfg :language: cfg diff --git a/tests/integration/generated/test_weekly_comprehensive_v3_chrysalis.cfg b/tests/integration/generated/test_weekly_comprehensive_v3_chrysalis.cfg index a7aeff11..80c788b8 100644 --- a/tests/integration/generated/test_weekly_comprehensive_v3_chrysalis.cfg +++ b/tests/integration/generated/test_weekly_comprehensive_v3_chrysalis.cfg @@ -201,17 +201,28 @@ tc_obs = "/lcrc/group/e3sm/diagnostics/observations/Atm/tc-analysis/" [mpas_analysis] active = True anomalyRefYear = 1985 -climo_years = "1985-1989", "1990-1995", -enso_years = "1985-1989", "1990-1995", environment_commands = "source /conda.sh; conda activate " mesh = "IcoswISC30E3r5" parallelTaskCount = 6 partition = "compute" qos = "regular" shortTermArchive = True -ts_years = "1985-1989", "1985-1995", walltime = "00:30:00" + [[ reference ]] + ts_years = "1985-1989", + climo_years = "1985-1989", + enso_years = "1985-1989", + + [[ test ]] + ts_years = "1985-1995", + climo_years = "1990-1995", + enso_years = "1990-1995", + + [[ mvm ]] + reference_data_path = [[ reference ]] + test_data_path = [[ test ]] + [global_time_series] active = True climo_years = "1985-1989", "1990-1995", @@ -219,6 +230,7 @@ environment_commands = "source /conda.sh; conda activate < experiment_name = "v3.LR.historical_0051" figstr = "v3.LR.historical_0051" #moc_file=mocTimeSeries_1985-1995.nc +mpas_analysis_subsections = "reference", "test", # plots_lnd = "FSH,RH2M,LAISHA,LAISUN,QINTR,QOVER,QRUNOFF,QSOIL,QVEGE,QVEGT,SOILWATER_10CM,TSA,H2OSNO,TOTLITC,CWDC,SOIL1C,SOIL2C,SOIL3C,SOIL4C,WOOD_HARVESTC,TOTVEGC,NBP,GPP,AR,HR" ts_num_years = 5 ts_years = "1985-1989", "1985-1995", diff --git a/tests/integration/image_checker.py b/tests/integration/image_checker.py index 13173fff..9327a459 100644 --- a/tests/integration/image_checker.py +++ b/tests/integration/image_checker.py @@ -185,10 +185,7 @@ def _check_mismatched_images( print(f"Reading expected images file {parameters.expected_images_list}") for line in f: image_name = line.strip("./").strip("\n") - proceed = False - if image_name.startswith(prefix): - proceed = True - if proceed: + if image_name.startswith(f"{prefix}/"): counter += 1 if counter % 250 == 0: print("On line #", counter) diff --git a/tests/integration/template_weekly_comprehensive_v3.cfg b/tests/integration/template_weekly_comprehensive_v3.cfg index fac286cb..b9c1e0ab 100644 --- a/tests/integration/template_weekly_comprehensive_v3.cfg +++ b/tests/integration/template_weekly_comprehensive_v3.cfg @@ -230,6 +230,7 @@ environment_commands = "#expand global_time_series_environment_commands#" experiment_name = "#expand case_name#" figstr = "#expand case_name#" #moc_file=mocTimeSeries_1985-1995.nc +mpas_analysis_subsections = "reference", "test", # plots_lnd = "FSH,RH2M,LAISHA,LAISUN,QINTR,QOVER,QRUNOFF,QSOIL,QVEGE,QVEGT,SOILWATER_10CM,TSA,H2OSNO,TOTLITC,CWDC,SOIL1C,SOIL2C,SOIL3C,SOIL4C,WOOD_HARVESTC,TOTVEGC,NBP,GPP,AR,HR" ts_num_years = 5 ts_years = "1985-1989", "1985-1995", diff --git a/tests/test_zppy_global_time_series.py b/tests/test_zppy_global_time_series.py index 6be322ce..808a2635 100644 --- a/tests/test_zppy_global_time_series.py +++ b/tests/test_zppy_global_time_series.py @@ -1,8 +1,16 @@ +import os from typing import Any, Dict, List import pytest +from configobj import ConfigObj +from validate import Validator from zppy.global_time_series import determine_and_add_dependencies, determine_components +from zppy.mpas_analysis import ( + _get_referenced_comparison_type, + _resolve_mpas_analysis_config_file, + get_mpas_analysis_prefixes, +) def test_determine_components(): @@ -123,6 +131,28 @@ def test_determine_and_add_dependencies(): expected = ["script_dir/mpas_analysis_ts_1980-1989_climo_1980-1989.status"] assert dependencies == expected + c = { + "use_atm": False, + "use_lnd": False, + "use_ocn": True, + "mpas_analysis_subsections": ["reference", "test"], + } + dependencies = [] + determine_and_add_dependencies( + c, + dependencies, + "script_dir", + mpas_analysis_prefixes={ + "reference": ["mpas_analysis_reference_ts_1980-1989_climo_1980-1989"], + "test": ["mpas_analysis_test_ts_1980-1999_climo_1990-1999"], + }, + ) + expected = [ + "script_dir/mpas_analysis_reference_ts_1980-1989_climo_1980-1989.status", + "script_dir/mpas_analysis_test_ts_1980-1999_climo_1990-1999.status", + ] + assert dependencies == expected + c = { "use_atm": False, "use_lnd": False, @@ -144,3 +174,157 @@ def test_determine_and_add_dependencies(): dependencies = [] with pytest.raises(Exception): determine_and_add_dependencies(c, dependencies, "script_dir") + + c = { + "use_atm": False, + "use_lnd": False, + "use_ocn": True, + "mpas_analysis_subsections": ["missing"], + } + dependencies = [] + with pytest.raises(ValueError): + determine_and_add_dependencies( + c, + dependencies, + "script_dir", + mpas_analysis_prefixes={ + "reference": ["mpas_analysis_reference_ts_1980-1989_climo_1980-1989"] + }, + ) + + +def test_get_mpas_analysis_prefixes(tmp_path): + config_path = tmp_path / "mpas_analysis.cfg" + config_path.write_text( + """ +[default] +case = "case_name" +input = "input_dir" +output = "output_dir" +www = "www_dir" + +[mpas_analysis] +active = True +mesh = "EC30to60E2r2" + + [[ reference ]] + ts_years = "1985-1989", + climo_years = "1985-1989", + enso_years = "1985-1989", + + [[ test ]] + ts_years = "1985-1995", + climo_years = "1990-1995", + enso_years = "1990-1995", + + [[ mvm ]] + reference_data_path = [[ reference ]] + test_data_path = [[ test ]] +""".strip() + ) + + config = ConfigObj( + str(config_path), configspec=os.path.join("zppy", "defaults", "default.ini") + ) + validator = Validator() + assert config.validate(validator) + + prefixes = get_mpas_analysis_prefixes(config) + + assert prefixes == { + "reference": ["mpas_analysis_reference_ts_1985-1989_climo_1985-1989"], + "test": ["mpas_analysis_test_ts_1985-1995_climo_1990-1995"], + "mvm": [ + "mpas_analysis_mvm_ts_1985-1995_climo_1990-1995_vs_ref_ts_1985-1989_climo_1985-1989" + ], + } + + +def test_resolve_mpas_analysis_config_file_uses_comparison_type(tmp_path): + config_file = _resolve_mpas_analysis_config_file( + str(tmp_path), "ts_1985-1989_climo_1985-1989", comparison_type="mvm" + ) + + assert config_file == os.path.join( + str(tmp_path.resolve()), + "post", + "analysis", + "mpas_analysis", + "mvm", + "cfg", + "mpas_analysis_ts_1985-1989_climo_1985-1989.cfg", + ) + + +def test_get_referenced_comparison_type_infers_from_subsection(): + comparison_type = _get_referenced_comparison_type( + "auto", + "/unused", + "reference", + "ts_1985-1989_climo_1985-1989", + {"reference": "mvm"}, + "reference_data_path", + ) + + assert comparison_type == "mvm" + + +def test_get_referenced_comparison_type_raises_on_subsection_mismatch(): + with pytest.raises(ValueError, match="which is a mvo run, not mvm"): + _get_referenced_comparison_type( + "mvm", + "/unused", + "reference", + "ts_1985-1989_climo_1985-1989", + {"reference": "mvo"}, + "reference_data_path", + ) + + +def test_get_referenced_comparison_type_auto_prefers_mvo_if_cfg_missing(tmp_path): + comparison_type = _get_referenced_comparison_type( + "auto", + str(tmp_path), + "", + "ts_1985-1989_climo_1985-1989", + {}, + "reference_data_path", + ) + + assert comparison_type == "mvo" + + +def test_get_referenced_comparison_type_auto_detects_mvm(tmp_path): + cfg_dir = tmp_path / "post" / "analysis" / "mpas_analysis" / "mvm" / "cfg" + cfg_dir.mkdir(parents=True) + (cfg_dir / "mpas_analysis_ts_1985-1989_climo_1985-1989.cfg").write_text("") + + comparison_type = _get_referenced_comparison_type( + "auto", + str(tmp_path), + "", + "ts_1985-1989_climo_1985-1989", + {}, + "reference_data_path", + ) + + assert comparison_type == "mvm" + + +def test_get_referenced_comparison_type_auto_raises_if_ambiguous(tmp_path): + for comparison_type in ["mvo", "mvm"]: + cfg_dir = ( + tmp_path / "post" / "analysis" / "mpas_analysis" / comparison_type / "cfg" + ) + cfg_dir.mkdir(parents=True, exist_ok=True) + (cfg_dir / "mpas_analysis_ts_1985-1989_climo_1985-1989.cfg").write_text("") + + with pytest.raises(ValueError, match="reference_comparison_type"): + _get_referenced_comparison_type( + "auto", + str(tmp_path), + "", + "ts_1985-1989_climo_1985-1989", + {}, + "reference_data_path", + ) diff --git a/zppy/defaults/default.ini b/zppy/defaults/default.ini index 768a3ce7..a7739e3e 100755 --- a/zppy/defaults/default.ini +++ b/zppy/defaults/default.ini @@ -323,11 +323,16 @@ enso_years = string_list(default=list("")) # Set these to the output directory of a prior zppy run (the one containing post/), # or to [[ subsection ]] to refer to a previous mpas_analysis subsection in this workflow. # zppy will use: -# /post/analysis/mpas_analysis/cfg/mpas_analysis_.cfg -# (or /post/analysis/mpas_analysis_mvm/cfg/ if the referenced run was MVM) +# /post/analysis/mpas_analysis//cfg/mpas_analysis_.cfg # where matches each MPAS-Analysis sub-run (e.g. ts_1850-2014_climo_1985-2014). +# comparison_type for the current run is inferred implicitly: +# if reference_data_path is set, it is an mvm run; otherwise it is an mvo run. +# These options are only for resolving referenced prior runs. In auto mode, zppy +# prefers mvo unless it finds only mvm; if both exist, it raises an error. reference_data_path = string(default="") test_data_path = string(default="") +reference_comparison_type = option("auto", "mvo", "mvm", default="auto") +test_comparison_type = option("auto", "mvo", "mvm", default="auto") # Required when reference_data_path is set to a non-subsection path. # If reference_data_path is set to [[subsection]], reference_case is inferred # to be the same as the current case. @@ -366,6 +371,8 @@ walltime = string(default="06:00:00") enso_years = string_list(default=None) reference_data_path = string(default=None) test_data_path = string(default=None) + reference_comparison_type = option("auto", "mvo", "mvm", default=None) + test_comparison_type = option("auto", "mvo", "mvm", default=None) reference_case = string(default=None) ref_ts_years = string_list(default=None) ref_climo_years = string_list(default=None) @@ -404,6 +411,9 @@ ts_years = string_list(default=list("")) # NOTE: always overrides value in [default] input_subdir = string(default="archive/ocn/hist") moc_file = string(default="") +# Name of the `[mpas_analysis]` subtasks to depend on +# Leave empty if no subtasks are defined for mpas_analysis +mpas_analysis_subsections = string_list(default=list("")) # nrows, ncols are DEPRECATED. Specifying them will have no effect. # Number of columns per page ncols = integer(default=2) @@ -437,6 +447,7 @@ plots_ocn = string(default="") input_subdir = string(default=None) make_viewer = boolean(default=None) moc_file = string(default=None) + mpas_analysis_subsections = string_list(default=None) ncols = integer(default=None) nrows = integer(default=None) plots_original = string(default=None) diff --git a/zppy/global_time_series.py b/zppy/global_time_series.py index 94cb9826..2f1de145 100644 --- a/zppy/global_time_series.py +++ b/zppy/global_time_series.py @@ -1,8 +1,9 @@ import os -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from zppy.bundle import handle_bundles from zppy.logger import _setup_custom_logger +from zppy.mpas_analysis import get_mpas_analysis_identifier, get_mpas_analysis_prefixes from zppy.utils import ( add_dependencies, check_status, @@ -23,6 +24,7 @@ def global_time_series(config, script_dir, existing_bundles, job_ids_file): template, template_env = initialize_template(config, "global_time_series.bash") + mpas_analysis_prefixes = get_mpas_analysis_prefixes(config) # --- List of global_time_series tasks --- tasks: List[Dict[str, Any]] = get_tasks(config, "global_time_series") @@ -59,7 +61,9 @@ def global_time_series(config, script_dir, existing_bundles, job_ids_file): # List of dependencies dependencies: List[str] = [] # Add Global Time Series dependencies - determine_and_add_dependencies(c, dependencies, script_dir) + determine_and_add_dependencies( + c, dependencies, script_dir, mpas_analysis_prefixes + ) c["dependencies"] = dependencies write_settings_file(settings_file, c, s) export = "NONE" @@ -144,7 +148,10 @@ def determine_components(c: Dict[str, Any]) -> None: def determine_and_add_dependencies( - c: Dict[str, Any], dependencies: List[str], script_dir: str + c: Dict[str, Any], + dependencies: List[str], + script_dir: str, + mpas_analysis_prefixes: Optional[Dict[str, List[str]]] = None, ) -> None: if c["use_atm"]: # Iterate from year1 to year2 incrementing by the number of years per time series file. @@ -174,19 +181,43 @@ def determine_and_add_dependencies( c["ts_num_years"], ) if c["use_ocn"]: + mpas_analysis_prefixes = ( + {} if mpas_analysis_prefixes is None else mpas_analysis_prefixes + ) + mpas_sub_list = _get_mpas_analysis_subsections(c) + if len(mpas_sub_list) > 0: + for mpas_sub in mpas_sub_list: + if mpas_sub not in mpas_analysis_prefixes: + raise ValueError( + f'global_time_series mpas_analysis_subsections contains "{mpas_sub}", ' + "but no such mpas_analysis subsection was found." + ) + for prefix in mpas_analysis_prefixes[mpas_sub]: + dependencies.append(os.path.join(script_dir, f"{prefix}.status")) + return + # Add MPAS Analysis dependencies ts_year_sets = get_years(c["ts_years"]) climo_year_sets = get_years(c["climo_years"]) if (not ts_year_sets) or (not climo_year_sets): raise Exception("ts_years and climo_years must both be set for ocn plots.") for ts_year_set, climo_year_set in zip(ts_year_sets, climo_year_sets): - c["ts_year1"] = ts_year_set[0] - c["ts_year2"] = ts_year_set[1] - c["climo_year1"] = climo_year_set[0] - c["climo_year2"] = climo_year_set[1] + identifier = get_mpas_analysis_identifier( + ts_year1=ts_year_set[0], + ts_year2=ts_year_set[1], + climo_year1=climo_year_set[0], + climo_year2=climo_year_set[1], + ) dependencies.append( - os.path.join( - script_dir, - f"mpas_analysis_ts_{c['ts_year1']:04d}-{c['ts_year2']:04d}_climo_{c['climo_year1']:04d}-{c['climo_year2']:04d}.status", - ) + os.path.join(script_dir, f"mpas_analysis_{identifier}.status") ) + + +def _get_mpas_analysis_subsections(c: Dict[str, Any]) -> List[str]: + mpas_analysis_subsections_input = c.get("mpas_analysis_subsections", []) + if isinstance(mpas_analysis_subsections_input, str): + if mpas_analysis_subsections_input == "": + return [] + # This will be the case if mpas_analysis_subsections is missing a trailing comma + return [mpas_analysis_subsections_input] + return [subsection for subsection in mpas_analysis_subsections_input if subsection] diff --git a/zppy/mpas_analysis.py b/zppy/mpas_analysis.py index 21e76f56..be6b606d 100644 --- a/zppy/mpas_analysis.py +++ b/zppy/mpas_analysis.py @@ -42,8 +42,8 @@ def mpas_analysis(config: ConfigObj, script_dir: str, existing_bundles, job_ids_ prior_subsection_outputs: Dict[str, str] = {} # Track year sets for previously defined subsections so later tasks can reference them. prior_subsection_year_sets: Dict[str, Dict[str, List[Tuple[int, int]]]] = {} - # Track analysis subdirectories (mpas_analysis vs mpas_analysis_mvm) for subsections. - prior_subsection_analysis_subdirs: Dict[str, str] = {} + # Track comparison types (mvo vs mvm) for subsections. + prior_subsection_comparison_types: Dict[str, str] = {} # Track identifiers used by MVM runs to avoid cfg name collisions. mvm_identifiers: Dict[str, str] = {} @@ -79,11 +79,21 @@ def mpas_analysis(config: ConfigObj, script_dir: str, existing_bundles, job_ids_ identifier, ref_identifier = _set_identifiers( c, script_dir, ctrl_ts, ctrl_climo ) - reference_analysis_subdir = _get_subsection_analysis_subdir( - reference_subsection, prior_subsection_analysis_subdirs + reference_comparison_type = _get_referenced_comparison_type( + c.get("reference_comparison_type", "auto"), + reference_data_path, + reference_subsection, + ref_identifier, + prior_subsection_comparison_types, + "reference_data_path", ) - test_analysis_subdir = _get_subsection_analysis_subdir( - test_subsection, prior_subsection_analysis_subdirs + test_comparison_type = _get_referenced_comparison_type( + c.get("test_comparison_type", "auto"), + test_data_path, + test_subsection, + identifier, + prior_subsection_comparison_types, + "test_data_path", ) _set_run_config_files( c, @@ -91,8 +101,8 @@ def mpas_analysis(config: ConfigObj, script_dir: str, existing_bundles, job_ids_ test_data_path, ref_identifier, identifier, - reference_analysis_subdir, - test_analysis_subdir, + reference_comparison_type, + test_comparison_type, ) _set_output_paths( c, @@ -163,14 +173,105 @@ def mpas_analysis(config: ConfigObj, script_dir: str, existing_bundles, job_ids_ "climo": climo_year_sets, "enso": enso_year_sets, } - prior_subsection_analysis_subdirs[c["subsection"]] = c.get( - "analysis_subdir", "mpas_analysis" - ) + prior_subsection_comparison_types[c["subsection"]] = c["comparison_type"] return existing_bundles -def _get_identifier( +def get_mpas_analysis_prefixes(config: ConfigObj) -> Dict[str, List[str]]: + tasks: List[Dict[str, Any]] = get_tasks(config, "mpas_analysis") + if len(tasks) == 0: + return {} + + prior_subsection_outputs: Dict[str, str] = {} + prior_subsection_year_sets: Dict[str, Dict[str, List[Tuple[int, int]]]] = {} + prior_subsection_comparison_types: Dict[str, str] = {} + prefixes_by_subsection: Dict[str, List[str]] = {} + + for c in tasks: + set_subdirs(config, c) + ( + reference_data_path, + test_data_path, + reference_subsection, + test_subsection, + ) = _resolve_subsection_paths(c, prior_subsection_outputs) + ts_year_sets, climo_year_sets, enso_year_sets = _resolve_test_year_sets( + c, test_subsection, prior_subsection_year_sets + ) + ref_ts_year_sets, ref_climo_year_sets, ref_enso_year_sets = ( + _resolve_reference_year_sets( + c, reference_subsection, prior_subsection_year_sets, ts_year_sets + ) + ) + + prefixes: List[str] = [] + for ts, climo, enso, ctrl_ts, ctrl_climo, ctrl_enso in zip( + ts_year_sets, + climo_year_sets, + enso_year_sets, + ref_ts_year_sets, + ref_climo_year_sets, + ref_enso_year_sets, + ): + if _set_run_years(c, ts, climo, enso): + continue + + identifier, ref_identifier = _set_identifiers( + c, script_dir="", ctrl_ts=ctrl_ts, ctrl_climo=ctrl_climo + ) + reference_comparison_type = _get_referenced_comparison_type( + c.get("reference_comparison_type", "auto"), + reference_data_path, + reference_subsection, + ref_identifier, + prior_subsection_comparison_types, + "reference_data_path", + ) + test_comparison_type = _get_referenced_comparison_type( + c.get("test_comparison_type", "auto"), + test_data_path, + test_subsection, + identifier, + prior_subsection_comparison_types, + "test_data_path", + ) + _set_run_config_files( + c, + reference_data_path, + test_data_path, + ref_identifier, + identifier, + reference_comparison_type, + test_comparison_type, + ) + _set_output_paths( + c, + reference_data_path, + reference_subsection, + identifier, + ref_identifier, + ) + prefixes.append(_build_prefix(c, ref_identifier, identifier)) + + prefixes_by_subsection[c.get("subsection") or ""] = prefixes + + if c.get("subsection"): + output_dir = os.path.abspath( + os.path.expandvars(os.path.expanduser(c["output"])) + ) + prior_subsection_outputs[c["subsection"]] = output_dir + prior_subsection_year_sets[c["subsection"]] = { + "ts": ts_year_sets, + "climo": climo_year_sets, + "enso": enso_year_sets, + } + prior_subsection_comparison_types[c["subsection"]] = c["comparison_type"] + + return prefixes_by_subsection + + +def get_mpas_analysis_identifier( *, ts_year1: int, ts_year2: int, climo_year1: int, climo_year2: int ) -> str: # Must match identifier in zppy/templates/mpas_analysis.bash @@ -321,7 +422,7 @@ def _set_identifiers( ctrl_climo: Tuple[int, int], ) -> Tuple[str, str]: c["scriptDir"] = script_dir - identifier = _get_identifier( + identifier = get_mpas_analysis_identifier( ts_year1=c["ts_year1"], ts_year2=c["ts_year2"], climo_year1=c["climo_year1"], @@ -329,7 +430,7 @@ def _set_identifiers( ) c["identifier"] = identifier - ref_identifier = _get_identifier( + ref_identifier = get_mpas_analysis_identifier( ts_year1=ctrl_ts[0], ts_year2=ctrl_ts[1], climo_year1=ctrl_climo[0], @@ -345,21 +446,23 @@ def _set_run_config_files( test_data_path: str, ref_identifier: str, identifier: str, - reference_analysis_subdir: str, - test_analysis_subdir: str, + reference_comparison_type: str, + test_comparison_type: str, ) -> None: c["controlRunConfigFile"] = ( _resolve_mpas_analysis_config_file( reference_data_path, ref_identifier, - analysis_subdir=reference_analysis_subdir, + comparison_type=reference_comparison_type, ) if reference_data_path else "" ) c["mainRunConfigFile"] = ( _resolve_mpas_analysis_config_file( - test_data_path, identifier, analysis_subdir=test_analysis_subdir + test_data_path, + identifier, + comparison_type=test_comparison_type, ) if test_data_path else "" @@ -382,12 +485,58 @@ def _build_prefix(c: Dict[str, Any], ref_identifier: str, identifier: str) -> st return prefix -def _get_subsection_analysis_subdir( - subsection: str, prior_subsection_analysis_subdirs: Dict[str, str] +def _get_referenced_comparison_type( + requested_comparison_type: str, + run_output_dir: str, + subsection: str, + identifier: str, + prior_subsection_comparison_types: Dict[str, str], + parameter_name: str, +) -> str: + if not run_output_dir: + return "mvo" + + if subsection: + actual_comparison_type = prior_subsection_comparison_types[subsection] + if ( + requested_comparison_type != "auto" + and requested_comparison_type != actual_comparison_type + ): + raise ValueError( + f"{parameter_name} refers to mpas_analysis subsection '{subsection}', " + f"which is a {actual_comparison_type} run, not {requested_comparison_type}." + ) + return actual_comparison_type + + if requested_comparison_type in {"mvo", "mvm"}: + return requested_comparison_type + + return _infer_referenced_comparison_type(run_output_dir, identifier, parameter_name) + + +def _infer_referenced_comparison_type( + run_output_dir: str, identifier: str, parameter_name: str ) -> str: - if subsection and (subsection in prior_subsection_analysis_subdirs): - return prior_subsection_analysis_subdirs[subsection] - return "mpas_analysis" + path = Path(os.path.expandvars(os.path.expanduser(run_output_dir))).resolve() + file_name = f"mpas_analysis_{identifier}.cfg" + + mvo_cfg = path / "post" / "analysis" / "mpas_analysis" / "mvo" / "cfg" / file_name + mvm_cfg = path / "post" / "analysis" / "mpas_analysis" / "mvm" / "cfg" / file_name + + has_mvo_cfg = mvo_cfg.exists() + has_mvm_cfg = mvm_cfg.exists() + + if has_mvo_cfg and has_mvm_cfg: + raise ValueError( + f"{parameter_name} is ambiguous for identifier '{identifier}'. Found both " + f"{mvo_cfg} and {mvm_cfg}. Set {parameter_name.replace('_data_path', '_comparison_type')} " + "to 'mvo' or 'mvm'." + ) + if has_mvo_cfg: + return "mvo" + if has_mvm_cfg: + return "mvm" + return "mvo" def _set_output_paths( @@ -398,15 +547,14 @@ def _set_output_paths( ref_identifier: str, ) -> None: is_mvm = bool(reference_data_path) + c["analysis_subdir"] = "mpas_analysis" + c["comparison_type"] = "mvm" if is_mvm else "mvo" + c["analysis_task_name"] = f"{c['analysis_subdir']}/{c['comparison_type']}" + if not is_mvm: - c["analysis_subdir"] = "mpas_analysis" - c["analysis_task_name"] = "mpas_analysis" c["output_dir_name"] = identifier return - c["analysis_subdir"] = "mpas_analysis_mvm" - c["analysis_task_name"] = "mpas_analysis_mvm" - if reference_subsection: c["reference_case"] = c["case"] else: @@ -517,7 +665,7 @@ def _parse_subsection_reference(value: str) -> str: def _resolve_mpas_analysis_config_file( - run_output_dir: str, identifier: str, *, analysis_subdir: str = "mpas_analysis" + run_output_dir: str, identifier: str, *, comparison_type: str = "mvo" ) -> str: """ Resolve the MPAS-Analysis config file path for a prior run. @@ -541,7 +689,7 @@ def _resolve_mpas_analysis_config_file( file_name = f"mpas_analysis_{identifier}.cfg" - cfg_dir = path / "post" / "analysis" / analysis_subdir / "cfg" + cfg_dir = path / "post" / "analysis" / "mpas_analysis" / comparison_type / "cfg" return str(cfg_dir / file_name) diff --git a/zppy/templates/mpas_analysis.bash b/zppy/templates/mpas_analysis.bash index d37d9004..3e4500eb 100644 --- a/zppy/templates/mpas_analysis.bash +++ b/zppy/templates/mpas_analysis.bash @@ -28,7 +28,7 @@ echo echo ===== SET UP MPAS-ANALYSIS DIRECTORY STRUCTURE ===== echo -workdir="../analysis/{{ analysis_subdir }}" +workdir="../analysis/{{ analysis_subdir }}/{{ comparison_type }}" mkdir -p ${workdir} cd ${workdir} @@ -331,7 +331,7 @@ echo ===== COPY FILES TO WEB SERVER ===== echo # Create top-level directory -f=${www}/${case}/{{ analysis_subdir }}/${output_dir_name}/ +f=${www}/${case}/{{ analysis_subdir }}/{{ comparison_type }}/${output_dir_name}/ mkdir -p ${f} if [ $? != 0 ]; then echo 'ERROR (3)' > {{ scriptDir }}/{{ prefix }}.status @@ -353,7 +353,7 @@ done {% endif %} # Copy files -rsync -a --delete ${output_dir_name}/html/ ${www}/${case}/{{ analysis_subdir }}/${output_dir_name}/ +rsync -a --delete ${output_dir_name}/html/ ${www}/${case}/{{ analysis_subdir }}/{{ comparison_type }}/${output_dir_name}/ if [ $? != 0 ]; then echo 'ERROR (4)' > {{ scriptDir }}/{{ prefix }}.status exit 4 @@ -361,7 +361,7 @@ fi {% if machine in ['pm-cpu', 'pm-gpu'] %} # For NERSC, change permissions of new files -pushd ${www}/${case}/{{ analysis_subdir }}/ +pushd ${www}/${case}/{{ analysis_subdir }}/{{ comparison_type }}/ chgrp -R e3sm ${output_dir_name} chmod -R go+rX,go-w ${output_dir_name} popd @@ -369,7 +369,7 @@ popd {% if machine in ['anvil', 'chrysalis'] %} # For LCRC, change permissions of new files -pushd ${www}/${case}/{{ analysis_subdir }}/ +pushd ${www}/${case}/{{ analysis_subdir }}/{{ comparison_type }}/ chmod -R go+rX,go-w ${output_dir_name} popd {% endif %}