Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a884ac7
rebase in progress
lee1043 Apr 10, 2025
cb5e106
rebase in progress
lee1043 Apr 10, 2025
939f01b
in progress
lee1043 Apr 1, 2025
49538d2
update
lee1043 Apr 2, 2025
213ae17
resolve conflict
lee1043 Apr 10, 2025
63da556
update
lee1043 Apr 2, 2025
2111a0b
in progress
lee1043 Apr 3, 2025
1b66adf
test
lee1043 Apr 10, 2025
d4cbb4e
add changelog
lee1043 Apr 10, 2025
5efc881
update
lee1043 Apr 10, 2025
3ffcfef
update
lee1043 Apr 15, 2025
7796774
replace print to logger
lee1043 Apr 15, 2025
ced74c4
style fix
lee1043 Apr 15, 2025
5a79f92
add pmp_clims to datasets
lee1043 Apr 16, 2025
c2e5330
Merge branch 'main' into pmp-annual-cycle
lee1043 Apr 16, 2025
bffec0e
update
lee1043 Apr 16, 2025
68fbcf0
Merge branch 'main' into pmp-annual-cycle
lee1043 Apr 22, 2025
90e656e
update pmp version
lee1043 Apr 24, 2025
c475b2e
use PMPClimatology source data type
lee1043 Apr 24, 2025
8f5ddd3
update pmp version
lee1043 Apr 24, 2025
3247626
Update conda-lock.yml
lee1043 Apr 25, 2025
157f232
update md5 and sha256 for pmp v3.9.1
lee1043 Apr 25, 2025
e8fa467
clean up -- temporary code removed
lee1043 Apr 25, 2025
76285f3
ref data source type fixed
lee1043 Apr 25, 2025
2c9a011
typo fix, clean up (remove development mode)
lee1043 Apr 25, 2025
94d2959
in progress
lee1043 Apr 26, 2025
6c73d48
update
lee1043 Apr 28, 2025
e1c952b
updated
lee1043 Apr 28, 2025
15dd3e6
Merge branch 'main' into pmp-annual-cycle
lee1043 Apr 28, 2025
2678649
clean up
lee1043 Apr 28, 2025
41b4533
Merge branch 'pmp-annual-cycle' of github.com:Climate-REF/climate-ref…
lee1043 Apr 28, 2025
3a3422b
clean up
lee1043 Apr 28, 2025
41f9fb6
clean up
lee1043 Apr 28, 2025
0581eaa
code style fix by running uv run ruff format
lee1043 Apr 28, 2025
73ed07a
pre-commit fix
lee1043 Apr 29, 2025
9969aa1
test: Add an integration test for the annual cycle
lewisjared Apr 29, 2025
97afe69
chore: Remove unused test
lewisjared Apr 29, 2025
7f4d96f
chore: Add test coverage
lewisjared Apr 29, 2025
e94b0e8
chore: Increase test coverage
lewisjared Apr 29, 2025
5c8d25e
chore: Use assert_called_once_with
lewisjared Apr 29, 2025
5d4fc1c
Update packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py
lee1043 Apr 29, 2025
ab00e10
enable when model files are multiple
lee1043 Apr 29, 2025
6f2cd30
update
lee1043 Apr 29, 2025
c3b7467
add unit tests for paths glob pattern detecting
lee1043 Apr 29, 2025
bb0b921
typo fix
lee1043 Apr 29, 2025
6170a22
clean up
lee1043 Apr 29, 2025
6b6f7e2
add more debug logger print
lee1043 Apr 29, 2025
dbf79dd
added to dos comments
lee1043 Apr 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/221.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added PMP's annual cycle metrics
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,16 @@ def _create_definition(
metric_dataset: MetricDataset | None = None,
cmip6: DatasetCollection | None = None,
obs4mips: DatasetCollection | None = None,
pmp_climatology: DatasetCollection | None = None,
) -> MetricExecutionDefinition:
if metric_dataset is None:
datasets = {}
if cmip6:
datasets[SourceDatasetType.CMIP6] = cmip6
if obs4mips:
datasets[SourceDatasetType.obs4MIPs] = obs4mips
if pmp_climatology:
datasets[SourceDatasetType.PMPClimatology] = pmp_climatology
metric_dataset = MetricDataset(datasets)

