From 4f466ab091e42f57f1e86d4a69ea6d9232a56491 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Thu, 5 Mar 2026 19:52:59 -0600 Subject: [PATCH 1/8] Add mpas_analysis_subsection parameter to global_time_series --- tests/integration/template_weekly_comprehensive_v3.cfg | 7 +++++++ zppy/defaults/default.ini | 3 +++ zppy/global_time_series.py | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/integration/template_weekly_comprehensive_v3.cfg b/tests/integration/template_weekly_comprehensive_v3.cfg index fac286cb..4a40d744 100644 --- a/tests/integration/template_weekly_comprehensive_v3.cfg +++ b/tests/integration/template_weekly_comprehensive_v3.cfg @@ -209,6 +209,12 @@ qos = "#expand qos_long#" shortTermArchive = True walltime = "#expand mpas_analysis_walltime#" + [[ original ]] + # Keep configuration from original mpas_analysis tests for global_time_series to use + ts_years = "1985-1989", "1985-1995", + climo_years = "1985-1989", "1990-1995", + enso_years = "1985-1989", "1990-1995", + [[ reference ]] ts_years = "1985-1989", climo_years = "1985-1989", @@ -230,6 +236,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_subsection = "original" # 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/zppy/defaults/default.ini b/zppy/defaults/default.ini index 768a3ce7..f21ba593 100755 --- a/zppy/defaults/default.ini +++ b/zppy/defaults/default.ini @@ -404,6 +404,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]` subtask to depend on +# Leave empty if no subtasks are defined for mpas_analysis +mpas_analysis_subsection = string(default="") # nrows, ncols are DEPRECATED. Specifying them will have no effect. # Number of columns per page ncols = integer(default=2) diff --git a/zppy/global_time_series.py b/zppy/global_time_series.py index 94cb9826..9a1671fc 100644 --- a/zppy/global_time_series.py +++ b/zppy/global_time_series.py @@ -184,9 +184,12 @@ def determine_and_add_dependencies( c["ts_year2"] = ts_year_set[1] c["climo_year1"] = climo_year_set[0] c["climo_year2"] = climo_year_set[1] + mpas_sub_str: str = "" + if "mpas_analysis_subsection" in c.keys() and c["mpas_analysis_subsection"]: + mpas_sub_str = f"_{c['mpas_analysis_subsection']}" 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", + f"mpas_analysis{mpas_sub_str}_ts_{c['ts_year1']:04d}-{c['ts_year2']:04d}_climo_{c['climo_year1']:04d}-{c['climo_year2']:04d}.status", ) ) From 3f3cadc867edd1f4bb405bf6d47883784a676dbf Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Fri, 13 Mar 2026 13:22:53 -0500 Subject: [PATCH 2/8] Allow multiple mpas_analysis subsections --- .../template_weekly_comprehensive_v3.cfg | 8 +----- zppy/defaults/default.ini | 4 +-- zppy/global_time_series.py | 27 +++++++++++++------ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/integration/template_weekly_comprehensive_v3.cfg b/tests/integration/template_weekly_comprehensive_v3.cfg index 4a40d744..b9c1e0ab 100644 --- a/tests/integration/template_weekly_comprehensive_v3.cfg +++ b/tests/integration/template_weekly_comprehensive_v3.cfg @@ -209,12 +209,6 @@ qos = "#expand qos_long#" shortTermArchive = True walltime = "#expand mpas_analysis_walltime#" - [[ original ]] - # Keep configuration from original mpas_analysis tests for global_time_series to use - ts_years = "1985-1989", "1985-1995", - climo_years = "1985-1989", "1990-1995", - enso_years = "1985-1989", "1990-1995", - [[ reference ]] ts_years = "1985-1989", climo_years = "1985-1989", @@ -236,7 +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_subsection = "original" +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/zppy/defaults/default.ini b/zppy/defaults/default.ini index f21ba593..c953f060 100755 --- a/zppy/defaults/default.ini +++ b/zppy/defaults/default.ini @@ -404,9 +404,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]` subtask to depend on +# Name of the `[mpas_analysis]` subtasks to depend on # Leave empty if no subtasks are defined for mpas_analysis -mpas_analysis_subsection = string(default="") +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) diff --git a/zppy/global_time_series.py b/zppy/global_time_series.py index 9a1671fc..88ce08ff 100644 --- a/zppy/global_time_series.py +++ b/zppy/global_time_series.py @@ -184,12 +184,23 @@ def determine_and_add_dependencies( c["ts_year2"] = ts_year_set[1] c["climo_year1"] = climo_year_set[0] c["climo_year2"] = climo_year_set[1] - mpas_sub_str: str = "" - if "mpas_analysis_subsection" in c.keys() and c["mpas_analysis_subsection"]: - mpas_sub_str = f"_{c['mpas_analysis_subsection']}" - dependencies.append( - os.path.join( - script_dir, - f"mpas_analysis{mpas_sub_str}_ts_{c['ts_year1']:04d}-{c['ts_year2']:04d}_climo_{c['climo_year1']:04d}-{c['climo_year2']:04d}.status", + mpas_sub_list: List[str] = [] + if ( + "mpas_analysis_subsections" in c.keys() + and c["mpas_analysis_subsection"] + ): + mpas_analysis_subsections_input = c["mpas_analysis_subsections"] + if isinstance(mpas_analysis_subsections_input, str): + # This will be the case if mpas_analysis_subsections is missing a trailing comma + mpas_sub_list = [mpas_analysis_subsections_input] + else: + mpas_sub_list = mpas_analysis_subsections_input + # Else: no subtasks are specified for mpas_analysis + for mpas_sub in mpas_sub_list: + mpas_sub_str = f"_{mpas_sub}" + dependencies.append( + os.path.join( + script_dir, + f"mpas_analysis{mpas_sub_str}_ts_{c['ts_year1']:04d}-{c['ts_year2']:04d}_climo_{c['climo_year1']:04d}-{c['climo_year2']:04d}.status", + ) ) - ) From 91d39e7225589ab7316d4250e1f4b033f2e157dd Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 27 Mar 2026 14:44:24 +0100 Subject: [PATCH 3/8] Fix mpas_analysis status file names. We need to use the climatology and time series bounds from that analysis, rather than from global time series, to construct their filenames. To make this easier and better share code, the bulk of this work happens in the mpas_analysis module, not in global_time_series. --- zppy/defaults/default.ini | 1 + zppy/global_time_series.py | 71 ++++++++++++++++++----------- zppy/mpas_analysis.py | 91 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 30 deletions(-) diff --git a/zppy/defaults/default.ini b/zppy/defaults/default.ini index c953f060..9d933396 100755 --- a/zppy/defaults/default.ini +++ b/zppy/defaults/default.ini @@ -440,6 +440,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 88ce08ff..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,33 +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] - mpas_sub_list: List[str] = [] - if ( - "mpas_analysis_subsections" in c.keys() - and c["mpas_analysis_subsection"] - ): - mpas_analysis_subsections_input = c["mpas_analysis_subsections"] - if isinstance(mpas_analysis_subsections_input, str): - # This will be the case if mpas_analysis_subsections is missing a trailing comma - mpas_sub_list = [mpas_analysis_subsections_input] - else: - mpas_sub_list = mpas_analysis_subsections_input - # Else: no subtasks are specified for mpas_analysis - for mpas_sub in mpas_sub_list: - mpas_sub_str = f"_{mpas_sub}" - dependencies.append( - os.path.join( - script_dir, - f"mpas_analysis{mpas_sub_str}_ts_{c['ts_year1']:04d}-{c['ts_year2']:04d}_climo_{c['climo_year1']:04d}-{c['climo_year2']:04d}.status", - ) - ) + 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_{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..3d0c63cb 100644 --- a/zppy/mpas_analysis.py +++ b/zppy/mpas_analysis.py @@ -170,7 +170,92 @@ def mpas_analysis(config: ConfigObj, script_dir: str, existing_bundles, job_ids_ 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_analysis_subdirs: 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_analysis_subdir = _get_subsection_analysis_subdir( + reference_subsection, prior_subsection_analysis_subdirs + ) + test_analysis_subdir = _get_subsection_analysis_subdir( + test_subsection, prior_subsection_analysis_subdirs + ) + _set_run_config_files( + c, + reference_data_path, + test_data_path, + ref_identifier, + identifier, + reference_analysis_subdir, + test_analysis_subdir, + ) + _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_analysis_subdirs[c["subsection"]] = c.get( + "analysis_subdir", "mpas_analysis" + ) + + 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 +406,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 +414,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], From 2964b0f598f4d7d7fe0f26bfdb787f74908b3728 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Fri, 27 Mar 2026 14:46:10 +0100 Subject: [PATCH 4/8] Add unit tests to ensure correct status filenames --- tests/test_zppy_global_time_series.py | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/tests/test_zppy_global_time_series.py b/tests/test_zppy_global_time_series.py index 6be322ce..61ac3724 100644 --- a/tests/test_zppy_global_time_series.py +++ b/tests/test_zppy_global_time_series.py @@ -1,8 +1,12 @@ +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_mpas_analysis_prefixes def test_determine_components(): @@ -123,6 +127,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 +170,67 @@ 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" + ], + } From 2857766f424390da8c27d0a263db9186fc9664e0 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Mon, 30 Mar 2026 19:11:54 -0500 Subject: [PATCH 5/8] Add comparison_type subdir --- tests/integration/image_checker.py | 5 +---- zppy/mpas_analysis.py | 8 +++++--- zppy/templates/mpas_analysis.bash | 8 ++++---- 3 files changed, 10 insertions(+), 11 deletions(-) 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/zppy/mpas_analysis.py b/zppy/mpas_analysis.py index 3d0c63cb..4cb8e419 100644 --- a/zppy/mpas_analysis.py +++ b/zppy/mpas_analysis.py @@ -483,14 +483,16 @@ def _set_output_paths( ref_identifier: str, ) -> None: is_mvm = bool(reference_data_path) + c["analysis_subdir"] = "mpas_analysis" + if not is_mvm: - c["analysis_subdir"] = "mpas_analysis" c["analysis_task_name"] = "mpas_analysis" + c["comparison_type"] = "mvo" c["output_dir_name"] = identifier return - c["analysis_subdir"] = "mpas_analysis_mvm" - c["analysis_task_name"] = "mpas_analysis_mvm" + c["analysis_task_name"] = "mpas_analysis" + c["comparison_type"] = "mvm" if reference_subsection: c["reference_case"] = c["case"] diff --git a/zppy/templates/mpas_analysis.bash b/zppy/templates/mpas_analysis.bash index d37d9004..25d11f64 100644 --- a/zppy/templates/mpas_analysis.bash +++ b/zppy/templates/mpas_analysis.bash @@ -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 %} From 6492928b56a8446ba119b45a4a0796bd303a8da0 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 31 Mar 2026 09:18:03 -0500 Subject: [PATCH 6/8] Put mpas_analysis runs in mvo or mvm subdirs This requires having a way for a user to specify whether an external reference run (not defined in a given config file as a subsection) was `mvo` or `mvm` via a new `reference_comparison_type` config option. --- tests/test_zppy_global_time_series.py | 96 +++++++++++++++++- zppy/defaults/default.ini | 11 ++- zppy/mpas_analysis.py | 135 +++++++++++++++++++------- zppy/templates/mpas_analysis.bash | 2 +- 4 files changed, 203 insertions(+), 41 deletions(-) diff --git a/tests/test_zppy_global_time_series.py b/tests/test_zppy_global_time_series.py index 61ac3724..808a2635 100644 --- a/tests/test_zppy_global_time_series.py +++ b/tests/test_zppy_global_time_series.py @@ -6,7 +6,11 @@ from validate import Validator from zppy.global_time_series import determine_and_add_dependencies, determine_components -from zppy.mpas_analysis import get_mpas_analysis_prefixes +from zppy.mpas_analysis import ( + _get_referenced_comparison_type, + _resolve_mpas_analysis_config_file, + get_mpas_analysis_prefixes, +) def test_determine_components(): @@ -234,3 +238,93 @@ def test_get_mpas_analysis_prefixes(tmp_path): "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 9d933396..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) diff --git a/zppy/mpas_analysis.py b/zppy/mpas_analysis.py index 4cb8e419..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,9 +173,7 @@ 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 @@ -177,7 +185,7 @@ def get_mpas_analysis_prefixes(config: ConfigObj) -> Dict[str, List[str]]: prior_subsection_outputs: Dict[str, str] = {} prior_subsection_year_sets: Dict[str, Dict[str, List[Tuple[int, int]]]] = {} - prior_subsection_analysis_subdirs: Dict[str, str] = {} + prior_subsection_comparison_types: Dict[str, str] = {} prefixes_by_subsection: Dict[str, List[str]] = {} for c in tasks: @@ -212,11 +220,21 @@ def get_mpas_analysis_prefixes(config: ConfigObj) -> Dict[str, List[str]]: identifier, ref_identifier = _set_identifiers( c, script_dir="", ctrl_ts=ctrl_ts, ctrl_climo=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, @@ -224,8 +242,8 @@ def get_mpas_analysis_prefixes(config: ConfigObj) -> Dict[str, List[str]]: test_data_path, ref_identifier, identifier, - reference_analysis_subdir, - test_analysis_subdir, + reference_comparison_type, + test_comparison_type, ) _set_output_paths( c, @@ -248,9 +266,7 @@ def get_mpas_analysis_prefixes(config: ConfigObj) -> Dict[str, List[str]]: "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 prefixes_by_subsection @@ -430,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 "" @@ -467,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 subsection and (subsection in prior_subsection_analysis_subdirs): - return prior_subsection_analysis_subdirs[subsection] - return "mpas_analysis" + 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: + 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( @@ -484,16 +548,13 @@ def _set_output_paths( ) -> 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_task_name"] = "mpas_analysis" - c["comparison_type"] = "mvo" c["output_dir_name"] = identifier return - c["analysis_task_name"] = "mpas_analysis" - c["comparison_type"] = "mvm" - if reference_subsection: c["reference_case"] = c["case"] else: @@ -604,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. @@ -628,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 25d11f64..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} From b7ef5d0151e2b968fc82afe92f8cb0ca9a281c86 Mon Sep 17 00:00:00 2001 From: Xylar Asay-Davis Date: Tue, 31 Mar 2026 09:23:43 -0500 Subject: [PATCH 7/8] Update the docs with reference_comparison_type, etc. --- docs/source/parameters.rst | 18 ++++++++++++++++-- .../post.mpas_analysis_model_vs_model.cfg | 8 +++++++- docs/source/tutorial.rst | 4 ++++ 3 files changed, 27 insertions(+), 3 deletions(-) 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 From fb8ef3ec670c98635ffd308b335e21860af46eeb Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Tue, 31 Mar 2026 12:25:15 -0500 Subject: [PATCH 8/8] Update auto-generated files --- .../test_weekly_comprehensive_v3_chrysalis.cfg | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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",