return MetricExecutionDefinition(
Expand Down
2 changes: 1 addition & 1 deletion packages/ref-metrics-pmp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies = [
]

[tool.uv.sources]
pcmdi_metrics = { git = "https://github.com/PCMDI/pcmdi_metrics", rev="v3.9" }
pcmdi_metrics = { git = "https://github.com/PCMDI/pcmdi_metrics", rev="v3.9.1" }

[project.license]
text = "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from cmip_ref_core.dataset_registry import dataset_registry_manager
from cmip_ref_core.providers import CondaMetricsProvider
from cmip_ref_metrics_pmp.annual_cycle import AnnualCycle
from cmip_ref_metrics_pmp.variability_modes import ExtratropicalModesOfVariability

__version__ = importlib.metadata.version("cmip_ref_metrics_pmp")
Expand All @@ -21,6 +22,7 @@
provider.register(ExtratropicalModesOfVariability("PNA"))
provider.register(ExtratropicalModesOfVariability("NPO"))
provider.register(ExtratropicalModesOfVariability("SAM"))
provider.register(AnnualCycle())


dataset_registry_manager.register(
Expand Down
239 changes: 239 additions & 0 deletions packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import datetime
import json
from collections.abc import Iterable

from loguru import logger

from cmip_ref_core.datasets import FacetFilter, SourceDatasetType
from cmip_ref_core.metrics import (
CommandLineMetric,
DataRequirement,
MetricExecutionDefinition,
MetricExecutionResult,
)
from cmip_ref_metrics_pmp.pmp_driver import build_glob_pattern, build_pmp_command, process_json_result


class AnnualCycle(CommandLineMetric):
"""
Calculate the annual cycle for a dataset
"""

def __init__(self) -> None:
self.name = "PMP Annual Cycle"
self.slug = "pmp-annual-cycle"
self.data_requirements = (
DataRequirement(
source_type=SourceDatasetType.PMPClimatology,
# TODO: Add or operators and enable more varaiables
filters=(
FacetFilter(
facets={"source_id": ("GPCP-Monthly-3-2", "ERA-5"), "variable_id": ("pr", "ts")}
),
),
group_by=("variable_id", "source_id"),
),
DataRequirement(
source_type=SourceDatasetType.CMIP6,
# TODO: Add or operators and enable more varaiables
filters=(
FacetFilter(
facets={
"frequency": "mon",
"experiment_id": ("amip", "historical", "hist-GHG", "piControl"),
"variable_id": ("pr", "ts"),
}
),
),
group_by=("variable_id", "source_id", "experiment_id", "variant_label", "member_id"),
),
)

self.parameter_file_1 = "pmp_param_annualcycle_1-clims.py"
self.parameter_file_2 = "pmp_param_annualcycle_2-metrics.py"

def build_cmd(self, definition: MetricExecutionDefinition) -> Iterable[str]:
"""
Build the command to run the metric

Parameters
----------
definition
Definition of the metric execution

Returns
-------
Command arguments to execute in the PMP environment
"""
raise NotImplementedError("Function not required")

def build_cmds(self, definition: MetricExecutionDefinition) -> list[list[str]]:
"""
Build the command to run the metric

Parameters
----------
definition
Definition of the metric execution

Returns
-------
Command arguments to execute in the PMP environment
"""
input_datasets = definition.metric_dataset[SourceDatasetType.CMIP6]
source_id = input_datasets["source_id"].unique()[0]
experiment_id = input_datasets["experiment_id"].unique()[0]
member_id = input_datasets["member_id"].unique()[0]
variable_id = input_datasets["variable_id"].unique()[0]

logger.debug(f"input_datasets['source_id'].unique(): {input_datasets['source_id'].unique()}")
logger.debug(f"input_datasets['experiment_id'].unique(): {input_datasets['experiment_id'].unique()}")
logger.debug(f"input_datasets['member_id'].unique(): {input_datasets['member_id'].unique()}")
logger.debug(f"input_datasets['variable_id'].unique(): {input_datasets['variable_id'].unique()}")

model_files_raw = input_datasets.path.to_list()
if len(model_files_raw) == 1:
model_files = model_files_raw[0] # If only one file, use it directly
elif len(model_files_raw) > 1:
model_files = build_glob_pattern(model_files_raw) # If multiple files, build a glob pattern

Check warning on line 98 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L98

Added line #L98 was not covered by tests
else:
raise ValueError("No model files found")

Check warning on line 100 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L100

Added line #L100 was not covered by tests

logger.debug("build_cmd start")

logger.debug(f"input_datasets: {input_datasets}")
logger.debug(f"input_datasets.keys(): {input_datasets.keys()}")
logger.debug(f"input_datasets['variable_id']: {input_datasets['variable_id']}")

logger.debug(f"source_id: {source_id}")
logger.debug(f"experiment_id: {experiment_id}")
logger.debug(f"member_id: {member_id}")
logger.debug(f"variable_id: {variable_id}")

reference_dataset = definition.metric_dataset[SourceDatasetType.PMPClimatology]
reference_dataset_name = reference_dataset["source_id"].unique()[0]
reference_dataset_path = reference_dataset.datasets.iloc[0]["path"]

logger.debug(f"reference_dataset.datasets: {reference_dataset.datasets}")
logger.debug(f"reference_dataset['source_id']: {reference_dataset['source_id']}")
logger.debug(
f"reference_dataset.datasets.iloc[0]['path']: {reference_dataset.datasets.iloc[0]['path']}"
)

logger.debug(f"reference_dataset_name: {reference_dataset_name}")
logger.debug(f"reference_dataset_path: {reference_dataset_path}")

output_directory_path = str(definition.output_directory)

cmds = []

# ----------------------------------------------
# PART 1: Build the command to get climatologies
# ----------------------------------------------
# Model
data_name = f"{source_id}_{experiment_id}_{member_id}"
data_path = model_files
params = {
"driver_file": "mean_climate/pcmdi_compute_climatologies.py",
"parameter_file": self.parameter_file_1,
"vars": variable_id,
"infile": data_path,
"outfile": f"{output_directory_path}/{variable_id}_{data_name}_clims.nc",
}

cmds.append(build_pmp_command(**params))

# ----------------------------------------------
# PART 2: Build the command to calculate metrics
# ----------------------------------------------

# Reference
obs_dict = {
variable_id: {
reference_dataset_name: {
"template": reference_dataset_path,
},
"default": reference_dataset_name,
}
}

# Generate a JSON file based on the obs_dict
with open(f"{output_directory_path}/obs_dict.json", "w") as f:
json.dump(obs_dict, f)

date = datetime.datetime.now().strftime("%Y%m%d")

params = {
"driver_file": "mean_climate/mean_climate_driver.py",
"parameter_file": self.parameter_file_2,
"vars": variable_id,
"custom_observations": f"{output_directory_path}/obs_dict.json",
"test_data_path": output_directory_path,
"test_data_set": source_id,
"realization": member_id,
"filename_template": f"{variable_id}_{data_name}_clims.198101-200512.AC.v{date}.nc",
"metrics_output_path": output_directory_path,
"cmec": "",
}

cmds.append(build_pmp_command(**params))

return cmds

def build_metric_result(self, definition: MetricExecutionDefinition) -> MetricExecutionResult:
"""
Build a metric result from the output of the PMP driver

Parameters
----------
definition
Definition of the metric execution

Returns
-------
Result of the metric execution
"""
input_datasets = definition.metric_dataset[SourceDatasetType.CMIP6]
variable_id = input_datasets["variable_id"].unique()[0]

Check warning on line 197 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L196-L197

Added lines #L196 - L197 were not covered by tests

results_directory = definition.output_directory
png_directory = results_directory / variable_id
data_directory = results_directory / variable_id

Check warning on line 201 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L199-L201

Added lines #L199 - L201 were not covered by tests

results_files = list(results_directory.glob("*_cmec.json"))

Check warning on line 203 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L203

Added line #L203 was not covered by tests
if len(results_files) != 1: # pragma: no cover
return MetricExecutionResult.build_from_failure(definition)

# Find the other outputs
png_files = list(png_directory.glob("*.png"))
data_files = list(data_directory.glob("*.nc"))

Check warning on line 209 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L208-L209

Added lines #L208 - L209 were not covered by tests

cmec_output, cmec_metric = process_json_result(results_files[0], png_files, data_files)

Check warning on line 211 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L211

Added line #L211 was not covered by tests

return MetricExecutionResult.build_from_output_bundle(

Check warning on line 213 in packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py

View check run for this annotation

Codecov / codecov/patch

packages/ref-metrics-pmp/src/cmip_ref_metrics_pmp/annual_cycle.py#L213

Added line #L213 was not covered by tests
definition,
cmec_output_bundle=cmec_output,
cmec_metric_bundle=cmec_metric,
)

def run(self, definition: MetricExecutionDefinition) -> MetricExecutionResult:
"""
Run the metric on the given configuration.

Parameters
----------
definition : MetricExecutionDefinition
The configuration to run the metric on.

Returns
-------
:
The result of running the metric.
"""
logger.debug("PMP annual cycle run start")
cmds = self.build_cmds(definition)

runs = [self.provider.run(cmd) for cmd in cmds]
logger.debug(f"runs: {runs}")

return self.build_metric_result(definition)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#
# OPTIONS ARE SET BY USER IN THIS FILE AS INDICATED BELOW BY:
#
#

# VARIABLES TO USE
# vars = ["rlut"]

# START AND END DATES FOR CLIMATOLOGY
start = "1981-01"
end = "2005-12"

# INPUT DATASET - CAN BE MODEL OR OBSERVATIONS
infile = (
"obs4MIPs_PCMDI_monthly/NASA-LaRC/CERES-EBAF-4-1"
"/mon/rlut/gn/v20210727"
"/rlut_mon_CERES-EBAF-4-1_PCMDI_gn_200301-201812.nc"
)

# DIRECTORY WHERE TO PUT RESULTS
outfile = "climo/rlut_mon_CERES-EBAF-4-1_BE_gn.nc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os

#
# OPTIONS ARE SET BY USER IN THIS FILE AS INDICATED BELOW BY:
#
#

# RUN IDENTIFICATION
# DEFINES A SUBDIRECTORY TO METRICS OUTPUT RESULTS SO MULTIPLE CASES CAN
# BE COMPARED
case_id = "basicTest"

# LIST OF MODEL VERSIONS TO BE TESTED - WHICH ARE EXPECTED TO BE PART OF
# CLIMATOLOGY FILENAME
# test_data_set = ["ACCESS1-0", "CanCM4"]


# VARIABLES TO USE
# vars = ["rlut"]


# Observations to use at the moment "default" or "alternate"
reference_data_set = ["all"]
# ext = '.nc'

# INTERPOLATION OPTIONS
target_grid = "2.5x2.5" # OPTIONS: '2.5x2.5' or an actual cdms2 grid object
regrid_tool = "regrid2" # 'regrid2' # OPTIONS: 'regrid2','esmf'
# -- OPTIONS: 'linear','conservative', only if tool is esmf
regrid_method = "linear"
regrid_tool_ocn = "esmf" # OPTIONS: "regrid2","esmf"
# -- OPTIONS: 'linear','conservative', only if tool is esmf
regrid_method_ocn = "linear"

# Templates for climatology files
# %(param) will subsitute param with values in this file
filename_template = "cmip5.historical.%(model_version).r1i1p1.mon.%(variable).198101-200512.AC.v20200426.nc"

# filename template for landsea masks ('sftlf')
sftlf_filename_template = "sftlf_%(model_version).nc"
generate_sftlf = True # if land surface type mask cannot be found, generate one

# Region
regions = {"rlut": ["Global"]}

# ROOT PATH FOR MODELS CLIMATOLOGIES
test_data_path = "demo_data_tmp/CMIP5_demo_clims/"
# ROOT PATH FOR OBSERVATIONS
# Note that atm/mo/%(variable)/ac will be added to this
reference_data_path = ""
custom_observations = "obs_dict.json"

# DIRECTORY WHERE TO PUT RESULTS
metrics_output_path = os.path.join("demo_output_tmp", "%(case_id)")
Loading