diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..76f03ce
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+dotenv_if_exists .env
diff --git a/README.md b/README.md
index 78b1c76..dd66be6 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,11 @@
This repository contains the API and Frontend for the Climate Rapid Evaluation Framework (REF). This system enables comprehensive benchmarking and evaluation of Earth system models against observational data, integrating with the `climate-ref` core library.
This is a full-stack application that consists of a:
-* **Backend**: FastAPI API (Python 3.11+)
- * FastAPI, Pydantic, SQLAlchemy, OpenAPI documentation
-* **Frontend**: React frontend (React 19, TypeScript)
- * Vite, Tanstack Router, Tanstack Query, Tailwind CSS, Shadcn/ui, Recharts
+
+* **Backend**: FastAPI API (Python 3.11+)
+ * FastAPI, Pydantic, SQLAlchemy, OpenAPI documentation
+* **Frontend**: React frontend (React 19, TypeScript)
+ * Vite, Tanstack Router, Tanstack Query, Tailwind CSS, Shadcn/ui, Recharts
**Status**: Alpha
@@ -15,29 +16,41 @@ This is a full-stack application that consists of a:
[](https://github.com/Climate-REF/climate-ref/commits/main)
[](https://github.com/Climate-REF/ref-app/graphs/contributors)
-
## Overview
The Climate REF Web Application provides researchers and scientists with tools to:
-- Enable rapid model evaluation and near real-time assessment of climate model performance.
-- Provide standardized, reproducible evaluation metrics across different models and datasets.
-- Make complex climate model diagnostics accessible through an intuitive web interface.
-- Ensure evaluation processes are transparent and results are traceable.
-- Consolidate various diagnostic tools into a unified framework.
-- Automate the execution of diagnostics when new datasets are available.
-- Help researchers find and understand available datasets and their evaluation status.
-- Enable easy comparison of model performance across different versions and experiments.
+
+* Enable rapid model evaluation and near real-time assessment of climate model performance.
+* Provide standardized, reproducible evaluation metrics across different models and datasets.
+* Make complex climate model diagnostics accessible through an intuitive web interface.
+* Ensure evaluation processes are transparent and results are traceable.
+* Consolidate various diagnostic tools into a unified framework.
+* Automate the execution of diagnostics when new datasets are available.
+* Help researchers find and understand available datasets and their evaluation status.
+* Enable easy comparison of model performance across different versions and experiments.
+
+## Updating Diagnostic Content
+
+Display metadata for each AFT diagnostic collection (descriptions, explanations, plain-language summaries)
+is maintained in YAML files under [`backend/static/collections/`](backend/static/collections/).
+See the [collections README](backend/static/collections/README.md) for the full schema and instructions.
+
+Diagnostic-level metadata overrides (display names, reference datasets, tags) are split into per-provider
+YAML files under `backend/static/diagnostics/` (e.g. `pmp.yaml`, `esmvaltool.yaml`, `ilamb.yaml`),
+which can be regenerated from the provider registry with `make generate-metadata`.
+
+After changing content fields or adding new collections, regenerate the frontend TypeScript client with `make generate-client`.
## Getting Started
### Prerequisites
-- Python 3.11+ (with `uv` for package management)
-- Node.js v20 and npm (for frontend)
-- Database: SQLite (development/test) or PostgreSQL (production)
-- Docker and Docker Compose (optional, for containerized deployment)
+* Python 3.11+ (with `uv` for package management)
+* Node.js v20 and npm (for frontend)
+* Database: SQLite (development/test) or PostgreSQL (production)
+* Docker and Docker Compose (optional, for containerized deployment)
-1. **Clone the repository**
+1. **Clone the repository**
```bash
git clone https://github.com/Climate-REF/ref-app.git
@@ -46,7 +59,7 @@ The Climate REF Web Application provides researchers and scientists with tools t
### Backend Setup
-2. **Set up environment variables**
+1. **Set up environment variables**
Create a `.env` file in the project root by copying the `.env.example` file.
@@ -56,14 +69,14 @@ The Climate REF Web Application provides researchers and scientists with tools t
Modify the `.env` to your needs. The `REF_CONFIGURATION` variable should point to the configuration directory for the REF, which defines the database connection string and other REF-specific settings.
-3. **Install dependencies**
+2. **Install dependencies**
```bash
cd backend
make virtual-environment
```
-4. **Start the backend server**
+3. **Start the backend server**
```bash
make dev
@@ -71,20 +84,20 @@ The Climate REF Web Application provides researchers and scientists with tools t
### Frontend Setup
-1. **Generate Client**
+1. **Generate Client**
```bash
make generate-client
```
-2. **Install dependencies**
+2. **Install dependencies**
```bash
cd frontend
npm install
```
-3. **Start the frontend server**
+3. **Start the frontend server**
```bash
npm run dev
@@ -104,6 +117,9 @@ ref-app/
│ │ │ └── main.py # API router aggregation
│ │ ├── core/ # Core application logic (config, file handling, REF initialization)
│ │ └── models.py # Pydantic models for API responses
+│ ├── static/
+│ │ ├── collections/ # Per-collection YAML metadata (see collections/README.md)
+│ │ └── diagnostics/ # Diagnostic metadata overrides
│ ├── tests/ # Backend test suite
│ ├── pyproject.toml # Python dependencies and project metadata
│ └── uv.lock # uv lock file for reproducible dependencies
@@ -125,6 +141,7 @@ ref-app/
## API Documentation
When the backend is running, API documentation is available at:
-- Swagger UI: http://localhost:8001/docs
-- ReDoc: http://localhost:8001/redoc
-- OpenAPI JSON: http://localhost:8001/openapi.json
+
+* Swagger UI:
+* ReDoc:
+* OpenAPI JSON:
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 48601c0..2441704 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -11,7 +11,7 @@ dependencies = [
"psycopg[binary]<4.0.0,>=3.1.13",
"pydantic-settings<3.0.0,>=2.2.1",
"sentry-sdk[fastapi]>=2.0.0",
- "climate-ref[aft-providers,postgres]>=0.12.0",
+ "climate-ref[aft-providers,postgres]>=0.12.2",
"loguru",
"pyyaml>=6.0",
"fastapi-sqlalchemy-monitor>=1.1.3",
diff --git a/backend/scripts/generate_metadata.py b/backend/scripts/generate_metadata.py
index 4b3cdb2..3842666 100644
--- a/backend/scripts/generate_metadata.py
+++ b/backend/scripts/generate_metadata.py
@@ -1,16 +1,17 @@
"""
Generate diagnostic metadata YAML from the current provider registry.
-This script bootstraps or updates the metadata.yaml file by iterating all
-registered diagnostics and capturing their current state (display_name,
-description, tags, reference_datasets). Existing values in metadata.yaml
+This script bootstraps or updates the per-provider metadata files by iterating
+all registered diagnostics and capturing their current state (display_name,
+description, tags, reference_datasets). Existing values in the metadata files
take precedence over auto-generated values.
Usage:
cd backend && uv run python scripts/generate_metadata.py
Options:
- --output PATH Write to a specific file (default: static/diagnostics/metadata.yaml)
+ --output PATH Write to a specific file (default: writes per-provider files
+ into static/diagnostics/)
--dry-run Print to stdout instead of writing to file
"""
@@ -72,45 +73,35 @@ def _build_entry(
def generate_metadata(output_path: Path | None = None, *, dry_run: bool = False) -> None:
- """Generate metadata.yaml from the provider registry, merging with existing values."""
+ """Generate per-provider metadata YAML files from the provider registry, merging with existing values."""
settings = Settings()
ref_config = get_ref_config(settings)
database = get_database(ref_config)
provider_registry = get_provider_registry(ref_config)
- # Load existing metadata (existing values take precedence)
- default_metadata_path = backend_dir / "static" / "diagnostics" / "metadata.yaml"
- metadata_path = output_path or default_metadata_path
- existing_metadata = load_diagnostic_metadata(metadata_path)
+ # Load existing metadata from the directory (existing values take precedence)
+ default_metadata_dir = backend_dir / "static" / "diagnostics"
+ metadata_dir = output_path or default_metadata_dir
+ existing_metadata = load_diagnostic_metadata(metadata_dir)
- # Iterate all registered diagnostics
- generated: dict[str, dict[str, Any]] = {}
+ # Group diagnostics by provider
+ by_provider: dict[str, dict[str, dict[str, Any]]] = {}
with database.session.connection():
for provider_slug, diagnostics in provider_registry.metrics.items():
for diagnostic_slug, concrete_diagnostic in diagnostics.items():
key = f"{provider_slug}/{diagnostic_slug}"
- generated[key] = _build_entry(key, diagnostic_slug, concrete_diagnostic, existing_metadata)
+ entry = _build_entry(key, diagnostic_slug, concrete_diagnostic, existing_metadata)
+ by_provider.setdefault(provider_slug, {})[key] = entry
# Also include any entries from existing metadata that weren't found in the registry
for key, metadata in existing_metadata.items():
- if key not in generated:
- generated[key] = _metadata_to_dict(metadata)
-
- # Sort by key for consistent output
- sorted_metadata = dict(sorted(generated.items()))
-
- # Generate YAML output
- yaml_content = yaml.dump(
- sorted_metadata,
- default_flow_style=False,
- sort_keys=False,
- allow_unicode=True,
- width=120,
- )
+ provider_slug = key.split("/")[0]
+ if key not in by_provider.get(provider_slug, {}):
+ by_provider.setdefault(provider_slug, {})[key] = _metadata_to_dict(metadata)
header = (
- "# Diagnostic Metadata\n"
+ "# {provider} Diagnostic Metadata\n"
"#\n"
"# Auto-generated by: cd backend && uv run python scripts/generate_metadata.py\n"
"#\n"
@@ -120,15 +111,32 @@ def generate_metadata(output_path: Path | None = None, *, dry_run: bool = False)
"#\n\n"
)
- output = header + yaml_content
+ total = 0
+ for provider_slug, entries in sorted(by_provider.items()):
+ sorted_entries = dict(sorted(entries.items()))
+ total += len(sorted_entries)
+
+ yaml_content = yaml.dump(
+ sorted_entries,
+ default_flow_style=False,
+ sort_keys=False,
+ allow_unicode=True,
+ width=120,
+ )
+
+ output = header.format(provider=provider_slug) + yaml_content
+
+ if dry_run:
+ print(f"--- {provider_slug}.yaml ---")
+ print(output)
+ else:
+ metadata_dir.mkdir(parents=True, exist_ok=True)
+ file_path = metadata_dir / f"{provider_slug}.yaml"
+ file_path.write_text(output)
+ print(f"Generated metadata written to {file_path} ({len(sorted_entries)} diagnostics)")
- if dry_run:
- print(output)
- else:
- metadata_path.parent.mkdir(parents=True, exist_ok=True)
- metadata_path.write_text(output)
- print(f"Generated metadata written to {metadata_path}")
- print(f"Total diagnostics: {len(sorted_metadata)}")
+ if not dry_run:
+ print(f"Total diagnostics across all providers: {total}")
def main() -> None:
@@ -137,7 +145,7 @@ def main() -> None:
"--output",
type=Path,
default=None,
- help="Output file path (default: static/diagnostics/metadata.yaml)",
+ help="Output directory (default: static/diagnostics/)",
)
parser.add_argument(
"--dry-run",
diff --git a/backend/src/ref_backend/api/main.py b/backend/src/ref_backend/api/main.py
index 6a59758..23b8c55 100644
--- a/backend/src/ref_backend/api/main.py
+++ b/backend/src/ref_backend/api/main.py
@@ -1,11 +1,12 @@
from fastapi import APIRouter
-from ref_backend.api.routes import aft, datasets, diagnostics, executions, results, utils
+from ref_backend.api.routes import aft, datasets, diagnostics, executions, explorer, results, utils
api_router = APIRouter()
api_router.include_router(aft.router)
api_router.include_router(datasets.router)
api_router.include_router(diagnostics.router)
api_router.include_router(executions.router)
+api_router.include_router(explorer.router)
api_router.include_router(results.router)
api_router.include_router(utils.router)
diff --git a/backend/src/ref_backend/api/routes/explorer.py b/backend/src/ref_backend/api/routes/explorer.py
new file mode 100644
index 0000000..f49104b
--- /dev/null
+++ b/backend/src/ref_backend/api/routes/explorer.py
@@ -0,0 +1,40 @@
+from fastapi import APIRouter, HTTPException
+
+from ref_backend.core.collections import (
+ AFTCollectionDetail,
+ AFTCollectionSummary,
+ ThemeDetail,
+ ThemeSummary,
+ get_collection_by_id,
+ get_collection_summaries,
+ get_theme_by_slug,
+ get_theme_summaries,
+)
+
+router = APIRouter(prefix="/explorer", tags=["Explorer"])
+
+
+@router.get("/collections/", response_model=list[AFTCollectionSummary])
+async def list_collections() -> list[AFTCollectionSummary]:
+ return get_collection_summaries()
+
+
+@router.get("/collections/{collection_id}", response_model=AFTCollectionDetail)
+async def get_collection(collection_id: str) -> AFTCollectionDetail:
+ result = get_collection_by_id(collection_id)
+ if result is None:
+ raise HTTPException(status_code=404, detail=f"Collection '{collection_id}' not found")
+ return result
+
+
+@router.get("/themes/", response_model=list[ThemeSummary])
+async def list_themes() -> list[ThemeSummary]:
+ return get_theme_summaries()
+
+
+@router.get("/themes/{theme_slug}", response_model=ThemeDetail)
+async def get_theme(theme_slug: str) -> ThemeDetail:
+ result = get_theme_by_slug(theme_slug)
+ if result is None:
+ raise HTTPException(status_code=404, detail=f"Theme '{theme_slug}' not found")
+ return result
diff --git a/backend/src/ref_backend/core/aft.py b/backend/src/ref_backend/core/aft.py
index e9a9ba8..2d49cb1 100644
--- a/backend/src/ref_backend/core/aft.py
+++ b/backend/src/ref_backend/core/aft.py
@@ -1,160 +1,43 @@
import logging
from functools import lru_cache
-from pathlib import Path
-
-import pandas as pd
-import yaml
-from pydantic import ValidationError
+from ref_backend.core.collections import AFTCollectionDetail, load_all_collections
from ref_backend.models import AFTDiagnosticBase, AFTDiagnosticDetail, AFTDiagnosticSummary, RefDiagnosticLink
logger = logging.getLogger(__name__)
-def get_aft_paths() -> tuple[Path, Path]:
- """
- Get paths to the official AFT CSV and YAML mapping files.
-
- This can be overridden in tests.
- """
- static_dir = Path(__file__).parents[3] / "static" / "aft"
-
- aft_csv = static_dir / "AFT REF Diagnostics-v2_draft_clean.csv"
- aft_yaml = static_dir / "ref_mapping.yaml"
-
- return aft_csv, aft_yaml
+def _collection_to_base(col: AFTCollectionDetail) -> AFTDiagnosticBase:
+ """Convert a collection detail to an AFTDiagnosticBase."""
+ return AFTDiagnosticBase(
+ id=col.id,
+ name=col.name,
+ theme=col.theme,
+ version_control=col.version_control,
+ reference_dataset=col.reference_dataset,
+ endorser=col.endorser,
+ provider_link=col.provider_link,
+ description=col.content.description if col.content else None,
+ short_description=col.content.short_description if col.content else None,
+ )
@lru_cache(maxsize=1)
def load_official_aft_diagnostics() -> list[AFTDiagnosticBase]:
"""
- Load official AFT diagnostics from CSV file.
+ Load official AFT diagnostics from collection YAML files.
Returns
-------
- List of AFTDiagnostic instances
-
- Raises
- ------
- ValueError: If CSV schema is invalid
+ List of AFTDiagnosticBase instances sorted by id
"""
- diagnostics: list[AFTDiagnosticBase] = []
- csv_path, _ = get_aft_paths()
-
- expected_headers = {
- "id",
- "name",
- "theme",
- "version_control",
- "reference_dataset",
- "endorser",
- "provider_link",
- "description",
- "short_description",
- }
- try:
- df = pd.read_csv(csv_path)
-
- if set(df.columns) != {
- "id",
- "name",
- "theme",
- "version_control",
- "reference_dataset",
- "endorser",
- "provider_link",
- "description",
- "short_description",
- }:
- raise ValueError(f"CSV headers mismatch. Expected: {expected_headers}, Got: {set(df.columns)}")
-
- if df.id.unique().size != len(df):
- raise ValueError("Duplicate 'id' values found in AFT CSV")
-
- # Clean data: strip whitespace and convert empty strings to None
- for key in df.columns:
- df[key] = df[key].astype(str).str.strip().replace({"": None}) # type: ignore
-
- for _, row in df.iterrows(): # Start at 2 since row 1 is headers
- try:
- diagnostic = AFTDiagnosticBase(**row.to_dict()) # type: ignore
- diagnostics.append(diagnostic)
- except ValidationError as e:
- raise ValueError(f"Validation error at row {row}: {e}") from e
-
- except FileNotFoundError:
- raise ValueError(f"AFT CSV file not found: {csv_path}")
- except Exception as e:
- raise ValueError(f"Error loading AFT CSV: {e}") from e
-
- # Sort by id for stable ordering
+ collections = load_all_collections()
+ diagnostics = [_collection_to_base(col) for col in collections.values()]
diagnostics.sort(key=lambda d: d.id)
-
return diagnostics
@lru_cache(maxsize=1)
-def load_ref_mapping() -> dict[str, list[RefDiagnosticLink]]:
- """
- Load REF diagnostic mapping from YAML file.
-
- Returns
- -------
- Dict mapping AFT ID to list of RefDiagnosticLink
-
- Raises
- ------
- ValueError: If YAML schema is invalid
- """
- _, yaml_path = get_aft_paths()
- try:
- with open(yaml_path, encoding="utf-8") as f:
- data = yaml.safe_load(f) or {}
- except FileNotFoundError:
- raise ValueError(f"AFT mapping YAML file not found: {yaml_path}")
- except Exception as e:
- raise ValueError(f"Error loading AFT mapping YAML: {e}") from e
-
- data = {str(k): v for k, v in data.items()} # Ensure keys are strings
-
- mapping = {}
- official_ids = {d.id for d in load_official_aft_diagnostics()}
-
- for aft_id, refs in data.items():
- if aft_id not in official_ids:
- logger.warning(f"AFT_CSV: Unknown AFT ID '{aft_id}' in {official_ids} official IDs), ignoring")
- continue
-
- if not isinstance(refs, list):
- raise ValueError(f"AFT_MAPPING: Expected list for AFT ID '{aft_id}', got {type(refs)}")
-
- links = []
- seen = set()
- for ref in refs:
- if not isinstance(ref, dict):
- raise ValueError(f"AFT_MAPPING: Expected dict for ref in AFT ID '{aft_id}', got {type(ref)}")
-
- provider_slug = ref.get("provider_slug")
- diagnostic_slug = ref.get("diagnostic_slug")
-
- if not provider_slug or not diagnostic_slug:
- raise ValueError(
- f"AFT_MAPPING: Missing provider_slug or diagnostic_slug in AFT ID '{aft_id}'"
- )
-
- key = (provider_slug, diagnostic_slug)
- if key in seen:
- logger.warning(f"AFT_MAPPING: Duplicate ref {key} for AFT ID '{aft_id}', deduplicating")
- continue
- seen.add(key)
-
- links.append(RefDiagnosticLink(provider_slug=provider_slug, diagnostic_slug=diagnostic_slug))
-
- mapping[aft_id] = links
-
- return mapping
-
-
def get_aft_diagnostics_index() -> list[AFTDiagnosticSummary]:
"""
Get all AFT diagnostics as summaries.
@@ -179,21 +62,39 @@ def get_aft_diagnostic_by_id(aft_id: str) -> AFTDiagnosticDetail | None:
-------
AFTDiagnosticDetail if found, None otherwise
"""
- diagnostics = load_official_aft_diagnostics()
- mapping = load_ref_mapping()
+ collections = load_all_collections()
+ col = collections.get(aft_id)
+ if col is None:
+ return None
- for d in diagnostics:
- if d.id == aft_id:
- refs = mapping.get(aft_id, [])
- return AFTDiagnosticDetail(**d.model_dump(), diagnostics=refs)
+ base = _collection_to_base(col)
+ refs = [
+ RefDiagnosticLink(provider_slug=d.provider_slug, diagnostic_slug=d.diagnostic_slug)
+ for d in col.diagnostics
+ ]
+ return AFTDiagnosticDetail(**base.model_dump(), diagnostics=refs)
- return None
+
+@lru_cache(maxsize=1)
+def _build_ref_to_aft_index() -> dict[tuple[str, str], str]:
+ """Build a reverse index from (provider_slug, diagnostic_slug) to AFT collection ID."""
+ index: dict[tuple[str, str], str] = {}
+ for col_id, col in load_all_collections().items():
+ for diag in col.diagnostics:
+ key = (diag.provider_slug, diag.diagnostic_slug)
+ if key in index:
+ logger.warning(
+ f"Multiple AFT IDs for {diag.provider_slug}/{diag.diagnostic_slug}: "
+ f"{index[key]} and {col_id}, keeping first"
+ )
+ continue
+ index[key] = col_id
+ return index
-@lru_cache(maxsize=128)
def get_aft_for_ref_diagnostic(provider_slug: str, diagnostic_slug: str) -> str | None:
"""
- Get AFT diagnostic associated with a REF diagnostic.
+ Get AFT diagnostic ID associated with a REF diagnostic.
Args:
provider_slug: Provider slug
@@ -201,20 +102,6 @@ def get_aft_for_ref_diagnostic(provider_slug: str, diagnostic_slug: str) -> str
Returns
-------
- The AFT diagnostic if found, None otherwise
+ The AFT diagnostic ID if found, None otherwise
"""
- mapping = load_ref_mapping()
- aft_ids = []
-
- for aft_id, ref_diagnostics in mapping.items():
- logger.info(f"Checking AFT ID {aft_id} with refs {ref_diagnostics}")
- for ref in ref_diagnostics:
- if ref.provider_slug == provider_slug and ref.diagnostic_slug == diagnostic_slug:
- aft_ids.append(aft_id)
-
- if len(aft_ids) == 1:
- return aft_ids[0]
- if len(aft_ids) > 1:
- logger.warning(f"Multiple AFT IDs found for {provider_slug}/{diagnostic_slug}: {aft_ids}")
- return aft_ids[0]
- return None
+ return _build_ref_to_aft_index().get((provider_slug, diagnostic_slug))
diff --git a/backend/src/ref_backend/core/collections.py b/backend/src/ref_backend/core/collections.py
new file mode 100644
index 0000000..d8269df
--- /dev/null
+++ b/backend/src/ref_backend/core/collections.py
@@ -0,0 +1,268 @@
+import logging
+from functools import lru_cache
+from pathlib import Path
+from typing import Literal
+
+import yaml
+from pydantic import BaseModel, HttpUrl, ValidationError
+
+from ref_backend.core.diagnostic_metadata import (
+ DiagnosticMetadata,
+ ReferenceDatasetLink,
+ load_diagnostic_metadata,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AFTCollectionGroupingConfig(BaseModel):
+ group_by: str
+ hue: str
+ style: str | None = None
+
+
+class AFTCollectionCardContent(BaseModel):
+ type: Literal["box-whisker-chart", "figure-gallery", "series-chart", "taylor-diagram"]
+ provider: str
+ diagnostic: str
+ title: str
+ description: str | None = None
+ interpretation: str | None = None
+ span: Literal[1, 2] | None = None
+ placeholder: bool | None = None
+ metric_units: str | None = None
+ clip_min: float | None = None
+ clip_max: float | None = None
+ y_min: float | None = None
+ y_max: float | None = None
+ show_zero_line: bool | None = None
+ symmetrical_axes: bool | None = None
+ reference_stddev: float | None = None
+ label_template: str | None = None
+ other_filters: dict[str, str] | None = None
+ grouping_config: AFTCollectionGroupingConfig | None = None
+ reference_datasets: list[ReferenceDatasetLink] | None = None
+
+
+class AFTCollectionCard(BaseModel):
+ title: str
+ description: str | None = None
+ placeholder: bool | None = None
+ content: list[AFTCollectionCardContent]
+
+
+class AFTCollectionPlainLanguage(BaseModel):
+ description: str | None = None
+ why_it_matters: str | None = None
+ takeaway: str | None = None
+
+
+class AFTCollectionContent(BaseModel):
+ description: str | None = None
+ short_description: str | None = None
+ why_it_matters: str | None = None
+ takeaway: str | None = None
+ plain_language: AFTCollectionPlainLanguage | None = None
+
+
+class AFTCollectionDiagnosticLink(BaseModel):
+ provider_slug: str
+ diagnostic_slug: str
+ provider_link: HttpUrl | None = None
+
+
+class AFTCollectionSummary(BaseModel):
+ id: str
+ name: str
+ theme: str | None = None
+ endorser: str | None = None
+ card_count: int
+
+
+class AFTCollectionDetail(BaseModel):
+ id: str
+ name: str
+ theme: str | None = None
+ endorser: str | None = None
+ version_control: str | None = None
+ reference_dataset: str | None = None
+ provider_link: HttpUrl | None = None
+ content: AFTCollectionContent | None = None
+ diagnostics: list[AFTCollectionDiagnosticLink]
+ explorer_cards: list[AFTCollectionCard]
+
+
+class ThemeSummary(BaseModel):
+ slug: str
+ title: str
+ description: str | None = None
+ collection_count: int
+ card_count: int
+
+
+class ThemeDetail(BaseModel):
+ slug: str
+ title: str
+ description: str | None = None
+ collections: list[AFTCollectionDetail]
+ explorer_cards: list[AFTCollectionCard]
+
+
+def get_diagnostics_dir() -> Path:
+ return Path(__file__).parents[3] / "static" / "diagnostics"
+
+
+def get_collections_dir() -> Path:
+ return Path(__file__).parents[3] / "static" / "collections"
+
+
+def get_themes_path() -> Path:
+ return get_collections_dir() / "themes.yaml"
+
+
+def _enrich_card_content_with_ref_datasets(
+ collection: AFTCollectionDetail,
+ metadata: dict[str, DiagnosticMetadata],
+) -> None:
+ """Populate reference_datasets on each card content item from diagnostic metadata."""
+ for card in collection.explorer_cards:
+ for content in card.content:
+ key = f"{content.provider}/{content.diagnostic}"
+ if key in metadata and metadata[key].reference_datasets:
+ content.reference_datasets = metadata[key].reference_datasets
+
+
+@lru_cache(maxsize=1)
+def _load_diagnostic_metadata_cached() -> dict[str, DiagnosticMetadata]:
+ return load_diagnostic_metadata(get_diagnostics_dir())
+
+
+@lru_cache(maxsize=1)
+def load_all_collections() -> dict[str, AFTCollectionDetail]:
+ collections_dir = get_collections_dir()
+
+ if not collections_dir.exists():
+ logger.warning(f"Collections directory not found: {collections_dir}")
+ return {}
+
+ diagnostic_metadata = _load_diagnostic_metadata_cached()
+ result: dict[str, AFTCollectionDetail] = {}
+
+ yaml_files = sorted(
+ [p for p in collections_dir.glob("*.yaml") if p.name != "themes.yaml"],
+ key=lambda p: p.stem,
+ )
+
+ for yaml_file in yaml_files:
+ try:
+ with open(yaml_file, encoding="utf-8") as f:
+ data = yaml.safe_load(f)
+
+ if not isinstance(data, dict):
+ logger.warning(f"Skipping {yaml_file.name}: expected a YAML mapping at top level")
+ continue
+
+ collection = AFTCollectionDetail(**data)
+ _enrich_card_content_with_ref_datasets(collection, diagnostic_metadata)
+
+ if collection.id in result:
+ logger.warning(f"Duplicate collection ID '{collection.id}' in {yaml_file.name}, skipping")
+ continue
+
+ result[collection.id] = collection
+
+ except ValidationError as e:
+ logger.warning(f"Skipping {yaml_file.name}: validation error: {e}")
+ except Exception as e:
+ logger.warning(f"Skipping {yaml_file.name}: unexpected error: {e}")
+
+ return result
+
+
+def get_collection_by_id(collection_id: str) -> AFTCollectionDetail | None:
+ return load_all_collections().get(collection_id)
+
+
+def get_collection_summaries() -> list[AFTCollectionSummary]:
+ collections = load_all_collections()
+ return [
+ AFTCollectionSummary(
+ id=c.id,
+ name=c.name,
+ theme=c.theme,
+ endorser=c.endorser,
+ card_count=len(c.explorer_cards),
+ )
+ for c in sorted(collections.values(), key=lambda c: c.id)
+ ]
+
+
+@lru_cache(maxsize=1)
+def load_theme_mapping() -> dict[str, ThemeDetail]:
+ themes_path = get_themes_path()
+
+ if not themes_path.exists():
+ logger.warning(f"Themes file not found: {themes_path}")
+ return {}
+
+ try:
+ with open(themes_path, encoding="utf-8") as f:
+ data = yaml.safe_load(f) or {}
+ except Exception as e:
+ logger.warning(f"Error loading themes.yaml: {e}")
+ return {}
+
+ all_collections = load_all_collections()
+ result: dict[str, ThemeDetail] = {}
+
+ for slug, theme_data in data.items():
+ try:
+ if not isinstance(theme_data, dict):
+ logger.warning(f"Skipping theme '{slug}': expected a mapping")
+ continue
+
+ title = theme_data["title"]
+ description = theme_data.get("description")
+ collection_ids: list[str] = theme_data.get("collections", [])
+
+ collections: list[AFTCollectionDetail] = []
+ explorer_cards: list[AFTCollectionCard] = []
+
+ for cid in collection_ids:
+ col = all_collections.get(cid)
+ if col is None:
+ logger.warning(f"Theme '{slug}' references unknown collection ID '{cid}', skipping")
+ continue
+ collections.append(col)
+ explorer_cards.extend(col.explorer_cards)
+
+ result[slug] = ThemeDetail(
+ slug=slug,
+ title=title,
+ description=description,
+ collections=collections,
+ explorer_cards=explorer_cards,
+ )
+
+ except (KeyError, TypeError) as e:
+ logger.warning(f"Skipping theme '{slug}': {e}")
+
+ return result
+
+
+def get_theme_summaries() -> list[ThemeSummary]:
+ themes = load_theme_mapping()
+ return [
+ ThemeSummary(
+ slug=t.slug,
+ title=t.title,
+ description=t.description,
+ collection_count=len(t.collections),
+ card_count=len(t.explorer_cards),
+ )
+ for t in themes.values()
+ ]
+
+
+def get_theme_by_slug(slug: str) -> ThemeDetail | None:
+ return load_theme_mapping().get(slug)
diff --git a/backend/src/ref_backend/core/config.py b/backend/src/ref_backend/core/config.py
index 0b2aa27..4ad3ca1 100644
--- a/backend/src/ref_backend/core/config.py
+++ b/backend/src/ref_backend/core/config.py
@@ -63,25 +63,26 @@ def all_cors_origins(self) -> list[str]:
DIAGNOSTIC_METADATA_PATH: Path | None = None
"""
- Path to the diagnostic metadata YAML file.
+ Path to the diagnostic metadata YAML file or directory.
- This file provides additional metadata for diagnostics that can override or supplement
- the default values from diagnostic implementations. If not provided, defaults to
- 'static/diagnostics/metadata.yaml' relative to the backend directory.
+ Provides additional metadata for diagnostics that can override or supplement
+ the default values from diagnostic implementations. Accepts either a single
+ YAML file or a directory of YAML files (one per provider). If not provided,
+ defaults to 'static/diagnostics/' relative to the backend directory.
"""
@computed_field # type: ignore[prop-decorator]
@property
def diagnostic_metadata_path_resolved(self) -> Path:
"""
- Get the resolved path to the diagnostic metadata file.
+ Get the resolved path to the diagnostic metadata file or directory.
Returns the configured path or the default location.
"""
if self.DIAGNOSTIC_METADATA_PATH is not None:
return self.DIAGNOSTIC_METADATA_PATH
- # Default to static/diagnostics/metadata.yaml relative to backend directory
- return Path(__file__).parent.parent.parent.parent / "static" / "diagnostics" / "metadata.yaml"
+ # Default to static/diagnostics/ directory relative to backend directory
+ return Path(__file__).parent.parent.parent.parent / "static" / "diagnostics"
def _check_default_secret(self, var_name: str, value: str | None) -> None:
if value == "changethis":
diff --git a/backend/src/ref_backend/core/diagnostic_metadata.py b/backend/src/ref_backend/core/diagnostic_metadata.py
index ce542de..c102398 100644
--- a/backend/src/ref_backend/core/diagnostic_metadata.py
+++ b/backend/src/ref_backend/core/diagnostic_metadata.py
@@ -54,13 +54,9 @@ class DiagnosticMetadata(BaseModel):
tags: list[str] | None = Field(None, description="Tags for categorizing the diagnostic")
-def load_diagnostic_metadata(yaml_path: Path) -> dict[str, DiagnosticMetadata]:
+def _load_metadata_from_file(yaml_path: Path) -> dict[str, DiagnosticMetadata]:
"""
- Load diagnostic metadata from a YAML file.
-
- This function loads metadata overrides from a YAML file, which should follow
- the structure defined in DiagnosticMetadata. The file uses diagnostic keys
- in the format "provider-slug/diagnostic-slug" to map metadata to diagnostics.
+ Load diagnostic metadata from a single YAML file.
Args:
yaml_path: Path to the YAML metadata file
@@ -68,27 +64,8 @@ def load_diagnostic_metadata(yaml_path: Path) -> dict[str, DiagnosticMetadata]:
Returns
-------
Dictionary mapping diagnostic keys (provider/diagnostic) to their metadata.
- Returns an empty dict if the file doesn't exist or cannot be parsed.
-
- Example YAML structure:
- ```yaml
- pmp/annual-cycle:
- reference_datasets:
- - slug: "obs4mips.CERES-EBAF.v4.2"
- description: "CERES Energy Balanced and Filled"
- type: "primary"
- display_name: "Annual Cycle Analysis"
- tags: ["atmosphere", "seasonal-cycle"]
- ```
+ Returns an empty dict if the file cannot be parsed.
"""
- if not yaml_path.exists():
- logger.warning(
- f"Diagnostic metadata file not found at {yaml_path}. "
- "No metadata overrides will be applied. This is expected if you haven't "
- "created the metadata file yet."
- )
- return {}
-
try:
with open(yaml_path) as f:
raw_data = yaml.safe_load(f)
@@ -97,7 +74,6 @@ def load_diagnostic_metadata(yaml_path: Path) -> dict[str, DiagnosticMetadata]:
logger.info(f"Diagnostic metadata file at {yaml_path} is empty.")
return {}
- # Parse each diagnostic's metadata
metadata_dict: dict[str, DiagnosticMetadata] = {}
for diagnostic_key, metadata_raw in raw_data.items():
try:
@@ -117,3 +93,65 @@ def load_diagnostic_metadata(yaml_path: Path) -> dict[str, DiagnosticMetadata]:
except Exception as e:
logger.error(f"Unexpected error loading diagnostic metadata from {yaml_path}: {e}")
return {}
+
+
+def load_diagnostic_metadata(path: Path) -> dict[str, DiagnosticMetadata]:
+ """
+ Load diagnostic metadata from a YAML file or a directory of YAML files.
+
+ When given a directory, all ``*.yaml`` and ``*.yml`` files in it are loaded
+ and merged. When given a single file, it is loaded directly (backwards
+ compatible with the old single-file layout).
+
+ The YAML files should follow the structure defined in DiagnosticMetadata,
+ using diagnostic keys in the format ``provider-slug/diagnostic-slug``.
+
+ Args:
+ path: Path to a YAML file or a directory containing YAML files
+
+ Returns
+ -------
+ Dictionary mapping diagnostic keys (provider/diagnostic) to their metadata.
+ Returns an empty dict if the path doesn't exist or cannot be parsed.
+
+ Example YAML structure::
+
+ pmp/annual-cycle:
+ reference_datasets:
+ - slug: "obs4mips.CERES-EBAF.v4.2"
+ description: "CERES Energy Balanced and Filled"
+ type: "primary"
+ display_name: "Annual Cycle Analysis"
+ tags: ["atmosphere", "seasonal-cycle"]
+ """
+ if not path.exists():
+ logger.warning(
+ f"Diagnostic metadata path not found at {path}. "
+ "No metadata overrides will be applied. This is expected if you haven't "
+ "created the metadata files yet."
+ )
+ return {}
+
+ if path.is_file():
+ return _load_metadata_from_file(path)
+
+ # Load all YAML files from the directory
+ yaml_files = sorted(path.glob("*.yaml")) + sorted(path.glob("*.yml"))
+ if not yaml_files:
+ logger.info(f"No YAML files found in directory {path}.")
+ return {}
+
+ merged: dict[str, DiagnosticMetadata] = {}
+ for yaml_file in yaml_files:
+ file_metadata = _load_metadata_from_file(yaml_file)
+ # Check for duplicate keys across files
+ duplicates = set(merged.keys()) & set(file_metadata.keys())
+ if duplicates:
+ logger.warning(
+ f"Duplicate diagnostic keys found in {yaml_file.name}: "
+ f"{', '.join(sorted(duplicates))}. Later values will override earlier ones."
+ )
+ merged.update(file_metadata)
+
+ logger.info(f"Loaded metadata for {len(merged)} diagnostics from {len(yaml_files)} files in {path}")
+ return merged
diff --git a/backend/static/aft/AFT REF Diagnostics-v2_draft_clean.csv b/backend/static/aft/AFT REF Diagnostics-v2_draft_clean.csv
deleted file mode 100644
index 2e45eaa..0000000
--- a/backend/static/aft/AFT REF Diagnostics-v2_draft_clean.csv
+++ /dev/null
@@ -1,26 +0,0 @@
-id,name,theme,version_control,reference_dataset,endorser,provider_link,description,short_description
-1.1,"Antarctic annual mean, Arctic September rate of sea ice area (SIA) loss per degree warming (dSIA / dGMST)",Oceans and sea ice,version 1 - 24-11-04 REF launch,"OSI SAF/CCI, HadCRUT",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_seaice_sensitivity.html,"This metric evaluates the rate of sea ice loss per degree of global warming, following the approach used for sea ice benchmarking within the Sea Ice Model Intercomparison Project analysis (Notz and SIMIP Community, 2020; Roach et al., 2020). The metric is calculated by regressing the time-series of sea ice area on global mean temperature. Sea ice responds strongly to climate forcing and warming","Rate of sea ice area loss per degree of warming, as in plot 1d of Notz et al. and figure 3e of Roach et al."
-1.2,Atlantic meridional overturning circulation (AMOC),Oceans and sea ice,version 1 - 24-11-04 REF launch,RAPID-v2023-1,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,"Provides a key indicator of the strength of ocean circulation, which redistributes freshwater, heat and carbon across the Atlantic Basin (Le Bras et al., 2023). The AMOC is a key component of the global ocean conveyor belt and plays an important role in transporting heat poleward and ocean biogeochemical tracers from the surface into the ocean interior. The strength of the AMOC at 26.5◦N is commonly used for evaluation of model fidelity since it can be compared with the long-term RAPID-MOCHA (Rapid Climate Change - Meridional Overturning Circulation and Heatflux Array) observational dataset (Moat et al., 2025)",Comparison of the model Atlantic meridional ocean circulation (AMOC) circulation strength with refernce data from RAPID-v2023-1
-1.3,"El Niño Southern Oscillation (ENSO) diagnostics (lifecycle, seasonality, amplitude, teleconnections)",Oceans and sea ice,version 1 - 24-11-04 REF launch,"CMAP-V1902, TropFlux-1-0, AVISO-1-0, ERA-5, GPCP-SG-2-3, HadISST-1-1",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_enso_ref.html,"The El Niño Southern Oscillation (ENSO) is the primary mode of the global interannual climate variability, mainly reflected by the variations in surface wind stress and ocean temperature in the tropical Pacific Ocean. The ENSO variability can be calculated from both sea surface temperature and atmospheric pressure differences 680 between different tropical Pacific areas. The Southern Oscillation Index (SOI) uses pressure differences between the Tahiti and Darwin regions. The Oceanic Niño Index (ONI) summarizes SST anomalies in the Niño 3.4 region.Given its implications for regional climate variability, capturing the observed ENSO spatial and temporal characteristics would increase the fidelity and robustness in a model’s climate projections.",ENSO CLIVAR metrics - reproducing background climatology and ENSO characteristics
-1.4,"Sea surface temperature (SST) bias, Sea surface salinity (SSS) bias",Oceans and sea ice,version 1 - 24-11-04 REF launch,WOA2023,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,"Th Sea surface temperature (SST) bias, Sea surface salinity (SSS) distributions provide large scale patterns of surface ocean circulation as well as reflecting dynamical air-sea interactions and ocean-sea ice interactions in the polar regions. SST and SSS biases have a significant impact on the coupling of ESM’s two majors components, the atmosphere and the ocean. Satellite data products and localized moored sensors are used to produce measurements that are incorporated into reference data to calculate SST and SSS biases in models",Apply the ILAMB Methodology* to compare the model sea surface temperature (SST) and sea surface salinity (SSS) with reference data from WOA2023
-1.5,Ocean heat content (OHC),Oceans and sea ice,version 1 - 24-11-04 REF launch,MGOHCTA-WOA09,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,"The Ocean Heat Content (OHC) may provide one of the most reliable signals about the long-term climate change and decadal to multidecadal variablity, including their temporal variation and spatial patterns. It is compared, between models and observations, on a gridded basis (1◦ × 1◦), based on almost all available in situ ocean observations (e.g., Argo, conductivity–temperature–depth (CTD) profilers, Mechanical BathyThermographs, bottles, moorings, gliders, and animal-borne ocean sensors; Cheng et al., 2024). Before use, the data are carefully bias corrected, vertically and horizontally interpolated and mapped onto a grid for comparison with models",Apply the ILAMB Methodology* to compare the model ocean heat content (OHC) with reference data from MGOHCTA-WOA09
-1.6,Antarctic & Arctic sea ice area seasonal cycle,Oceans and sea ice,version 1 - 24-11-04 REF launch,OSISAF-V3,CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html,"The sea ice area, calculated as the sum over the Northern (Arctic) and Southern (Antarctic) Hemisphere grid cell areas multiplied by the sea ice fraction within each cell, exhibits a distinct seasonal cycle. Arctic sea ice area typically has minimum values in September, while Antarctic sea ice area is lowest in February. The seasonal cycle is driven by the seasonal cycle of the insolation, sea ice processes, as well as the exchange with the atmosphere and ocean and can be seen as an overview metric for the general state of the sea ice in a model. In addition to the multi-year average seasonal cycle of Arctic and Antarctic sea ice area, the diagnostic produces time series of the September (Arctic) and February (Antarctic) sea ice area.","Seasonal cycle of Arctic (NH) and Antarctic (SH) sea ice area, time series of Arctic September (NH) and Antarctic February (SH) sea ice area"
-2.1,Soil carbon,Land and land ice,version 1 - 24-11-04 REF launch,"HWSD-2-0, NCSCD-2-2",CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,Soil carbon is the organic matter and inorganic carbon in global soils. It is an important component of the global carbon cycle and affects soil moisture retention and saturation. Analyzing stored soil carbon helps track quantify the dynamics of the terrestrial carbon cycle within models and the movement of carbon through the Earth system.,Apply the ILAMB Methodology* to compare the model soil carbon with reference data from HWSD-2-0 and NCSCD-2-2
-2.2,Gross primary production (GPP),Land and land ice,version 1 - 24-11-04 REF launch,"FLUXNET2015-1-0, WECANN-1-0, CRU-4-9 (source_id not confirmed)",CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,"Gross primary production is the process by which plants “fix” atmospheric or aqueous carbon dioxide through photosynthetic reduction into organic compounds, and it is affected by increases in atmospheric carbon dioxide (CO2) levels and warming (Anav et al., 2015). A fraction of gross primary productivity supports plant respiration and the rest is stored as biomass in stems, leaves, roots, or other plant parts. Land use change, heat and drought stress due to anthropogenic warming, and rising atmospheric CO2 will differentially influence gross primary production in ecosystems and alter the global carbon cycle. Thus, models must be evaluated to ensure they capture the observed responses to these changes.",Apply the ILAMB Methodology* to compare the model gross primary production (GPP) with reference data from FLUXNET2015-1-0 and WECANN-1-0
-2.3,Runoff,Land and land ice,version 1 - 24-11-04 REF launch,LORA-1-1,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,"Surface water runoff plays an important role in the hydrological cycle by returning excess precipitation to the oceans and controlling how much water flows into water systems (Trenberth et al., 2007; Trenberth and Caron, 2001). Changes in atmospheric circulation and distributions of precipitation have a direct effect on changes in runoff from land. Models must be evaluated to ensure they exhibit the observed responses to precipitation and soil moisture processes that lead to runoff and transport of freshwater into rivers and oceans.",Apply the ILAMB Methodology* to compare the model surface runoff with reference data from LORA-1-1
-2.4,Surface soil moisture,Land and land ice,version 1 - 24-11-04 REF launch,OLC-ORS-V0,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,,Apply the ILAMB Methodology* to compare the model surface soil moisture with reference data from OLC-ORS-V0
-2.5,Net ecosystem carbon balance,Land and land ice,version 1 - 24-11-04 REF launch,HOFFMAN-1-0,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,,Comparison of the model integrated land net ecosystem carbon balance with reference data from HOFFMAN-1-0
-2.6,Leaf area index (LAI),Land and land ice,version 1 - 24-11-04 REF launch,"NOAA-NCEI-LAI-5-0, LAI4g-1-2",CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,,"Apply the ILAMB Methodology* to compare the model leaf area index (LAI) with reference data from NOAA-NCEI-LAI-5-0, and LAI4g-1-2"
-2.7,Snow cover,Land and land ice,version 1 - 24-11-04 REF launch,CCI-CryoClim-FSC-1,CMIP Model Benchmarking Task Team,https://doi.org/10.1029/2018MS001354,,Apply the ILAMB Methodology* to compare the model snow cover with reference data from CCI-CryoClim-FSC-1
-3.1,Annual cycle and seasonal mean of multiple variables,Atmosphere,version 1 - 24-11-04 REF launch,"C3S-GTO-ECV-9-0, SAGE-CCI-OMPS-v0008, ERA-5",CMIP Model Benchmarking Task Team,http://pcmdi.github.io/pcmdi_metrics/examples/Demo_1b_mean_climate.html,,"Maps of seasonal and annual climatology are generated for the reference datasets and model output on a target grid (2.5x2.5 deg), then calculate diverse metrics including bias, RMSE, spatial pattern correlation, and standard deviation."
-3.2,Radiative and heat fluxes at the surface and top of atmosphere (TOA),Atmosphere,version 1 - 24-11-04 REF launch,CERES-EBAF-4-2,CMIP Model Benchmarking Task Team,http://pcmdi.github.io/pcmdi_metrics/examples/Demo_1a_compute_climatologies.html,,"Maps of seasonal and annual climatology are generated for the reference dataset and model output on a target grid (2.5x2.5 deg), then calculate diverse metrics including bias, RMSE, spatial pattern correlation, and standard deviation."
-3.3,"Climate variability modes (e.g., ENSO, Madden-Julian Oscillation (MJO), Extratropical modes of variability, monsoon)",Atmosphere,version 1 - 24-11-04 REF launch,"20CR-V2, HadISST-1-1",CMIP Model Benchmarking Task Team,http://pcmdi.github.io/pcmdi_metrics/metrics.html,,"For extratropical modes of variability, maps of variability mode pattern and thier principal component time series are generated from the reference dataset and model output. Then maps are compared to calculate bias, RMSE, and spatial pattern correlation, and time series are compared to calculate the ratio from their stand deviations."
-3.6,Cloud radiative effects,Atmosphere,version 1 - 24-11-04 REF launch,"CALIPSO-ICECLOUD-1-0, ESACCI-CLOUD-AVHRR-AMPM-3-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html,,Maps and zonal means of longwave and shortwave cloud radiative effect
-3.7,Scatterplots of two cloud-relevant variables (for specific regions of the globe and specific cloud regimes),Atmosphere,version 1 - 24-11-04 REF launch,"CALIPSO-ICECLOUD-1-0, ESACCI-CLOUD-AVHRR-AMPM-3-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html,,2D histograms with focus on clouds
-4.1,Equilibrium climate sensitivity (ECS),Earth System,version 1 - 24-11-04 REF launch,,CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ecs.html,,Equilibrium climate sensitivity is defined as the change in global mean temperature as a result of a doubling of the atmospheric CO2 concentration compared to pre-industrial times after the climate system has reached a new equilibrium. This diagnostic uses a regression method based on Gregory et al. (2004).
-4.2,Transient climate response (TCR),Earth System,version 1 - 24-11-04 REF launch,,CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_tcr.html,,"The transient climate response (TCR) is defined as the global and annual mean surface air temperature anomaly in the 1pctCO2 scenario (1% CO2 increase per year) for a 20 year period centered at the time of CO2 doubling, i.e. using the years 61 to 80 after the start of the simulation. We calculate the temperature anomaly by subtracting a linear fit of the piControl run for all 140 years of the 1pctCO2 experiment prior to the TCR calculation (see Gregory and Forster, 2008)."
-4.3,Transient climate response to cumulative emissions of carbon dioxide (TCRE),Earth System,version 1 - 24-11-04 REF launch,,CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_tcre.html,,"The idea that global temperature rise is directly proportional to the total amount of carbon dioxide (CO2) released into the atmosphere is fundamental to climate policy. The concept stems from research showing a clear linear relationship between cumulative CO2 emissions and global temperature change in climate models (Allen et al. 2009; Matthews et al. 2009; Zickfeld et al. 2009). This relationship is called the Transient Climate Response to Cumulative CO2 Emissions (TCRE), which represents the amount of global warming caused by each trillion tonnes of carbon emitted. This simple yet powerful tool allows policymakers to directly link emission budgets to specific temperature targets and compare the long-term effects of different emissions scenarios."
-4.4,Zero emissions commitment (ZEC),Earth System,version 1 - 24-11-04 REF launch,,CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_zec.html,,"The Zero Emissions Commitment (ZEC) quantifies the change in global mean temperature expected to occur after net carbon dioxide (CO2) emissions cease. ZEC is therefore important to consider when estimating the remaining carbon budget. Calculation of ZEC requires dedicated simulations with emissions set to zero, branching off a base simulation with emissions. In CMIP6 the simulations were part of ZECMIP, with the simulations called esm-1pct-brch-xPgC branching off the 1pctCO2 simulation when emissions reach x PgC. The default x was 1000PgC, with additional simulations for 750PgC and 2000PgC. In CMIP7, ZEC simulations (esm-flat10-zec) are part of the fast track and branch off (esm-flat10) with constant emissions of 10GtC/yr at year 100 (Sanderson 2024)."
-4.5,"Historical changes in climate variables (time series, trends)",Earth System,version 1 - 24-11-04 REF launch,"HadCRUT5-0-2-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3, HadISST-1-1",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html,,"Time series, linear trend, and annual cycle for IPCC regions"
-5.3,Evaluation of key climate variables at global warming levels,Impacts and Adaptation,version 1 - 24-11-04 REF launch,"GPCP-SG-2-3, HadCRUT5-0-2-0",CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_calculate_gwl_exceedance_stats.html,,"This diagnostic calculates years of Global Warming Level (GWL) exceedances in CMIP models as described in Swaminathan et al (2022). Time series of the anomalies in annual global mean surface air temperature (GSAT) are calculated with respect to the 1850-1900 time-mean of each individual time series. To limit the influence of short-term variability, a 21-year centered running mean is applied to the time series. The year at which the time series exceeds warming levels or temperatures such as 1.5C is then recorded for the specific model ensemble member and future scenario. Once the years of exceedance are calculated, the time averaged global mean and standard deviation for the multimodel ensemble over the 21-year period around the year of exceedance are plotted."
-5.4,"Climate drivers for fire (fire burnt area, fire weather and fuel continuity)",Impacts and Adaptation,version 1 - 24-11-04 REF launch,GFED-5 (source_id not confirmed),CMIP Model Benchmarking Task Team,https://docs.esmvaltool.org/en/latest/recipes/recipe_ref_fire.html,,"The diagnostic relies on the processing of fire climate drivers through the ConFire model and is based on Jones et al. (2024). The diagnostic computes the burnt fraction for each grid cell based on a number of drivers. Additionally, the respective controls due to fire weather and fuel load/continuity are computed. The stochastic control corresponds to the unmodelled processed influencing to fire occurrence."
diff --git a/backend/static/aft/ref_mapping.yaml b/backend/static/aft/ref_mapping.yaml
deleted file mode 100644
index a18eccd..0000000
--- a/backend/static/aft/ref_mapping.yaml
+++ /dev/null
@@ -1,110 +0,0 @@
-1.1:
- - provider_slug: esmvaltool
- diagnostic_slug: sea-ice-sensitivity
-1.2:
- - provider_slug: ilamb
- diagnostic_slug: amoc-rapid
-1.3:
- - provider_slug: esmvaltool
- diagnostic_slug: enso-basic-climatology
- - provider_slug: esmvaltool
- diagnostic_slug: enso-characteristics
- - provider_slug: pmp
- diagnostic_slug: enso_proc
- - provider_slug: pmp
- diagnostic_slug: enso_tel
-1.4:
- - provider_slug: ilamb
- diagnostic_slug: so-woa2023-surface
- - provider_slug: ilamb
- diagnostic_slug: thetao-woa2023-surface
-
-1.6:
- - provider_slug: esmvaltool
- diagnostic_slug: sea-ice-area-basic
-2.1:
- - provider_slug: ilamb
- diagnostic_slug: csoil-hwsd2
-2.2:
- - provider_slug: ilamb
- diagnostic_slug: gpp-wecann
-2.3:
- - provider_slug: ilamb
- diagnostic_slug: mrro-lora
-2.4:
- - provider_slug: ilamb
- diagnostic_slug: mrsos-wangmao
-2.5:
- - provider_slug: ilamb
- diagnostic_slug: nbp-hoffman
-2.6:
- - provider_slug: ilamb
- diagnostic_slug: lai-avh15c1
-2.7:
- - provider_slug: ilamb
- diagnostic_slug: snc-esacci
-3.1:
- - provider_slug: pmp
- diagnostic_slug: annual-cycle
- # ESMValTool too?
-3.2: []
- # likely also the pmp diagnostic
- # - provider_slug: pmp
- # diagnostic_slug: annual-cycle
-3.3:
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-nam
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-nao
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-npgo
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-npo
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-pdo
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-pna
- - provider_slug: pmp
- diagnostic_slug: extratropical-modes-of-variability-sam
-3.6:
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-radiative-effects
-3.7:
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-scatterplots-cli-ta
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-scatterplots-clivi-lwcre
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-scatterplots-clt-swcre
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-scatterplots-clwvi-pr
- - provider_slug: esmvaltool
- diagnostic_slug: cloud-scatterplots-reference
-
-4.1:
- - provider_slug: esmvaltool
- diagnostic_slug: equilibrium-climate-sensitivity
-4.2:
- - provider_slug: esmvaltool
- diagnostic_slug: transient-climate-response
-4.3:
- - provider_slug: esmvaltool
- diagnostic_slug: transient-climate-response-emissions
-4.4:
- - provider_slug: esmvaltool
- diagnostic_slug: zero-emission-commitment
-4.5:
- - provider_slug: esmvaltool
- diagnostic_slug: regional-historical-annual-cycle
- - provider_slug: esmvaltool
- diagnostic_slug: regional-historical-timeseries
- - provider_slug: esmvaltool
- diagnostic_slug: regional-historical-trend
-5.3:
- - provider_slug: esmvaltool
- diagnostic_slug: climate-at-global-warming-levels
-5.4:
- - provider_slug: esmvaltool
- diagnostic_slug: climate-drivers-for-fire
- - provider_slug: ilamb
- diagnostic_slug: burntfractionall-gfed
diff --git a/backend/static/collections/1-1_sea-ice-sensitivity.yaml b/backend/static/collections/1-1_sea-ice-sensitivity.yaml
new file mode 100644
index 0000000..064b9b2
--- /dev/null
+++ b/backend/static/collections/1-1_sea-ice-sensitivity.yaml
@@ -0,0 +1,36 @@
+id: "1.1"
+name: "Sea ice sensitivity to warming"
+theme: "Oceans and sea ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "OSI SAF/CCI, HadCRUT"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_seaice_sensitivity.html"
+content:
+ description: >-
+ This metric evaluates the rate of sea ice loss per degree of global warming,
+ following the approach used for sea ice benchmarking within the Sea Ice Model
+ Intercomparison Project analysis (Notz and SIMIP Community, 2020; Roach et al.,
+ 2020). The metric is calculated by regressing the time-series of sea ice area on
+ global mean temperature. This is done on an annual basis using the annual-mean
+ for Antarctic sea ice and the September-mean for Arctic sea ice.
+ short_description: >-
+ Rate of sea ice area loss per degree of warming, as in plot 1d of Notz et al.
+ and figure 3e of Roach et al.
+ why_it_matters: >-
+ The rapid decline in Arctic summer sea ice areal extent is a highly visible
+ indicator of climate change.
+
+ Previous sea ice benchmarking assessments have
+ highlighted the fact that CMIP models systematically underestimate the amount of
+ Arctic sea ice area loss per degree of global warming. Very few CMIP models are
+ able to simulate both a plausible sea ice loss and a plausible change in global
+ mean temperature over the satellite period.
+ takeaway: >-
+ Sea ice responds strongly to climate forcing and so sea ice decline should be
+ considered relative to global warming. Considering the rate of change of sea ice
+ area with warming allows us to consider whether the sea ice is responding to the
+ forcing in the right way.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: sea-ice-sensitivity
+explorer_cards: []
diff --git a/backend/static/collections/1-2_amoc.yaml b/backend/static/collections/1-2_amoc.yaml
new file mode 100644
index 0000000..2d2cb16
--- /dev/null
+++ b/backend/static/collections/1-2_amoc.yaml
@@ -0,0 +1,53 @@
+id: "1.2"
+name: "Atlantic meridional overturning circulation (AMOC)"
+theme: "Oceans and sea ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "RAPID-v2023-1"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ In this diagnostic the AMOC is represented by a two-dimensional streamfunction
+ field, in which the streamfunction at any latitude and any depth equals the
+ vertical integration from the surface to that depth of the zonally-integrated
+ meridional velocity from the western boundary to the eastern boundary of the
+ Atlantic at that latitude.
+ short_description: >-
+ Comparison of the model Atlantic meridional ocean circulation (AMOC) circulation
+ strength with reference data from RAPID-v2023-1
+ why_it_matters: >-
+ The AMOC is one of the major components in the global climate system of the Earth.
+ By the transport of heat, salinity, and nutrients, the AMOC influences the global
+ pattern of climate system and ecosystem. The strengthening, weakening or collapse
+ of the AMOC are usually associated with an abrupt change of the climate system.
+ The possible weakening or even collapse of the AMOC with global warming is still
+ under debate. Historical evidence confirms the existence of multiple states of
+ AMOC, but when and how its sudden transition might occur is still uncertain.
+ takeaway: >-
+ Both the mean condition and the variability of AMOC are important fingerprints of
+ the global climate system. A smaller bias of AMOC relative to the observation may
+ be indicative of better performance of a coupled ESM.
+ plain_language:
+ why_it_matters: >-
+ The AMOC is widely considered a major possible tipping element in the global
+ climate system, and the approach to the AMOC's tipping point with global
+ warming may result in severe consequences to the world.
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: amoc-rapid
+explorer_cards:
+ - title: "AMOC"
+ placeholder: true
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: amoc-rapid
+ title: "AMOC Strength"
+ metric_units: "Sv"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: "None"
+ metric: timeseries
+ statistic: "Period Mean"
diff --git a/backend/static/collections/1-3_enso.yaml b/backend/static/collections/1-3_enso.yaml
new file mode 100644
index 0000000..457ea79
--- /dev/null
+++ b/backend/static/collections/1-3_enso.yaml
@@ -0,0 +1,55 @@
+id: "1.3"
+name: "El Niño Southern Oscillation (ENSO) diagnostics (lifecycle, seasonality, amplitude, teleconnections)"
+theme: "Oceans and sea ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "CMAP-V1902, TropFlux-1-0, AVISO-1-0, ERA-5, GPCP-SG-2-3, HadISST-1-1"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_enso_ref.html"
+content:
+ description: >-
+ The El Niño Southern Oscillation (ENSO) is the primary mode of the global
+ interannual climate variability, mainly reflected by the variations in surface wind
+ stress and ocean temperature in the tropical Pacific Ocean. The ENSO variability can
+ be calculated from both sea surface temperature and atmospheric pressure differences
+ 680 between different tropical Pacific areas. The Southern Oscillation Index (SOI)
+ uses pressure differences between the Tahiti and Darwin regions. The Oceanic Niño
+ Index (ONI) summarizes SST anomalies in the Niño 3.4 region. Given its implications
+ for regional climate variability, capturing the observed ENSO spatial and temporal
+ characteristics would increase the fidelity and robustness in a model's climate
+ projections.
+ short_description: >-
+ ENSO CLIVAR metrics - reproducing background climatology and ENSO characteristics
+ why_it_matters: >-
+ ENSO is a dominant mode of interannual climate variability across the globe
+ affecting precipitation, temperature and seasons. It is important to understand
+ performance, physical processes and teleconnections in models.
+ takeaway: >-
+ Values closer to 0 can indicate better performance in that characteristic. Models
+ simulate ENSO though some can represent the different aspects of climatology,
+ characteristics and teleconnections, processes better.
+ plain_language:
+ description: >-
+ This diagnostic checks how well the models simulate the El Nino Southern
+ Oscillation (ENSO) in the tropical Pacific.
+ why_it_matters: >-
+ Weather and climate extremes are strongly modulated by ENSO and can have severe
+ impacts on the regional scale.
+ takeaway: >-
+ Models simulate ENSO though there are some model biases that influence weather
+ projections.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: enso-basic-climatology
+ - provider_slug: esmvaltool
+ diagnostic_slug: enso-characteristics
+ - provider_slug: pmp
+ diagnostic_slug: enso_proc
+ - provider_slug: pmp
+ diagnostic_slug: enso_tel
+explorer_cards:
+ - title: "El Niño-Southern Oscillation (ENSO)"
+ description: >-
+ Characteristics of ENSO, the dominant mode of interannual climate variability,
+ which influences global weather patterns, precipitation, and temperature extremes.
+ content: []
+ placeholder: true
diff --git a/backend/static/collections/1-4_sst-sss-bias.yaml b/backend/static/collections/1-4_sst-sss-bias.yaml
new file mode 100644
index 0000000..46bcd77
--- /dev/null
+++ b/backend/static/collections/1-4_sst-sss-bias.yaml
@@ -0,0 +1,120 @@
+id: "1.4"
+name: "Sea surface temperature (SST) bias, Sea surface salinity (SSS) bias"
+theme: "Oceans and sea ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "WOA2023"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ The SST bias period-mean map is calculated as the model's SST field (monthly tos)
+ time-averaged over the historical experiment minus the sea water potential
+ temperature field from the WOA 2023 gridded observational product. The SSS bias
+ map is calculated as the model's SSS field (monthly sos) time-averaged over the
+ historical experiment minus the sea water salinity field from the WOA 2023
+ gridded observational product. Prior to differencing all model output was regridded
+ to the 1-degree WOA23 horizontal grid. The resulting period-mean bias maps show
+ the magnitude of the difference (bias) between the observed and simulated values
+ (SSS or SST) at each model grid cell.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model sea surface temperature (SST)
+ and sea surface salinity (SSS) with reference data from WOA2023
+ why_it_matters: >-
+ The density of seawater and thus its stratification is set by a nonlinear relation
+ between temperature, salinity, and pressure. Thus, patterns of SSS and SST are
+ important in understanding a model's representation of the buoyancy structure of
+ the global ocean and also making sense of its air-sea heat and freshwater fluxes.
+ An accurate representation of SSS and SST are also critical for air-sea CO2 fluxes
+ and thus the global carbon budget. Biases in surface temperature and salinity have
+ the potential to propagate into the ocean interior via water mass transformation
+ processes and influence other climate metrics such as regional sea ice concentrations.
+ takeaway: >-
+ The global patterns of sea surface temperature and salinity are the result of
+ air-sea coupling (heat and freshwater exchange) as well as reflective of the
+ advection of heat and freshwater by large scale ocean circulation patterns and
+ mixing via the ocean mesoscale, particularly in higher resolution models. Achieving
+ accuracy in SSS and SST is vital for accurately representing air-sea heat and
+ carbon fluxes, water mass transformation processes, as well as representing climate
+ feedbacks that are important for a model's ability to accurately represent the
+ transient climate response.
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: so-woa2023-surface
+ - provider_slug: ilamb
+ diagnostic_slug: thetao-woa2023-surface
+explorer_cards:
+ - title: "SST & SSS"
+ placeholder: true
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: so-woa2023-surface
+ title: "Sea Surface Salinity"
+ metric_units: "psu"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: "None"
+ metric: Bias
+ statistic: "Period Mean"
+ placeholder: true
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: thetao-woa2023-surface
+ title: "Sea Surface Temperature"
+ metric_units: "K"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: "None"
+ metric: Bias
+ statistic: "Period Mean"
+ placeholder: true
+ - type: taylor-diagram
+ provider: ilamb
+ diagnostic: thetao-woa2023-surface
+ title: "Sea Surface Temperature (Taylor Diagram)"
+ description: >-
+ Taylor diagram showing the performance of global sea surface temperatures
+ against WOA2023 observations. Taylor diagrams summarize how closely models
+ match observations in terms of correlation, standard deviation, and
+ root-mean-square difference.
+ interpretation: >-
+ Points closer to the reference (black square) indicate better model
+ performance. Distance from the origin represents RMSE.
+ other_filters:
+ region: "None"
+ metric: "Spatial Distribution"
+ placeholder: true
+ - title: "Surface Ocean Salinity"
+ placeholder: true
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: so-woa2023-surface
+ title: "Bias"
+ description: "Bias in Surface Ocean Salinity with respect to WOA2023"
+ span: 1
+ metric_units: "psu"
+ other_filters:
+ statistic: Bias
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ placeholder: true
+ - type: taylor-diagram
+ provider: ilamb
+ diagnostic: so-woa2023-surface
+ title: "Taylor Diagram"
+ description: >-
+ Taylor diagram showing the performance of global surface ocean salinity
+ against WOA2023 observations.
+ interpretation: >-
+ Points closer to the reference (black square) indicate better model
+ performance. Distance from the origin represents RMSE.
+ other_filters:
+ region: "None"
+ metric: "Spatial Distribution"
+ placeholder: true
diff --git a/backend/static/collections/1-6_sea-ice-area.yaml b/backend/static/collections/1-6_sea-ice-area.yaml
new file mode 100644
index 0000000..0cea6ae
--- /dev/null
+++ b/backend/static/collections/1-6_sea-ice-area.yaml
@@ -0,0 +1,77 @@
+id: "1.6"
+name: "Antarctic & Arctic sea ice area seasonal cycle"
+theme: "Oceans and sea ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "OSISAF-V3"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html"
+content:
+ description: >-
+ The sea ice area is calculated as the sum over the Northern (Arctic) and Southern
+ (Antarctic) Hemisphere grid cell areas multiplied by the sea ice fraction within
+ each cell.
+
+ This is done throughout the year to assess the seasonal cycle. Arctic
+ sea ice area typically has minimum values in September, while Antarctic sea ice
+ area is lowest in February. The seasonal cycle is the result of several important
+ melting and growth processes and so can be seen as an overview metric for the
+ general state of the sea ice in a model.
+
+ In addition to the multi-year average
+ seasonal cycle of NH (Arctic) and SH (Antarctic) sea ice area, the diagnostic
+ produces time series of the September (Arctic) and February (Antarctic) sea ice
+ area.
+ short_description: >-
+ Seasonal cycle of Arctic (NH) and Antarctic (SH) sea ice area, time series of
+ Arctic September (NH) and Antarctic February (SH) sea ice area
+ why_it_matters: >-
+ Sea ice is an important component of the Earth system. Since sea ice has a much
+ higher albedo than the ocean surface, sea ice area plays an important role in the
+ surface energy and radiation budgets, particularly in summer. Its low thermal
+ conductivity means that sea ice insulates the relatively warm ocean from the cold
+ atmosphere above in winter. Sea ice also presents a physical barrier to the
+ exchange of moisture, gases, and aerosols between the ocean and atmosphere.
+ takeaway: >-
+ The seasonal cycle of sea ice area is the result of several important melting and
+ growth processes and so can be seen as an overview metric for the general state of
+ the sea ice in a climate model.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: sea-ice-area-basic
+explorer_cards:
+ - title: "Sea Ice"
+ description: "Sea Ice Area Seasonal Cycle"
+ placeholder: true
+ content:
+ - type: series-chart
+ provider: esmvaltool
+ diagnostic: sea-ice-area-basic
+ title: "Sea Ice Area Seasonal Cycle (Southern Hemisphere)"
+ description: >-
+ 20-year average seasonal cycle of the sea ice area in the Southern Hemisphere
+ (OSISAF-CCI reference data currently missing).
+ span: 2
+ metric_units: "1e6 km^2"
+ other_filters:
+ region: "Southern Hemisphere"
+ isolate_ids: "568191,568195,568199,568203,568207,568211,568215,568219,568223,568227,568231,568235,568239,568243,568247,568251,568255,568259,568263,568267,568271,568275,568279,568283,568287,568291,568295,568299,568303,568307,568311,568315,568319,568323,568327,568331,568335,568339,568343,568347,568351,568355,568359,568363,568367,568371,568375,568379,568383,568387,568391,568395,568399,568403,568407,568411,568415,568419,568423,568427,568431,568435,568439,568443,568447,568451,568455,568459,568463,568467,568471,568475,568479,568483,568487,568491,568495,568499,568503,568507,568511,568515,568519,568523,568527,568531,568535,568541,568545,568561,568585,568589,568593,568601,568609,568621,568627,568635,568639,568643"
+ exclude_ids: "568564,568570"
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ - type: series-chart
+ provider: esmvaltool
+ diagnostic: sea-ice-area-basic
+ title: "Sea Ice Area Seasonal Cycle (Northern Hemisphere)"
+ description: >-
+ 20-year average seasonal cycle of the sea ice area in the Northern Hemisphere
+ (OSISAF-CCI reference data currently missing).
+ span: 2
+ metric_units: "1e6 km^2"
+ other_filters:
+ statistic: "20-year average seasonal cycle of the sea ice area"
+ exclude_ids: "568564,568570"
+ region: "Northern Hemisphere"
+ grouping_config:
+ group_by: source_id
+ hue: source_id
diff --git a/backend/static/collections/2-1_soil-carbon.yaml b/backend/static/collections/2-1_soil-carbon.yaml
new file mode 100644
index 0000000..9c38aa4
--- /dev/null
+++ b/backend/static/collections/2-1_soil-carbon.yaml
@@ -0,0 +1,34 @@
+id: "2.1"
+name: "Soil carbon"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "HWSD-2-0, NCSCD-2-2"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ Soil carbon is the organic matter and inorganic carbon in global soils. It is an
+ important component of the global carbon cycle and affects soil moisture retention
+ and saturation. Analyzing stored soil carbon helps track and quantify the dynamics
+ of the terrestrial carbon cycle within models and the movement of carbon through
+ the Earth system.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model soil carbon with reference data
+ from HWSD-2-0 and NCSCD-2-2
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: csoil-hwsd2
+explorer_cards:
+ - title: "Soil Carbon"
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: csoil-hwsd2
+ title: "Soil Carbon"
+ description: "Bias in Soil Carbon Content compared to HWSDv2"
+ metric_units: "kg/m^2"
+ other_filters:
+ statistic: Bias
+ grouping_config:
+ group_by: region
+ hue: region
diff --git a/backend/static/collections/2-2_gross-primary-production.yaml b/backend/static/collections/2-2_gross-primary-production.yaml
new file mode 100644
index 0000000..6fcc5ee
--- /dev/null
+++ b/backend/static/collections/2-2_gross-primary-production.yaml
@@ -0,0 +1,58 @@
+id: "2.2"
+name: "Gross primary production (GPP)"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "FLUXNET2015-1-0, WECANN-1-0, CRU-4-9 (source_id not confirmed)"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ Gross primary production is the process by which plants "fix" atmospheric or
+ aqueous carbon dioxide through photosynthetic reduction into organic compounds, and
+ it is affected by increases in atmospheric carbon dioxide (CO2) levels and warming
+ (Anav et al., 2015). A fraction of gross primary productivity supports plant
+ respiration and the rest is stored as biomass in stems, leaves, roots, or other
+ plant parts. Land use change, heat and drought stress due to anthropogenic warming,
+ and rising atmospheric CO2 will differentially influence gross primary production
+ in ecosystems and alter the global carbon cycle. Thus, models must be evaluated to
+ ensure they capture the observed responses to these changes.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model gross primary production (GPP)
+ with reference data from FLUXNET2015-1-0 and WECANN-1-0
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: gpp-wecann
+ - provider_slug: ilamb
+ diagnostic_slug: gpp-fluxnet2015
+explorer_cards:
+ - title: "Gross Primary Production"
+ placeholder: true
+ content:
+ - type: series-chart
+ provider: ilamb
+ diagnostic: gpp-fluxnet2015
+ title: "Gross Primary Production (GPP) Annual Cycle"
+ description: "Calculated as the mean seasonal cycle over 2001-2010."
+ span: 2
+ metric_units: "kgC/m^2/s"
+ other_filters:
+ metric: cycle_global
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ placeholder: true
+ - type: taylor-diagram
+ provider: ilamb
+ diagnostic: gpp-fluxnet2015
+ title: "GPP Spatial Performance"
+ description: >-
+ Model performance in reproducing spatial patterns of Gross Primary Production
+ (GPP) compared to FLUXNET2015 observations.
+ interpretation: >-
+ Points closer to the reference (black square) indicate better model
+ performance. Distance from the origin represents RMSE.
+ span: 1
+ other_filters:
+ region: global
+ reference_stddev: 1.0
+ placeholder: true
diff --git a/backend/static/collections/2-3_runoff.yaml b/backend/static/collections/2-3_runoff.yaml
new file mode 100644
index 0000000..ba8eef8
--- /dev/null
+++ b/backend/static/collections/2-3_runoff.yaml
@@ -0,0 +1,46 @@
+id: "2.3"
+name: "Runoff"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "LORA-1-1"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ The diagnostic shows total runoff per grid cell expressed in kg/m^2/s and compares
+ it with the Linear Optimal Runoff Aggregate (LORA) (Hobeichi et al, 2019)
+ observation-based product. The diagnostics calculates metrics following the
+ standard methodology from the ILAMB project (Collier et al. 2018). For each model
+ run, maps of total runoff, month of maximum, root-mean squared error (RMSE), bias,
+ bias score, phase shift, cycle score with respect to LORA are produced, as well as
+ time series and Taylor diagrams for both global and tropical domain.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model surface runoff with reference
+ data from LORA-1-1
+ why_it_matters: >-
+ Runoff is important for the whole terrestrial hydrological cycle. In the coastal
+ regions, the biases in runoff could also influence the fresh water and nutrient
+ supply to the ocean.
+ plain_language:
+ why_it_matters: >-
+ Runoff is a variable which is indicative of the fresh water supply and drought
+ metrics. Thus understanding the biases in the models would be beneficial in
+ particular for future planning.
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: mrro-lora
+explorer_cards:
+ - title: "Total Runoff"
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: mrro-lora
+ title: "Total Runoff"
+ metric_units: "kg/m^2/s"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: global
+ metric: Bias
+ statistic: Bias
diff --git a/backend/static/collections/2-4_surface-soil-moisture.yaml b/backend/static/collections/2-4_surface-soil-moisture.yaml
new file mode 100644
index 0000000..6b699ad
--- /dev/null
+++ b/backend/static/collections/2-4_surface-soil-moisture.yaml
@@ -0,0 +1,29 @@
+id: "2.4"
+name: "Surface soil moisture"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "OLC-ORS-V0"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model surface soil moisture with
+ reference data from OLC-ORS-V0
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: mrsos-wangmao
+explorer_cards:
+ - title: "Surface Soil Moisture"
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: mrsos-wangmao
+ title: "Surface Soil Moisture"
+ metric_units: "kg/m^2"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: global
+ metric: Bias
+ statistic: Bias
diff --git a/backend/static/collections/2-5_net-ecosystem-carbon-balance.yaml b/backend/static/collections/2-5_net-ecosystem-carbon-balance.yaml
new file mode 100644
index 0000000..92a4cdd
--- /dev/null
+++ b/backend/static/collections/2-5_net-ecosystem-carbon-balance.yaml
@@ -0,0 +1,30 @@
+id: "2.5"
+name: "Net ecosystem carbon balance"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "HOFFMAN-1-0"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ short_description: >-
+ Comparison of the model integrated land net ecosystem carbon balance with reference
+ data from HOFFMAN-1-0
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: nbp-hoffman
+explorer_cards:
+ - title: "Net Biome Production"
+ placeholder: true
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: nbp-hoffman
+ title: "Net Biome Production"
+ description: >-
+ Bias in Net Biome Production (NBP) compared to Hoffman et al. (2020) estimates
+ metric_units: "PgC/yr"
+ clip_max: 2000
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ placeholder: true
diff --git a/backend/static/collections/2-6_leaf-area-index.yaml b/backend/static/collections/2-6_leaf-area-index.yaml
new file mode 100644
index 0000000..7e135e4
--- /dev/null
+++ b/backend/static/collections/2-6_leaf-area-index.yaml
@@ -0,0 +1,42 @@
+id: "2.6"
+name: "Leaf area index (LAI)"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "NOAA-NCEI-LAI-5-0, LAI4g-1-2"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ This diagnostic computes gridpoint-wise Leaf Area Index (LAI) values and compares
+ them with the AVHRR-based 30-year LAI/FAPAR dataset (AVH15C1; Claverie et al.
+ 2016). The diagnostics ingests the specific product from the ILAMB project
+ (Collier et al. 2018). For each model run, maps of LAI, month of maximum,
+ root-mean squared error (RMSE), bias, bias score, phase shift, and cycle score
+ with respect to AVH15C1 are produced, as well as time series and Taylor diagrams
+ for the Tropics and for the whole globe.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model leaf area index (LAI) with
+ reference data from NOAA-NCEI-LAI-5-0, and LAI4g-1-2
+ why_it_matters: >-
+ LAI has fundamental implications for the closure of the biogeochemical cycle over
+ land and within ecosystems. With the main focus on emission-driven simulations,
+ these diagnostics are crucial to assess the reliability of carbon fluxes in future
+ climate scenarios.
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: lai-avh15c1
+explorer_cards:
+ - title: "Leaf Area Index"
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: lai-avh15c1
+ title: "Leaf Area Index"
+ metric_units: "1"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: global
+ metric: Bias
+ statistic: Bias
diff --git a/backend/static/collections/2-7_snow-cover.yaml b/backend/static/collections/2-7_snow-cover.yaml
new file mode 100644
index 0000000..18a2fa8
--- /dev/null
+++ b/backend/static/collections/2-7_snow-cover.yaml
@@ -0,0 +1,47 @@
+id: "2.7"
+name: "Snow cover"
+theme: "Land and land ice"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "CCI-CryoClim-FSC-1"
+provider_link: "https://doi.org/10.1029/2018MS001354"
+content:
+ description: >-
+ The diagnostic shows fractional snow cover per grid cell expressed in percent of
+ the grid cell area covered by snow and compares it with the CCI-CryoClim-FSC-1
+ (Solberg et al, 2014) observation-based product. The diagnostics calculates
+ metrics following the standard methodology from the ILAMB project (Collier et al.
+ 2018). For each model run, maps of fractional snow cover, month of maximum,
+ root-mean squared error (RMSE), bias, bias score, phase shift, cycle score with
+ respect to CCI-CryoClim-FSC-1 are produced, as well as global time series and
+ Taylor diagrams.
+ short_description: >-
+ Apply the ILAMB Methodology* to compare the model snow cover with reference data
+ from CCI-CryoClim-FSC-1
+ why_it_matters: >-
+ Snow cover is a variable which influences a lot of other variables, in particular
+ those related to land surface processes (e.g., runoff, soil temperature). Biases in
+ snow cover could also propagate into the radiative processes through the changes in
+ albedo.
+ plain_language:
+ why_it_matters: >-
+ Snow is an important metric for a lot of industries and Indigenous Arctic
+ communities. Assessing the model biases in snow cover would help in future
+ planning.
+diagnostics:
+ - provider_slug: ilamb
+ diagnostic_slug: snc-esacci
+explorer_cards:
+ - title: "Snow Cover Extent"
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: snc-esacci
+ title: "Snow Cover Extent"
+ metric_units: "%"
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ other_filters:
+ region: global
+ metric: Bias
diff --git a/backend/static/collections/3-1_annual-cycle.yaml b/backend/static/collections/3-1_annual-cycle.yaml
new file mode 100644
index 0000000..4a054ba
--- /dev/null
+++ b/backend/static/collections/3-1_annual-cycle.yaml
@@ -0,0 +1,96 @@
+id: "3.1"
+name: "Annual cycle and seasonal mean of multiple variables"
+theme: "Atmosphere"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "C3S-GTO-ECV-9-0, SAGE-CCI-OMPS-v0008, ERA-5"
+provider_link: "http://pcmdi.github.io/pcmdi_metrics/examples/Demo_1b_mean_climate.html"
+content:
+ description: >-
+ Mean state metrics quantify how well models simulate observed climatological
+ fields at a large scale, gauged by a suite of well-established statistics such as
+ RMSE, mean absolute error (MAE), and pattern correlation. The seasonally and
+ annually averaged fields of multiple variables from large-scale observationally
+ based datasets and results from model simulations are compared.
+ short_description: >-
+ Maps of seasonal and annual climatology are generated for the reference datasets
+ and model output on a target grid (2.5x2.5 deg), then calculate diverse metrics
+ including bias, RMSE, spatial pattern correlation, and standard deviation.
+ why_it_matters: >-
+ These metrics quantify how well models simulate observed climatological fields at
+ a large scale.
+ plain_language:
+ why_it_matters: >-
+ These metrics provide an entry-level high-level overview for model performance.
+diagnostics:
+ - provider_slug: pmp
+ diagnostic_slug: annual-cycle
+explorer_cards:
+ - title: "Mean Climate Performance"
+ description: >-
+ Model skill at reproducing observed climatological patterns, assessed via
+ spatial correlation, RMSE, and bias for key atmospheric variables.
+ content:
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: annual-cycle
+ title: "Surface Temperature Spatial Correlation"
+ description: >-
+ Spatial pattern correlation between simulated and observed annual-mean
+ surface temperature (ts). Higher values indicate better agreement with
+ the observed climatological pattern.
+ metric_units: ""
+ other_filters:
+ variable_id: ts
+ statistic: cor_xy
+ region: global
+ grouping_config:
+ group_by: season
+ hue: season
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: annual-cycle
+ title: "Precipitation RMSE"
+ description: >-
+ Root mean square error of the spatial pattern of annual and seasonal
+ precipitation (pr) relative to GPCP observations. Lower values indicate
+ better performance.
+ metric_units: "mm/day"
+ other_filters:
+ variable_id: pr
+ statistic: rms_xy
+ region: global
+ grouping_config:
+ group_by: season
+ hue: season
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: annual-cycle
+ title: "Surface Temperature Bias"
+ description: >-
+ Global mean bias in surface temperature (ts) relative to ERA-5. Positive
+ values indicate the model is too warm; negative values indicate too cold.
+ metric_units: "K"
+ other_filters:
+ variable_id: ts
+ statistic: bias_xy
+ region: global
+ show_zero_line: true
+ grouping_config:
+ group_by: season
+ hue: season
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: annual-cycle
+ title: "TOA Outgoing Longwave Radiation RMSE"
+ description: >-
+ Spatial RMSE of outgoing longwave radiation at the top of atmosphere
+ (rlut) compared to CERES-EBAF observations.
+ metric_units: "W/m^2"
+ other_filters:
+ variable_id: rlut
+ statistic: rms_xy
+ region: global
+ grouping_config:
+ group_by: season
+ hue: season
diff --git a/backend/static/collections/3-2_radiative-heat-fluxes.yaml b/backend/static/collections/3-2_radiative-heat-fluxes.yaml
new file mode 100644
index 0000000..b4dc0f6
--- /dev/null
+++ b/backend/static/collections/3-2_radiative-heat-fluxes.yaml
@@ -0,0 +1,43 @@
+id: "3.2"
+name: "Radiative and heat fluxes at the surface and top of atmosphere (TOA)"
+theme: "Atmosphere"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "CERES-EBAF-4-2"
+provider_link: "http://pcmdi.github.io/pcmdi_metrics/examples/Demo_1a_compute_climatologies.html"
+content:
+ description: >-
+ This diagnostic computes and compares simulated radiative fluxes (shortwave and
+ longwave, up and down) and turbulent heat fluxes (sensible and latent) at the
+ surface and TOA against observational datasets such as CERES-EBAF. Multi-year
+ climatological means are computed for each flux component, and the
+ model-observation differences (biases) are mapped globally. Metrics include
+ spatial RMSE, pattern correlation, and global/regional mean biases.
+ short_description: >-
+ Maps of seasonal and annual climatology are generated for the reference dataset
+ and model output on a target grid (2.5x2.5 deg), then calculate diverse metrics
+ including bias, RMSE, spatial pattern correlation, and standard deviation.
+ why_it_matters: >-
+ The global energy budget is a fundamental constraint on the climate system. TOA
+ radiative imbalance determines the rate of climate change, while surface fluxes
+ drive ocean-atmosphere heat exchange and hydrological processes. Biases in these
+ fluxes propagate into errors in temperature, precipitation, and circulation
+ patterns.
+ takeaway: >-
+ Comparison against CERES-EBAF provides a well-constrained benchmark for model
+ radiative performance. Systematic biases in net TOA flux indicate potential issues
+ with cloud representation, aerosol forcing, or surface albedo. Regional flux
+ biases often correlate with biases in other fields (e.g., SST, precipitation).
+ plain_language:
+ description: >-
+ This diagnostic checks whether models correctly simulate how much energy enters
+ and leaves the Earth system at different levels -- at the top of the atmosphere
+ and at the surface.
+ why_it_matters: >-
+ The Earth's energy balance determines how fast the planet warms. Getting these
+ fluxes right is essential for accurate climate projections.
+ takeaway: >-
+ Models that accurately simulate energy flows are more trustworthy for projecting
+ future warming.
+diagnostics: []
+explorer_cards: []
diff --git a/backend/static/collections/3-3_climate-variability-modes.yaml b/backend/static/collections/3-3_climate-variability-modes.yaml
new file mode 100644
index 0000000..ee5e976
--- /dev/null
+++ b/backend/static/collections/3-3_climate-variability-modes.yaml
@@ -0,0 +1,177 @@
+id: "3.3"
+name: "Climate variability modes (e.g., ENSO, Madden-Julian Oscillation (MJO), Extratropical modes of variability, monsoon)"
+theme: "Atmosphere"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "20CR-V2, HadISST-1-1"
+provider_link: "http://pcmdi.github.io/pcmdi_metrics/metrics.html"
+content:
+ description: >-
+ PMP calculates skill metrics for extra-tropical modes of variability (EMoV),
+ including the Northern Annular Mode (NAM), the North Atlantic Oscillation (NAO),
+ the Southern Annular Mode (SAM), the Pacific North American pattern (PNA), the
+ North Pacific Oscillation (NPO), the Pacific Decadal Oscillation (PDO), and the
+ North Pacific Gyre Oscillation (NPGO). For NAM, NAO, SAM, PNA, and NPO the
+ results are based on sea-level pressure, while the results for PDO and NPGO are
+ based on sea surface temperature. Our approach distinguishes itself from other
+ studies that analyze modes of variability in that we use the Common Basis Function
+ approach (CBF), in which model anomalies are projected onto the observed modes of
+ variability, together with the traditional EOF approach. Using the Historical
+ simulations, the skill of the spatial patterns is given by the Root-Mean-Squared
+ Error (RMSE), and the Amplitude gives the standard deviation of the Principal
+ Component time series.
+ short_description: >-
+ For extratropical modes of variability, maps of variability mode pattern and their
+ principal component time series are generated from the reference dataset and model
+ output. Then maps are compared to calculate bias, RMSE, and spatial pattern
+ correlation, and time series are compared to calculate the ratio from their standard
+ deviations.
+ why_it_matters: >-
+ Modes of variability operate on hemispheric to ocean-basin scale and explain a
+ significant fraction of variance of the weather and sea surface temperature
+ variability at smaller scales, ranging from regionally aggregated to in-situ
+ values. The modes are typically described by a fixed spatial pattern, typically
+ showing a dipole of opposing anomalies, and an index value describing variations
+ from this fixed pattern for a given point in time. If climate models miss this
+ pattern and/or the climatological (e.g. 30-year) statistics of the index time
+ series, then they will also miss regional-scale climate variability. The index
+ values typically vary on multiple timescales ranging from a few weeks to several
+ decades.
+ takeaway: >-
+ Climate variability modes are diagnostics of hemispheric to ocean-basin scale
+ climate variability that partly control the weather and SST variability on smaller
+ scales, particularly during the winter half-year, when pressure gradients are
+ pronounced.
+ plain_language:
+ description: >-
+ Climate variability modes are large-scale "weather makers". They are described
+ with fixed spatial patterns obtained from Principal Component Analysis (aka
+ Empirical Orthogonal Functions), covering entire continents or ocean basins that
+ depict regions of contrasting atmospheric states that are mutually related.
+ why_it_matters: >-
+ Extra-tropical modes of variability represent a roof concept of hemispheric to
+ regional-scale climate variability in terms of standard meteorological variables
+ such as temperature and precipitation.
+ takeaway: >-
+ Climate variability modes are large-scale "weather makers".
+diagnostics:
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-nam
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-nao
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-npgo
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-npo
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-pdo
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-pna
+ - provider_slug: pmp
+ diagnostic_slug: extratropical-modes-of-variability-sam
+explorer_cards:
+ - title: "Extratropical Modes of Variability"
+ description: >-
+ Spatial comparison of simulated vs. observed leading EOF patterns representing
+ the main modes of low-frequency variability in the extra-tropical atmosphere and
+ ocean, based on seasonal-mean sea level pressure and monthly-mean sea surface
+ temperature anomalies in the spatial domains defined in
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4), Table 1. Simulated
+ EOFs are obtained with the "Common Basis Function" (CBF) approach described in
+ the appendix of this article. The considered time-period is 1901-2005 for the NH
+ modes and 1951-2005 for the SAM.
+ content:
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-nam
+ title: "Northern Annular Mode (NAM) RMSE"
+ description: >-
+ Northern Annular Mode (NAM) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: season
+ y_min: 0
+ y_max: 1.5
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-sam
+ title: "Southern Annual Mode (SAM) RMSE"
+ description: >-
+ Southern Annual Mode (SAM) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: season
+ y_min: 0
+ y_max: 1.5
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-pna
+ title: "Pacific-North American (PNA) RMSE"
+ description: >-
+ Pacific-North American (PNA) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: season
+ y_min: 0
+ y_max: 1.5
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-nao
+ title: "North Atlantic Oscillation (NAO) RMSE"
+ description: >-
+ North Atlantic Oscillation (NAO) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: season
+ y_min: 0
+ y_max: 1.5
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-pdo
+ title: "Pacific-Decadal Oscillation (PDO) RMSE"
+ description: >-
+ Pacific-Decadal Oscillation (PDO) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: season
+ y_min: 0
+ - type: box-whisker-chart
+ provider: pmp
+ diagnostic: extratropical-modes-of-variability-npgo
+ title: "North Pacific Gyre Oscillation (NPGO) RMSE"
+ description: >-
+ North Pacific Gyre Oscillation (NPGO) individual-model pattern RMSE, see
+ [Lee et al. 2019](https://doi.org/10.1007/s00382-018-4355-4)
+ other_filters:
+ method: cbf
+ statistic: rms
+ experiment_id: historical
+ grouping_config:
+ group_by: season
+ hue: experiment_id
+ y_min: 0
diff --git a/backend/static/collections/3-6_cloud-radiative-effects.yaml b/backend/static/collections/3-6_cloud-radiative-effects.yaml
new file mode 100644
index 0000000..8d87cd2
--- /dev/null
+++ b/backend/static/collections/3-6_cloud-radiative-effects.yaml
@@ -0,0 +1,58 @@
+id: "3.6"
+name: "Cloud radiative effects"
+theme: "Atmosphere"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "CALIPSO-ICECLOUD-1-0, ESACCI-CLOUD-AVHRR-AMPM-3-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html"
+content:
+ description: >-
+ This diagnostic computes shortwave and longwave cloud radiative effects (CRE) as
+ the difference between all-sky and clear-sky radiative fluxes at TOA.
+ Model-simulated CRE fields are compared against CERES-EBAF satellite
+ observations. Output includes global maps of SW CRE, LW CRE, and net CRE biases,
+ along with zonal mean profiles and summary statistics (RMSE, bias, pattern
+ correlation).
+ short_description: >-
+ Maps and zonal means of longwave and shortwave cloud radiative effect
+ why_it_matters: >-
+ Cloud feedbacks remain the largest source of uncertainty in climate sensitivity
+ estimates. SW CRE (cooling effect from reflected sunlight) and LW CRE (warming
+ effect from trapped infrared radiation) largely compensate globally but have
+ distinct regional patterns. Accurate CRE simulation is necessary for reliable
+ projections of future warming.
+ takeaway: >-
+ Most models show biases in subtropical stratocumulus regions (too little SW
+ cooling) and in the tropics (LW/SW compensation errors). The net CRE bias
+ indicates whether a model's cloud representation produces the correct radiative
+ impact. Persistent biases often relate to parameterized convection and
+ boundary-layer schemes.
+ plain_language:
+ description: >-
+ This diagnostic measures how clouds affect the Earth's energy balance by
+ comparing what satellites observe to what models simulate.
+ why_it_matters: >-
+ Clouds can either cool or warm the planet depending on their type and location.
+ How clouds change with warming is the biggest uncertainty in predicting future
+ climate.
+ takeaway: >-
+ Regions where models disagree with observations highlight where cloud processes
+ need improvement.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-radiative-effects
+explorer_cards:
+ - title: "Cloud & Radiation"
+ description: "Cloud properties and their effect on the Earth's energy balance."
+ placeholder: true
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: cloud-radiative-effects
+ title: "Cloud Radiative Effects"
+ description: "Not sure"
+ other_filters:
+ statistic: bias
+ grouping_config:
+ group_by: metric
+ hue: metric
diff --git a/backend/static/collections/3-7_cloud-scatterplots.yaml b/backend/static/collections/3-7_cloud-scatterplots.yaml
new file mode 100644
index 0000000..dfaa993
--- /dev/null
+++ b/backend/static/collections/3-7_cloud-scatterplots.yaml
@@ -0,0 +1,94 @@
+id: "3.7"
+name: "Scatterplots of two cloud-relevant variables (for specific regions of the globe and specific cloud regimes)"
+theme: "Atmosphere"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "CALIPSO-ICECLOUD-1-0, ESACCI-CLOUD-AVHRR-AMPM-3-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html"
+content:
+ description: >-
+ This diagnostic produces scatterplots of paired cloud-relevant variables (e.g.,
+ cloud fraction vs. estimated inversion strength, LWP vs. SST, CRE vs. lower
+ tropospheric stability) stratified by region and/or dynamical regime. Observed
+ relationships from satellite data (e.g., CERES, MODIS, ISCCP) are compared
+ against model output to assess whether models reproduce observed covariability and
+ regime-dependent cloud behaviour.
+ short_description: "2D histograms with focus on clouds"
+ why_it_matters: >-
+ Cloud-controlling factor relationships underpin emergent constraint approaches for
+ narrowing climate sensitivity. If models fail to reproduce observed relationships
+ between cloud properties and their meteorological drivers, their cloud feedbacks
+ may be unreliable. This diagnostic targets process-level evaluation rather than
+ mean-state biases alone.
+ takeaway: >-
+ The slope and scatter of these relationships reveal whether models capture the
+ correct physical sensitivities. Departures from observed relationships --
+ particularly in subtropical subsidence regions -- often indicate deficiencies in
+ boundary-layer or shallow convection parameterizations that affect cloud feedback
+ strength.
+ plain_language:
+ description: >-
+ This diagnostic tests whether models capture how cloud properties relate to
+ their environment -- for example, whether low clouds behave realistically in
+ regions of sinking air.
+ why_it_matters: >-
+ Understanding why clouds form where they do helps us predict how they'll change
+ in a warmer world.
+ takeaway: >-
+ Models that get these relationships right are more likely to produce reliable
+ predictions of cloud changes under warming.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-scatterplots-cli-ta
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-scatterplots-clivi-lwcre
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-scatterplots-clt-swcre
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-scatterplots-clwvi-pr
+ - provider_slug: esmvaltool
+ diagnostic_slug: cloud-scatterplots-reference
+explorer_cards:
+ - title: "Cloud Scatterplots"
+ description: >-
+ 2D histograms comparing cloud-relevant variable pairs in models and
+ observations, revealing whether models capture regime-dependent cloud
+ behaviour.
+ content:
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: cloud-scatterplots-reference
+ title: "Reference Observations"
+ description: >-
+ Observed relationships between paired cloud-relevant variables from
+ satellite data (CERES, CALIPSO, ERA5, GPCP), providing the benchmark
+ against which model scatterplots are compared.
+ span: 2
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: cloud-scatterplots-clt-swcre
+ title: "Cloud Fraction vs. Shortwave CRE"
+ description: >-
+ Scatterplots of total cloud fraction (clt) against shortwave cloud
+ radiative effect (SW CRE) for individual models, testing whether models
+ reproduce the observed relationship between cloud amount and its
+ radiative impact.
+ span: 2
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: cloud-scatterplots-clivi-lwcre
+ title: "Ice Water Path vs. Longwave CRE"
+ description: >-
+ Scatterplots of cloud ice water path (clivi) against longwave cloud
+ radiative effect (LW CRE), assessing how well models capture the
+ warming effect of high ice clouds.
+ span: 2
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: cloud-scatterplots-clwvi-pr
+ title: "Liquid Water Path vs. Precipitation"
+ description: >-
+ Scatterplots of cloud liquid water path (clwvi) against precipitation
+ (pr), evaluating the modelled relationship between cloud water content
+ and precipitation efficiency.
+ span: 2
diff --git a/backend/static/collections/4-1_equilibrium-climate-sensitivity.yaml b/backend/static/collections/4-1_equilibrium-climate-sensitivity.yaml
new file mode 100644
index 0000000..26752dd
--- /dev/null
+++ b/backend/static/collections/4-1_equilibrium-climate-sensitivity.yaml
@@ -0,0 +1,79 @@
+id: "4.1"
+name: "Equilibrium climate sensitivity (ECS)"
+theme: "Earth System"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: ""
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ecs.html"
+content:
+ description: >-
+ Equilibrium climate sensitivity is defined as the change in global mean temperature
+ as a result of a doubling of the atmospheric CO2 concentration compared to
+ pre-industrial times after the climate system has reached a new equilibrium. This
+ recipe uses a regression method based on Gregory et al. (2004) to calculate it for
+ several CMIP models.
+ short_description: >-
+ Equilibrium climate sensitivity is defined as the change in global mean temperature
+ as a result of a doubling of the atmospheric CO2 concentration compared to
+ pre-industrial times after the climate system has reached a new equilibrium. This
+ diagnostic uses a regression method based on Gregory et al. (2004).
+ why_it_matters: >-
+ Equilibrium climate sensitivity is the temperature change realized after allowing
+ the climate system to equilibrate with a value of CO2 doubled compared to
+ pre-industrial values. Therefore, it accounts for the response of the climate
+ system after the deep oceans had time to equilibrate. Even though it is less
+ relevant than TCR for the assessment of changes that are set to be observed in the
+ 21st Century, they are fundamental for inter-comparison of model performances.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: equilibrium-climate-sensitivity
+explorer_cards:
+ - title: "Climate Sensitivity (ECS & Lambda)"
+ description: "Fundamental metrics of the global climate response to CO2."
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: equilibrium-climate-sensitivity
+ title: "Equilibrium Climate Sensitivity (ECS)"
+ description: >-
+ ECS represents the long-term change in global mean surface temperature
+ following a doubling of atmospheric CO2 concentrations. It is a key metric
+ for understanding the sensitivity of Earth's climate system to radiative
+ forcing and is crucial for predicting future climate change and informing
+ policy decisions. ECS is influenced by various feedback mechanisms, including
+ water vapor, clouds, and ice-albedo feedbacks, which can amplify or dampen
+ the initial warming response.
+ interpretation: >-
+ Higher ECS values indicate a more sensitive climate system, leading to greater
+ warming for a given increase in CO2.
+ metric_units: ""
+ clip_max: 10
+ other_filters:
+ metric: ecs
+ grouping_config:
+ group_by: metric
+ hue: metric
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: equilibrium-climate-sensitivity
+ title: "Lambda"
+ description: >-
+ Climate feedback parameter (λ) quantifies the sensitivity of Earth's climate
+ system to radiative forcing, representing the change in global mean surface
+ temperature per unit of radiative forcing (W/m²). It is a key metric for
+ understanding the balance between incoming solar radiation and outgoing
+ terrestrial radiation, influencing how the climate responds to factors such
+ as greenhouse gas concentrations and aerosols.
+ interpretation: >-
+ A more negative λ value indicates that the climate system has stronger
+ stabilizing feedbacks, which help to counteract warming and maintain
+ equilibrium. Conversely, a less negative or positive λ suggests that the
+ climate system is more sensitive to perturbations, potentially leading to
+ amplified warming in response to increased greenhouse gas concentrations.
+ metric_units: "W/m²/K"
+ clip_max: 10
+ other_filters:
+ metric: lambda
+ grouping_config:
+ group_by: metric
+ hue: metric
diff --git a/backend/static/collections/4-2_transient-climate-response.yaml b/backend/static/collections/4-2_transient-climate-response.yaml
new file mode 100644
index 0000000..22f3ea7
--- /dev/null
+++ b/backend/static/collections/4-2_transient-climate-response.yaml
@@ -0,0 +1,49 @@
+id: "4.2"
+name: "Transient climate response (TCR)"
+theme: "Earth System"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: ""
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_tcr.html"
+content:
+ description: >-
+ The transient climate response (TCR) is defined as the global and annual mean
+ surface air temperature anomaly in the 1pctCO2 scenario (1% CO2 increase per year)
+ for a 20 year period centered at the time of CO2 doubling, i.e. using the years 61
+ to 80 after the start of the simulation. We calculate the temperature anomaly by
+ subtracting a linear fit of the piControl run for all 140 years of the 1pctCO2
+ experiment prior to the TCR calculation (see Gregory and Forster, 2008).
+ short_description: >-
+ The transient climate response (TCR) is defined as the global and annual mean
+ surface air temperature anomaly in the 1pctCO2 scenario (1% CO2 increase per year)
+ for a 20 year period centered at the time of CO2 doubling, i.e. using the years 61
+ to 80 after the start of the simulation. We calculate the temperature anomaly by
+ subtracting a linear fit of the piControl run for all 140 years of the 1pctCO2
+ experiment prior to the TCR calculation (see Gregory and Forster, 2008).
+ why_it_matters: >-
+ Unlike Equilibrium Climate Sensitivity (ECS), TCR is important in order to account
+ for the response of the climate at the timescales relevant for climate mitigation
+ policies (i.e. multidecadal to centennial). The parameter is set to immediately
+ compare the climate response among different models.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: transient-climate-response
+explorer_cards:
+ - title: "Transient Climate Response"
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: transient-climate-response
+ title: "Transient Climate Response (TCR)"
+ description: >-
+ TCR measures the immediate warming response of the climate system to a
+ sustained increase in CO2 concentrations. It is a key metric for understanding
+ the short-term impacts of greenhouse gas emissions on global temperatures and
+ is critical for informing climate policy and adaptation strategies.
+ interpretation: >-
+ Higher TCR values indicate a more sensitive climate system, leading to greater
+ warming in the near term for a given increase in CO2.
+ metric_units: "K"
+ grouping_config:
+ group_by: metric
+ hue: metric
diff --git a/backend/static/collections/4-3_tcre.yaml b/backend/static/collections/4-3_tcre.yaml
new file mode 100644
index 0000000..532f0d5
--- /dev/null
+++ b/backend/static/collections/4-3_tcre.yaml
@@ -0,0 +1,74 @@
+id: "4.3"
+name: "Transient climate response to cumulative emissions of carbon dioxide (TCRE)"
+theme: "Earth System"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: ""
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_tcre.html"
+content:
+ description: >-
+ The idea that global temperature rise is directly proportional to the total amount
+ of carbon dioxide (CO2) released into the atmosphere is fundamental to climate
+ policy. The concept stems from research showing a clear linear relationship between
+ cumulative CO2 emissions and global temperature change in climate models (Allen et
+ al. 2009; Matthews et al. 2009; Zickfeld et al. 2009). This relationship is called
+ the Transient Climate Response to Cumulative CO2 Emissions (TCRE), which represents
+ the amount of global warming caused by each trillion tonnes of carbon emitted. This
+ simple yet powerful tool allows policymakers to directly link emission budgets to
+ specific temperature targets and compare the long-term effects of different
+ emissions scenarios.
+ short_description: >-
+ This diagnostic calculates the linear trend in global mean surface temperature warming (since
+ the preindustrial) to respective cumulative anthropogenic CO2 emissions, usually
+ presented in units of degrees C per GtCO2. This relationship is usually calculated
+ from historical followed by future scenarios simulations.
+ why_it_matters: >-
+ It provides a valuable metric linking the physical climate system response to CO2
+ emissions that reflects climate sensitivity and the models' carbon cycle response
+ to emissions.
+ takeaway: >-
+ The inter-model spread reflects uncertainty associated with Zero Emissions
+ Commitment warming and uncertainty in carbon cycle feedback among the models.
+ plain_language:
+ why_it_matters: >-
+ TCRE is one of the few metrics that directly link climate change (global warming)
+ and policy planning (CO2 emissions). For instance, it quantifies how much total
+ CO2 emissions can be emitted to keep global warming at a certain threshold, such
+ as the Paris Agreement.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: transient-climate-response-emissions
+explorer_cards:
+ - title: "TCRE"
+ description: >-
+ Transient Climate Response to Cumulative Emissions -- the warming per
+ trillion tonnes of carbon emitted, linking emissions budgets to
+ temperature targets.
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: transient-climate-response-emissions
+ title: "Transient Climate Response to Cumulative Emissions (TCRE)"
+ description: >-
+ TCRE quantifies the global mean surface temperature increase per unit
+ of cumulative CO2 emissions. It combines the physical climate
+ sensitivity with the carbon cycle response and is central to estimating
+ remaining carbon budgets consistent with temperature targets such as
+ the Paris Agreement.
+ interpretation: >-
+ Higher TCRE values mean less cumulative CO2 can be emitted before
+ reaching a given warming threshold.
+ metric_units: "K/TtC"
+ other_filters:
+ metric: tcre
+ grouping_config:
+ group_by: metric
+ hue: metric
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: transient-climate-response-emissions
+ title: "TCRE Regression Plots"
+ description: >-
+ Scatter plots of cumulative CO2 emissions against global mean
+ temperature change, with regression lines used to derive TCRE.
+ span: 2
diff --git a/backend/static/collections/4-4_zero-emissions-commitment.yaml b/backend/static/collections/4-4_zero-emissions-commitment.yaml
new file mode 100644
index 0000000..32729b9
--- /dev/null
+++ b/backend/static/collections/4-4_zero-emissions-commitment.yaml
@@ -0,0 +1,68 @@
+id: "4.4"
+name: "Zero emissions commitment (ZEC)"
+theme: "Earth System"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: ""
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_zec.html"
+content:
+ description: >-
+ ZEC represents the long-term change in global mean surface temperature following the cessation of CO2 emissions after a sustained period of increase.
+ It reflects the balance between ongoing ocean heat uptake and the reduction in radiative forcing due to decreasing atmospheric CO2 levels.
+ short_description: >-
+ The Zero Emissions Commitment (ZEC) quantifies the change in global mean temperature
+ expected to occur after net carbon dioxide (CO2) emissions cease. ZEC is therefore
+ important to consider when estimating the remaining carbon budget. Calculation of
+ ZEC requires dedicated simulations with emissions set to zero, branching off a base
+ simulation with emissions. In CMIP6 the simulations were part of ZECMIP, with the
+ simulations called esm-1pct-brch-xPgC branching off the 1pctCO2 simulation when
+ emissions reach x PgC. The default x was 1000PgC, with additional simulations for
+ 750PgC and 2000PgC. In CMIP7, ZEC simulations (esm-flat10-zec) are part of the
+ fast track and branch off (esm-flat10) with constant emissions of 10GtC/yr at year
+ 100 (Sanderson 2024).
+ why_it_matters: >-
+ In the first order, ZEC informs how the Earth system feedback processes behave
+ once anthropogenic CO2 emissions have ceased. ZEC also reflects the legacy of past
+ climate change and hence ZEC will likely be sensitive to the pathways toward net
+ zero emissions.
+ plain_language:
+ why_it_matters: >-
+ ZEC provides an additional constraint when quantifying global warming with
+ cumulative emissions (TCRE), hence is relevant for emission scenario study and
+ planning.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: zero-emission-commitment
+explorer_cards:
+ - title: "Zero Emissions Commitment"
+ description: >-
+ Temperature change expected after CO2 emissions cease, informing carbon
+ budget estimates and net-zero policy planning.
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: zero-emission-commitment
+ title: "Zero Emissions Commitment (ZEC)"
+ description: >-
+ ZEC represents the additional global warming (or cooling) that occurs
+ after anthropogenic CO2 emissions are set to zero. Positive values
+ indicate continued warming; negative values indicate the climate system
+ begins to cool once emissions stop.
+ interpretation: >-
+ A positive ZEC means that even after emissions cease, temperatures
+ continue to rise, implying a stricter carbon budget is needed.
+ metric_units: "K"
+ show_zero_line: true
+ other_filters:
+ metric: zec
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: zero-emission-commitment
+ title: "ZEC Temperature Evolution"
+ description: >-
+ Time series and scatter plots showing the temperature response after
+ emissions cessation for each model.
+ span: 2
diff --git a/backend/static/collections/4-5_historical-changes.yaml b/backend/static/collections/4-5_historical-changes.yaml
new file mode 100644
index 0000000..19b0ac1
--- /dev/null
+++ b/backend/static/collections/4-5_historical-changes.yaml
@@ -0,0 +1,86 @@
+id: "4.5"
+name: "Historical changes in climate variables (time series, trends)"
+theme: "Earth System"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "HadCRUT5-0-2-0, CERES-EBAF-4-2, ERA-5, GPCP-SG-2-3, HadISST-1-1"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ref.html"
+content:
+ description: >-
+ These metrics quantify how well models simulate observed state and trends at the
+ large scale. In addition to the global mean, time series and trends of key climate
+ variables like temperature, pressure, wind and humidity must be investigated
+ locally.
+ short_description: >-
+ Time series, linear trend, and annual cycle for IPCC regions
+ why_it_matters: >-
+ To assess the ability of climate models to look into the future, it is important to
+ evaluate them on the past climate state and changes, which have been observed in
+ recent decades, where observations and reanalysis data provide a reference.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: regional-historical-annual-cycle
+ - provider_slug: esmvaltool
+ diagnostic_slug: regional-historical-timeseries
+ - provider_slug: esmvaltool
+ diagnostic_slug: regional-historical-trend
+explorer_cards:
+ - title: "Historical Changes"
+ description: >-
+ Regional trends and annual cycles of key climate variables, comparing
+ model simulations against observations and reanalysis for IPCC reference
+ regions.
+ content:
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: regional-historical-trend
+ title: "Regional Temperature Trends"
+ description: >-
+ Linear trends in near-surface air temperature (tas) across IPCC
+ reference regions. Comparing modelled and observed trends reveals
+ whether models reproduce the spatial pattern of recent warming.
+ metric_units: "K/decade"
+ other_filters:
+ variable_id: tas
+ metric: trend
+ grouping_config:
+ group_by: region
+ hue: region
+ - type: box-whisker-chart
+ provider: esmvaltool
+ diagnostic: regional-historical-trend
+ title: "Regional Precipitation Trends"
+ description: >-
+ Linear trends in precipitation (pr) across IPCC reference regions.
+ Precipitation trends are harder for models to capture due to large
+ internal variability and complex regional drivers.
+ metric_units: "mm/day/decade"
+ other_filters:
+ variable_id: pr
+ metric: trend
+ grouping_config:
+ group_by: region
+ hue: region
+ - type: series-chart
+ provider: esmvaltool
+ diagnostic: regional-historical-annual-cycle
+ title: "Annual Cycle of Temperature (Global)"
+ description: >-
+ Monthly climatology of near-surface air temperature for the global
+ mean, comparing individual models with reanalysis data.
+ span: 2
+ metric_units: "K"
+ other_filters:
+ region: Global
+ statistic: tas regional mean
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: regional-historical-trend
+ title: "Regional Trend Maps"
+ description: >-
+ Maps of linear trends across IPCC reference regions for temperature,
+ precipitation, and other variables.
+ span: 2
diff --git a/backend/static/collections/5-3_global-warming-levels.yaml b/backend/static/collections/5-3_global-warming-levels.yaml
new file mode 100644
index 0000000..8f147ff
--- /dev/null
+++ b/backend/static/collections/5-3_global-warming-levels.yaml
@@ -0,0 +1,51 @@
+id: "5.3"
+name: "Evaluation of key climate variables at global warming levels"
+theme: "Impacts and Adaptation"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "GPCP-SG-2-3, HadCRUT5-0-2-0"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_calculate_gwl_exceedance_stats.html"
+content:
+ description: >-
+ This recipe calculates years of Global Warming Level (GWL) as described in
+ Swaminathan et al (2022). Time series of the anomalies in annual global mean
+ surface air temperature (GSAT) are calculated with respect to the 1850-1900
+ time-mean of each individual time series. To limit the influence of short-term
+ variability, a 21-year centered running mean is applied to the time series. Once
+ the years of exceedance are calculated, the time averaged global mean and standard
+ deviation for the multimodel ensemble over the 21-year period around the year of
+ exceedance are plotted. By selecting specific scenarios, the multimodel mean and
+ spread for a single future scenario can be plotted. Multimodel mean and standard
+ deviation global maps of precipitation and temperature anomalies are plotted for
+ each available global warming level.
+ short_description: >-
+ This diagnostic calculates years of Global Warming Level (GWL) exceedances in CMIP
+ models as described in Swaminathan et al (2022). Time series of the anomalies in
+ annual global mean surface air temperature (GSAT) are calculated with respect to
+ the 1850-1900 time-mean of each individual time series. To limit the influence of
+ short-term variability, a 21-year centered running mean is applied to the time
+ series. The year at which the time series exceeds warming levels or temperatures
+ such as 1.5C is then recorded for the specific model ensemble member and future
+ scenario. Once the years of exceedance are calculated, the time averaged global
+ mean and standard deviation for the multimodel ensemble over the 21-year period
+ around the year of exceedance are plotted.
+ takeaway: >-
+ After the Paris agreements, the importance of setting the intensity and pattern of
+ anomalies for key climate variables has increased significantly. Furthermore,
+ overshoot experiments will be a substantial part of the CMIP7 effort, therefore it
+ is of paramount importance to assess the uncertainty among models on the projected
+ changes depending on the achieved warming level in different scenarios.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: climate-at-global-warming-levels
+explorer_cards:
+ - title: "Warming Levels"
+ description: >-
+ Climate conditions at different global warming levels, relevant to policy targets.
+ content:
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: climate-at-global-warming-levels
+ title: "Global Mean Temperature Change at Warming Levels"
+ span: 2
+ placeholder: true
diff --git a/backend/static/collections/5-4_fire-climate-drivers.yaml b/backend/static/collections/5-4_fire-climate-drivers.yaml
new file mode 100644
index 0000000..9dcbecb
--- /dev/null
+++ b/backend/static/collections/5-4_fire-climate-drivers.yaml
@@ -0,0 +1,118 @@
+id: "5.4"
+name: "Climate drivers for fire (fire burnt area, fire weather and fuel continuity)"
+theme: "Impacts and Adaptation"
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "GFED-5 (source_id not confirmed)"
+provider_link: "https://docs.esmvaltool.org/en/latest/recipes/recipe_ref_fire.html"
+content:
+ description: >-
+ This diagnostic computes burnt area from the climate models using Bayesian model
+ ConFire, which in turn is based on the observational data. To show the predominant
+ drivers of the burn areas the diagnostic also computes contribution of fire
+ controls: weather or fuel load/moisture.
+ short_description: >-
+ The diagnostic relies on the processing of fire climate drivers through the ConFire
+ model and is based on Jones et al. (2024). The diagnostic computes the burnt
+ fraction for each grid cell based on a number of drivers. Additionally, the
+ respective controls due to fire weather and fuel load/continuity are computed. The
+ stochastic control corresponds to the unmodelled processed influencing to fire
+ occurrence.
+ why_it_matters: >-
+ Wildfires are becoming an extremely important hazard, in particular in the changing
+ climate. However, depending on the region, the exact wildfires and hence burnt area
+ can be driven by different factors. By computing the burnt area one can determine
+ the wildfire extent over the globe and which factor -- weather or fuel -- has
+ primary responsibility in which region.
+ takeaway: >-
+ The spread of burnt area in different climates, and uneven contribution of
+ different drivers in different forested areas.
+ plain_language:
+ why_it_matters: >-
+ The results are important for the adaptation community to predict the amount of
+ burnt area in different climates to sufficiently distribute resources.
+diagnostics:
+ - provider_slug: esmvaltool
+ diagnostic_slug: climate-drivers-for-fire
+ - provider_slug: ilamb
+ diagnostic_slug: burntfractionall-gfed
+explorer_cards:
+ - title: "Fire & Burnt Area"
+ description: >-
+ Model evaluation of burnt area and the climate drivers of fire, comparing
+ simulated fire extent with GFED observations.
+ content:
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: burntfractionall-gfed
+ title: "Burnt Fraction Bias"
+ description: >-
+ Spatial bias in modelled burnt fraction relative to GFED observations.
+ Values near zero indicate good agreement; large positive or negative
+ values highlight regions where models over- or under-estimate fire
+ extent.
+ metric_units: ""
+ show_zero_line: true
+ other_filters:
+ metric: Bias
+ region: global
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ - type: box-whisker-chart
+ provider: ilamb
+ diagnostic: burntfractionall-gfed
+ title: "Burnt Fraction Spatial Distribution"
+ description: >-
+ Spatial distribution score for burnt fraction, measuring how well
+ models reproduce the observed geographic pattern of fire occurrence.
+ metric_units: ""
+ other_filters:
+ metric: Spatial Distribution
+ region: global
+ grouping_config:
+ group_by: statistic
+ hue: statistic
+ - type: series-chart
+ provider: ilamb
+ diagnostic: burntfractionall-gfed
+ title: "Global Burnt Fraction Time Series"
+ description: >-
+ Time series of global mean burnt fraction for each model compared with
+ GFED reference data.
+ span: 2
+ other_filters:
+ metric: trace_global
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ - type: series-chart
+ provider: ilamb
+ diagnostic: burntfractionall-gfed
+ title: "Burnt Fraction Annual Cycle (Global)"
+ description: >-
+ Annual cycle of global burnt fraction, comparing the seasonal timing
+ and amplitude of fire activity across models and observations.
+ span: 2
+ other_filters:
+ metric: cycle_global
+ grouping_config:
+ group_by: source_id
+ hue: source_id
+ - type: figure-gallery
+ provider: esmvaltool
+ diagnostic: climate-drivers-for-fire
+ title: "Fire Climate Drivers (ConFire)"
+ description: >-
+ Maps of modelled burnt area and the relative contribution of fire
+ weather versus fuel load/continuity, computed using the ConFire
+ Bayesian model.
+ span: 2
+ - type: figure-gallery
+ provider: ilamb
+ diagnostic: burntfractionall-gfed
+ title: "Burnt Fraction Spatial Maps"
+ description: >-
+ Spatial maps of burnt fraction bias, RMSE, and model-observation
+ comparison for individual models.
+ span: 2
diff --git a/backend/static/collections/README.md b/backend/static/collections/README.md
new file mode 100644
index 0000000..fae6d9b
--- /dev/null
+++ b/backend/static/collections/README.md
@@ -0,0 +1,86 @@
+# Collection YAML Files
+
+This directory contains per-collection YAML files that provide display metadata for the CMIP7 Assessment Fast Track (AFT) diagnostic collections shown on the REF dashboard.
+
+## File naming
+
+Files are named `{id}_{short-name}.yaml`, e.g. `1-1_sea-ice-sensitivity.yaml`. The ID inside the file (not the filename) is what the loader uses, so filenames are purely for human readability.
+
+`themes.yaml` maps collections into thematic groups for the explorer UI.
+
+## Schema
+
+Each collection YAML has the following structure:
+
+```yaml
+id: "1.1" # AFT diagnostic ID (must be unique across all files)
+name: "Sea ice sensitivity to warming" # Full display name
+theme: "Oceans and sea ice" # Thematic category
+endorser: "CMIP Model Benchmarking Task Team"
+version_control: "version 1 - 24-11-04 REF launch"
+reference_dataset: "OSI SAF/CCI, HadCRUT"
+provider_link: "https://..." # Link to recipe/methodology docs
+
+content:
+ description: >- # What this diagnostic is doing (for ESM community)
+ ...
+ short_description: >- # Brief one-liner summary
+ ...
+ why_it_matters: >- # Why this diagnostic is important (for ESM community)
+ ...
+ takeaway: >- # What you should take away from the results (for ESM community)
+ ...
+ plain_language: # Optional: simplified versions for non-specialist audiences
+ description: >-
+ ...
+ why_it_matters: >-
+ ...
+ takeaway: >-
+ ...
+
+diagnostics: # Links to REF diagnostic implementations
+ - provider_slug: esmvaltool
+ diagnostic_slug: sea-ice-sensitivity
+
+explorer_cards: [] # Card definitions for the data explorer UI
+```
+
+### Content fields
+
+| Field | Purpose |
+|---|---|
+| `description` | What the diagnostic calculates and how (technical) |
+| `short_description` | Brief summary, typically shown in listings |
+| `why_it_matters` | Scientific importance and relevance |
+| `takeaway` | What users should learn from the results |
+| `plain_language.*` | Simplified versions of the above for non-ESM audiences |
+
+All content fields are optional. Not every collection has all fields populated.
+
+### Explorer cards
+
+The `explorer_cards` list defines visualisation cards for the data explorer. See `backend/src/ref_backend/core/collections.py` for the full card schema (`AFTCollectionCardContent`). Supported card types:
+
+- `box-whisker-chart` - Box and whisker plots for scalar metrics
+- `series-chart` - Time series or seasonal cycle line charts
+- `taylor-diagram` - Taylor diagrams for spatial performance
+- `figure-gallery` - Gallery of pre-rendered figures
+
+## Adding or editing a collection
+
+1. Create or edit a YAML file in this directory following the schema above
+2. Ensure the `id` field is unique and matches the AFT diagnostic numbering
+3. If adding a new collection, add its ID to the appropriate theme in `themes.yaml`
+4. Run `uv run pytest tests/test_api/test_api_explorer.py` to validate
+
+The collection loader (`core/collections.py`) validates files on startup using Pydantic. Invalid files are skipped with a warning.
+
+## Regenerating client types
+
+After changing the content schema (adding new fields to the Pydantic models), regenerate the frontend TypeScript client:
+
+```bash
+make generate-client
+```
+
+This exports the OpenAPI schema from the backend and generates `frontend/src/client/`. Never edit generated client files by hand.
diff --git a/backend/static/collections/themes.yaml b/backend/static/collections/themes.yaml
new file mode 100644
index 0000000..84c2b7a
--- /dev/null
+++ b/backend/static/collections/themes.yaml
@@ -0,0 +1,24 @@
+atmosphere:
+ title: "Atmosphere"
+ description: "Atmospheric diagnostics including variability modes, clouds, and radiation."
+ collections: ["3.1", "3.2", "3.3", "3.6", "3.7"]
+
+earth-system:
+ title: "Earth System"
+ description: "Earth system diagnostics including climate sensitivity and ENSO."
+ collections: ["1.3", "4.1", "4.2", "4.3", "4.4", "4.5"]
+
+impact-and-adaptation:
+ title: "Impact and Adaptation"
+ description: "Diagnostics related to climate impacts, adaptation, and warming levels."
+ collections: ["5.3", "5.4"]
+
+land:
+ title: "Land and Land Ice"
+ description: "Land surface diagnostics including carbon cycle, hydrology, and vegetation."
+ collections: ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"]
+
+ocean:
+ title: "Ocean and Sea Ice"
+ description: "Ocean and sea ice diagnostics including circulation, temperature, and ice area."
+ collections: ["1.1", "1.2", "1.3", "1.4", "1.6"]
diff --git a/backend/static/diagnostics/esmvaltool.yaml b/backend/static/diagnostics/esmvaltool.yaml
new file mode 100644
index 0000000..24841cc
--- /dev/null
+++ b/backend/static/diagnostics/esmvaltool.yaml
@@ -0,0 +1,260 @@
+# ESMValTool Diagnostic Metadata
+#
+# Reference datasets and display metadata for ESMValTool diagnostics.
+
+esmvaltool/cloud-radiative-effects:
+ reference_datasets:
+ - slug: "esmvaltool.CERES-EBAF"
+ description: "CERES Energy Balanced and Filled - TOA radiation fluxes (rlut, rlutcs, rsut, rsutcs) (planned to move to obs4MIPs)"
+ type: "primary"
+ - slug: "esmvaltool.ESACCI-CLOUD"
+ description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
+ type: "secondary"
+ - slug: "esmvaltool.ISCCP-FH"
+ description: "ISCCP cloud properties (planned to move to obs4MIPs)"
+ type: "secondary"
+ display_name: "Cloud Radiative Effects"
+ description: >-
+ Plot climatologies and zonal mean profiles of cloud radiative effects
+ (shortwave + longwave) for a dataset.
+ tags: ["atmosphere", "clouds", "radiation"]
+
+esmvaltool/cloud-scatterplots-cli-ta:
+ reference_datasets:
+ - slug: "obs4mips.ERA-5"
+ description: "ERA5 - Air temperature (ta)"
+ type: "primary"
+ - slug: "esmvaltool.CALIPSO-ICECLOUD"
+ description: "CALIPSO - Ice cloud properties (cli), baked into recipe"
+ type: "secondary"
+ display_name: "Cloud-Temperature Scatterplots (cli vs ta)"
+ description: >-
+ Scatterplot of ice water content of cloud (cli) vs air temperature (ta).
+ Observational scatterplots for reference datasets are provided in the separate
+ "Cloud Scatterplots for Reference dataset" diagnostic.
+ tags: ["atmosphere", "clouds", "temperature"]
+
+esmvaltool/cloud-scatterplots-clivi-lwcre:
+ reference_datasets:
+ - slug: "esmvaltool.CERES-EBAF"
+ description: "CERES Energy Balanced and Filled - Longwave radiation (rlut, rlutcs; lwcre derived), baked into recipe"
+ type: "primary"
+ - slug: "esmvaltool.ESACCI-CLOUD"
+ description: "ESA CCI Cloud properties - Ice water path (clivi), baked into recipe"
+ type: "secondary"
+ display_name: "Cloud-Radiation Scatterplots (clivi vs lwcre)"
+ description: "Scatterplot of ice water path (clivi) vs longwave cloud radiative effect (lwcre)."
+ tags: ["atmosphere", "clouds", "radiation"]
+
+esmvaltool/cloud-scatterplots-clt-swcre:
+ reference_datasets:
+ - slug: "esmvaltool.CERES-EBAF"
+ description: "CERES Energy Balanced and Filled - Shortwave radiation (rsut, rsutcs; swcre derived), baked into recipe"
+ type: "primary"
+ - slug: "esmvaltool.ESACCI-CLOUD"
+ description: "ESA CCI Cloud properties - Total cloud fraction (clt), baked into recipe"
+ type: "secondary"
+ display_name: "Cloud-Radiation Scatterplots (clt vs swcre)"
+ description: "Scatterplot of total cloud fraction (clt) vs shortwave cloud radiative effect (swcre)."
+ tags: ["atmosphere", "clouds", "radiation"]
+
+esmvaltool/cloud-scatterplots-clwvi-pr:
+ reference_datasets:
+ - slug: "obs4MIPs.GPCPv2.3"
+ description: "GPCP v2.3 - Precipitation (pr)"
+ type: "secondary"
+ - slug: "esmvaltool.ESACCI-CLOUD"
+ description: "ESA CCI Cloud properties (clwvi, planned to move to obs4MIPs)"
+ type: "secondary"
+ - slug: "esmvaltool.CERES-EBAF"
+ description: "CERES Energy Balanced and Filled - Radiation, baked into recipe"
+ type: "primary"
+ display_name: "Cloud-Precipitation Scatterplots (clwvi vs pr)"
+ description: "Scatterplot of condensed water path (clwvi) vs precipitation (pr)."
+ tags: ["atmosphere", "clouds", "precipitation"]
+
+esmvaltool/cloud-scatterplots-reference:
+ reference_datasets:
+ - slug: "obs4mips.ERA-5"
+ description: "ERA5 - Air temperature (ta), ingested via REF"
+ type: "primary"
+ - slug: "esmvaltool.CERES-EBAF"
+ description: "CERES Energy Balanced and Filled - Radiation, baked into recipe"
+ type: "primary"
+ - slug: "esmvaltool.CALIPSO-ICECLOUD"
+ description: "CALIPSO - Ice cloud properties (cli), baked into recipe"
+ type: "secondary"
+ - slug: "esmvaltool.ESACCI-CLOUD"
+ description: "ESA CCI Cloud properties (clt, clwvi, clivi), baked into recipe"
+ type: "secondary"
+ - slug: "esmvaltool.GPCP-V2.3"
+ description: "GPCP v2.3 - Precipitation (pr), hardcoded in update_recipe"
+ type: "secondary"
+ display_name: "Cloud Scatterplots for Reference dataset"
+ description: >-
+ Reference scatterplots of two cloud-relevant variables from observational datasets.
+ These provide the observational baseline for comparison with model cloud scatterplot diagnostics.
+ tags: ["atmosphere", "clouds"]
+
+esmvaltool/enso-basic-climatology:
+ reference_datasets:
+ - slug: "esmvaltool.GPCP-V2.3"
+ description: "GPCP v2.3 - Precipitation (pr), baked into recipe"
+ type: "secondary"
+ - slug: "esmvaltool.TROPFLUX"
+ description: "TROPFLUX (OBS6) - Sea surface temperature (tos) and wind stress (tauu), baked into recipe"
+ type: "secondary"
+ display_name: "ENSO Basic Climatology"
+ description: >-
+ Calculate the ENSO CLIVAR metrics for background climatology, assessing the
+ mean state of the tropical Pacific upon which ENSO events develop.
+ tags: ["ocean", "enso", "climatology"]
+
+esmvaltool/enso-characteristics:
+ reference_datasets:
+ - slug: "esmvaltool.TROPFLUX"
+ description: "TROPFLUX (OBS6) - Sea surface temperature (tos), hardcoded in update_recipe"
+ type: "primary"
+ display_name: "ENSO Characteristics"
+ description: >-
+ Calculate the ENSO CLIVAR metrics for basic ENSO characteristics,
+ evaluating the amplitude, frequency, and spatial structure of ENSO events.
+ tags: ["ocean", "enso", "variability"]
+
+esmvaltool/equilibrium-climate-sensitivity:
+ reference_datasets: []
+ display_name: "Equilibrium Climate Sensitivity (ECS)"
+ description: >-
+ Calculate the global mean equilibrium climate sensitivity for a dataset.
+ ECS is defined as the long-term global mean surface temperature change
+ in response to a doubling of atmospheric CO2 concentration, estimated
+ using the Gregory regression method from abrupt-4xCO2 experiments.
+ tags: ["climate-sensitivity", "feedback"]
+
+esmvaltool/transient-climate-response:
+ reference_datasets: []
+ display_name: "Transient Climate Response (TCR)"
+ description: >-
+ Calculate the global mean transient climate response for a dataset.
+ TCR is defined as the global mean surface temperature change at the time
+ of CO2 doubling in a 1pctCO2 experiment (1% per year CO2 increase).
+ tags: ["climate-sensitivity", "transient"]
+
+esmvaltool/transient-climate-response-emissions:
+ reference_datasets: []
+ display_name: "Transient Climate Response to Emissions (TCRE)"
+ description: >-
+ Calculate the global mean Transient Climate Response to Cumulative CO2 Emissions.
+ TCRE quantifies the relationship between global warming and total cumulative
+ carbon emissions, a key metric for carbon budget estimation.
+ tags: ["climate-sensitivity", "carbon-cycle"]
+
+esmvaltool/zero-emission-commitment:
+ reference_datasets: []
+ display_name: "Zero Emission Commitment (ZEC)"
+ description: >-
+ Calculate the global mean Zero Emission Commitment (ZEC) temperature.
+ ZEC measures the additional warming (or cooling) that occurs after all
+ CO2 emissions cease, reflecting the committed warming from past emissions.
+ tags: ["climate-sensitivity", "commitment"]
+
+esmvaltool/regional-historical-annual-cycle:
+ reference_datasets:
+ - slug: "obs4mips.ERA-5"
+ description: "ERA-5 - Sea level pressure (psl) and zonal wind (ua), ingested via REF"
+ type: "primary"
+ - slug: "esmvaltool.HadCRUT5"
+ description: "HadCRUT5 v5.0.1.0-analysis - Near-surface air temperature (tas), hardcoded in recipe"
+ type: "secondary"
+ - slug: "esmvaltool.GPCP-V2.3"
+ description: "GPCP v2.3 - Precipitation (pr), hardcoded in recipe (cannot be ingested, issue #260)"
+ type: "secondary"
+ - slug: "esmvaltool.ERA5-native6"
+ description: "ERA5 native6 - Specific humidity (hus), not yet on obs4MIPs"
+ type: "secondary"
+ display_name: "Regional Historical Annual Cycle"
+ description: >-
+ Plot the regional historical annual cycle of climate variables including
+ near-surface air temperature (tas), precipitation (pr), sea level pressure (psl),
+ zonal wind (ua), and specific humidity (hus). Regions are defined using
+ IPCC AR6 reference regions as described by Iturbide et al. (2020).
+ tags: ["regional", "seasonal-cycle", "historical"]
+
+esmvaltool/regional-historical-timeseries:
+ reference_datasets:
+ - slug: "obs4mips.ERA-5"
+ description: "ERA-5 - Sea level pressure (psl) and zonal wind (ua), ingested via REF"
+ type: "primary"
+ - slug: "esmvaltool.HadCRUT5"
+ description: "HadCRUT5 v5.0.1.0-analysis - Near-surface air temperature (tas), hardcoded in recipe"
+ type: "secondary"
+ - slug: "esmvaltool.GPCP-V2.3"
+ description: "GPCP v2.3 - Precipitation (pr), hardcoded in recipe (cannot be ingested, issue #260)"
+ type: "secondary"
+ - slug: "esmvaltool.ERA5-native6"
+ description: "ERA5 native6 - Specific humidity (hus), not yet on obs4MIPs"
+ type: "secondary"
+ display_name: "Regional Historical Timeseries"
+ description: >-
+ Plot the regional historical mean and anomaly timeseries of climate variables including
+ near-surface air temperature (tas), precipitation (pr), sea level pressure (psl),
+ zonal wind (ua), and specific humidity (hus). Regions are defined
+ using IPCC AR6 reference regions as described by Iturbide et al. (2020).
+ tags: ["regional", "timeseries", "historical"]
+
+esmvaltool/regional-historical-trend:
+ reference_datasets:
+ - slug: "obs4mips.ERA-5"
+ description: "ERA-5 - Sea level pressure (psl), near-surface air temperature (tas), and zonal wind (ua), ingested via REF"
+ type: "primary"
+ - slug: "esmvaltool.HadCRUT5"
+ description: "HadCRUT5 v5.0.1.0-analysis - Near-surface air temperature (tas), hardcoded in recipe"
+ type: "secondary"
+ - slug: "esmvaltool.GPCP-V2.3"
+ description: "GPCP v2.3 - Precipitation (pr), hardcoded in recipe (cannot be ingested, issue #260)"
+ type: "secondary"
+ - slug: "esmvaltool.ERA5-native6"
+ description: "ERA5 native6 - Specific humidity (hus) and precipitation (pr), hardcoded in recipe"
+ type: "secondary"
+ display_name: "Regional Historical Trends"
+ description: >-
+ Plot the regional historical trend of climate variables including near-surface
+ air temperature (tas), precipitation (pr), sea level pressure (psl),
+ zonal wind (ua), and specific humidity (hus). Regions are defined using IPCC AR6 reference
+ regions as described by Iturbide et al. (2020).
+ tags: ["regional", "trends", "historical"]
+
+esmvaltool/sea-ice-area-basic:
+ reference_datasets:
+ - slug: "esmvaltool.OSI-450-nh"
+ description: "OSI-450 v3 - Sea ice concentration (siconc), Northern Hemisphere, hardcoded in recipe"
+ type: "primary"
+ - slug: "esmvaltool.OSI-450-sh"
+ description: "OSI-450 v3 - Sea ice concentration (siconc), Southern Hemisphere, hardcoded in recipe"
+ type: "primary"
+ display_name: "Arctic and Antarctic Sea Ice Area Seasonal Cycle"
+ description: >-
+ Calculate the seasonal cycle and time series of Northern Hemisphere and
+ Southern Hemisphere sea ice area.
+ tags: ["sea-ice", "cryosphere"]
+
+esmvaltool/sea-ice-sensitivity:
+ reference_datasets: []
+ display_name: "Sea Ice Sensitivity"
+ description: >-
+ Calculate sea ice sensitivity, quantifying the relationship between sea ice area (siconc)
+ and global temperature change (tas). This is an inter-model comparison diagnostic
+ with no observational datasets ingested.
+ tags: ["sea-ice", "cryosphere", "sensitivity"]
+
+esmvaltool/climate-at-global-warming-levels:
+ reference_datasets: []
+ display_name: "Climate at Global Warming Levels"
+ description: "Calculate climate variables at global warming levels (e.g. 1.5C, 2C, 3C, 4C above pre-industrial)."
+ tags: ["scenarios", "warming-levels"]
+
+esmvaltool/climate-drivers-for-fire:
+ reference_datasets: []
+ display_name: "Climate Drivers for Fire"
+ description: "Calculate diagnostics regarding climate drivers for fire, including temperature, precipitation, and humidity conditions that influence fire risk."
+ tags: ["fire", "climate-drivers", "impacts"]
diff --git a/backend/static/diagnostics/ilamb.yaml b/backend/static/diagnostics/ilamb.yaml
new file mode 100644
index 0000000..3714288
--- /dev/null
+++ b/backend/static/diagnostics/ilamb.yaml
@@ -0,0 +1,134 @@
+# ILAMB Diagnostic Metadata
+#
+# Reference datasets and display metadata for ILAMB (International Land Model
+# Benchmarking) and IOMB (International Ocean Model Benchmarking) diagnostics.
+
+# Land diagnostics
+
+ilamb/gpp-wecann:
+ reference_datasets:
+ - slug: "obs4REF.WECANN"
+ description: "WECANN GPP dataset - Gross primary productivity (gpp)"
+ type: "primary"
+ - slug: "obs4REF.GPCPv2.3"
+ description: "GPCP v2.3 - Precipitation (pr), relationship dataset"
+ type: "secondary"
+ - slug: "ilamb.CRU4.02"
+ description: "CRU TS v4.02 - Near-surface air temperature (tas), relationship dataset"
+ type: "secondary"
+ display_name: "Gross Primary Productivity (WECANN)"
+ description: "Apply the standard ILAMB analysis of gross primary productivity (gpp) with respect to the WECANN reference dataset."
+ tags: ["land", "carbon-cycle", "vegetation"]
+
+ilamb/gpp-fluxnet2015:
+ reference_datasets:
+ - slug: "obs4REF.FLUXNET2015"
+ description: "FLUXNET2015 - Tower-based GPP measurements"
+ type: "primary"
+ display_name: "Gross Primary Productivity (FLUXNET2015)"
+ description: "Apply the standard ILAMB analysis of gross primary productivity (gpp) with respect to the FLUXNET2015 tower-based reference dataset."
+ tags: ["land", "carbon-cycle", "vegetation", "flux-towers"]
+
+ilamb/mrro-lora:
+ reference_datasets:
+ - slug: "obs4REF.LORA"
+ description: "LORA dataset - Runoff observations"
+ type: "primary"
+ display_name: "Runoff (LORA)"
+ description: "Apply the standard ILAMB analysis of total runoff (mrro) with respect to the LORA reference dataset."
+ tags: ["land", "hydrology", "runoff"]
+
+ilamb/mrsos-wangmao:
+ reference_datasets:
+ - slug: "obs4REF.WangMao"
+ description: "Wang-Mao dataset - Soil moisture (mrsos)"
+ type: "primary"
+ display_name: "Surface Soil Moisture (WangMao)"
+ description: "Apply the standard ILAMB analysis of moisture in upper portion of soil column (mrsos) with respect to the WangMao reference dataset."
+ tags: ["land", "hydrology", "soil-moisture"]
+
+ilamb/csoil-hwsd2:
+ reference_datasets:
+ - slug: "obs4REF.HWSD2"
+ description: "Harmonized World Soil Database v2 - Soil carbon content"
+ type: "primary"
+ display_name: "Soil Carbon (HWSD2)"
+ description: "Apply the standard ILAMB analysis of carbon mass in soil pool (cSoil) with respect to the HWSD2 reference dataset."
+ tags: ["land", "carbon-cycle", "soil"]
+
+ilamb/lai-avh15c1:
+ reference_datasets:
+ - slug: "obs4REF.AVH15C1"
+ description: "AVHRR - Leaf area index observations"
+ type: "primary"
+ display_name: "Leaf Area Index (AVH15C1)"
+ description: "Apply the standard ILAMB analysis of leaf area index (lai) with respect to the AVH15C1 (AVHRR) reference dataset."
+ tags: ["land", "vegetation", "remote-sensing"]
+
+ilamb/nbp-hoffman:
+ reference_datasets:
+ - slug: "obs4REF.Hoffman"
+ description: "Hoffman dataset - Net biome productivity"
+ type: "primary"
+ display_name: "Net Biome Productivity (Hoffman)"
+ description: "Apply the standard ILAMB analysis of net biome productivity (nbp) with respect to the Hoffman reference dataset."
+ tags: ["land", "carbon-cycle", "net-flux"]
+
+ilamb/snc-esacci:
+ reference_datasets:
+ - slug: "obs4REF.CCI-CryoClim-FSC"
+ description: "ESA CCI Snow - Snow cover fraction"
+ type: "primary"
+ display_name: "Snow Cover (ESA CCI)"
+ description: "Apply the standard ILAMB analysis of snow cover fraction (snc) with respect to the ESA CCI CryoClim fractional snow cover reference dataset."
+ tags: ["land", "cryosphere", "snow"]
+
+ilamb/burntfractionall-gfed:
+ reference_datasets:
+ - slug: "obs4REF.GFED"
+ description: "Global Fire Emissions Database - Burnt area fraction"
+ type: "primary"
+ display_name: "Burnt Fraction (GFED)"
+ description: "Apply the standard ILAMB analysis of burnt area fraction (burntFractionAll) with respect to the GFED reference dataset."
+ tags: ["land", "fire", "disturbance"]
+
+ilamb/emp-gleamgpcp:
+ reference_datasets:
+ - slug: "obs4REF.GLEAMv3.3a"
+ description: "GLEAM v3.3a - Evapotranspiration (et)"
+ type: "primary"
+ - slug: "obs4REF.GPCP-2-3"
+ description: "GPCP v2.3 (obs4REF) - Precipitation (pr)"
+ type: "primary"
+ display_name: "Evaporation minus Precipitation (GLEAM/GPCP)"
+ description: "Apply the standard ILAMB analysis of evaporation minus precipitation (E-P) using GLEAMv3.3a evapotranspiration and GPCP v2.3 precipitation reference datasets."
+ tags: ["land", "hydrology", "water-balance"]
+
+# Ocean diagnostics (IOMB)
+
+ilamb/thetao-woa2023-surface:
+ reference_datasets:
+ - slug: "obs4REF.WOA2023"
+ description: "World Ocean Atlas 2023 - Sea water potential temperature (thetao, surface, mapped to tos)"
+ type: "primary"
+ display_name: "Sea Water Temperature (WOA2023 Surface)"
+ description: "Apply the standard ILAMB analysis of sea water potential temperature (thetao) at the surface with respect to the WOA2023 reference dataset."
+ tags: ["ocean", "temperature", "surface"]
+
+ilamb/so-woa2023-surface:
+ reference_datasets:
+ - slug: "obs4REF.WOA2023"
+ description: "World Ocean Atlas 2023 - Sea water salinity (so, surface)"
+ type: "primary"
+ display_name: "Sea Water Salinity (WOA2023 Surface)"
+ description: "Apply the standard ILAMB analysis of sea water salinity (so) at the surface with respect to the WOA2023 reference dataset."
+ tags: ["ocean", "salinity", "surface"]
+
+ilamb/amoc-rapid:
+ reference_datasets:
+ - slug: "obs4REF.RAPID"
+ description: "RAPID array - Atlantic Meridional Overturning Circulation (amoc, from msftmz)"
+ type: "primary"
+ display_name: "Atlantic Meridional Overturning Circulation (RAPID)"
+ description: "Apply the standard ILAMB analysis of the Atlantic Meridional Overturning Circulation (AMOC) with respect to the RAPID array reference dataset."
+ tags: ["ocean", "circulation", "amoc"]
diff --git a/backend/static/diagnostics/metadata.yaml b/backend/static/diagnostics/metadata.yaml
deleted file mode 100644
index 4d2a1ad..0000000
--- a/backend/static/diagnostics/metadata.yaml
+++ /dev/null
@@ -1,526 +0,0 @@
-# Diagnostic Metadata Overrides
-#
-# This file provides additional metadata for diagnostics that supplements or overrides
-# the default values from the diagnostic implementations. It's particularly useful for:
-# - Exposing which reference datasets are used by each diagnostic
-# - Providing display names and descriptions
-# - Adding tags for categorization
-#
-
-# ============================================================================
-# PMP DIAGNOSTICS
-# ============================================================================
-
-pmp/annual-cycle:
- reference_datasets:
- - slug: "obs4MIPs-climatology.ERA-5"
- description: "ERA5 Reanalysis - Primary climatology for surface temperature (ts), winds (uas, vas), and sea level pressure (psl)"
- type: "primary"
- - slug: "obs4MIPs-climatology.GPCP-Monthly-3-2"
- description: "Global Precipitation Climatology Project v3.2 - Precipitation reference"
- type: "primary"
- - slug: "obs4MIPs-climatology.CERES-EBAF-4-2"
- description: "CERES Energy Balanced and Filled v4.2 - Radiation fields (rlds, rlus, rsds, rsus, rlut, rsut)"
- type: "primary"
- display_name: "Annual Cycle Analysis"
- description: >-
- Calculate the annual cycle for a dataset. Variables provided include surface temperature (ts),
- precipitation (pr), winds (uas, vas), sea level pressure (psl), and radiation fields
- (rlds, rlus, rsds, rsus, rlut, rsut).
- tags: ["atmosphere", "seasonal-cycle", "climatology"]
-
-pmp/enso_perf:
- reference_datasets:
- - slug: "obs4mips.GPCP-Monthly-3-2"
- description: "Global Precipitation Climatology Project v3.2 - Precipitation patterns"
- type: "primary"
- - slug: "obs4mips.TropFlux-1-0"
- description: "TropFlux v1.0 - Tropical flux data"
- type: "primary"
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and Sea Surface Temperature v1.1 - SST reference"
- type: "primary"
- display_name: "ENSO Performance Metrics"
- description: >-
- Calculate the ENSO performance metrics for a dataset. These metrics evaluate
- how well a model simulates key aspects of El Nino-Southern Oscillation variability.
- tags: ["ocean", "atmosphere", "enso", "variability"]
-
-pmp/enso_tel:
- reference_datasets:
- - slug: "obs4mips.GPCP-Monthly-3-2"
- description: "Global Precipitation Climatology Project v3.2 - Precipitation teleconnections"
- type: "primary"
- - slug: "obs4mips.TropFlux-1-0"
- description: "TropFlux v1.0 - Tropical flux data"
- type: "primary"
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and SST v1.1 - SST teleconnections"
- type: "primary"
- display_name: "ENSO Teleconnections"
- description: >-
- Calculate the ENSO teleconnection metrics for a dataset. Teleconnection metrics
- assess how well a model captures the remote climate impacts of ENSO events
- on temperature and precipitation patterns worldwide.
- tags: ["ocean", "atmosphere", "enso", "teleconnections"]
-
-pmp/enso_proc:
- reference_datasets:
- - slug: "obs4mips.GPCP-Monthly-3-2"
- description: "Global Precipitation Climatology Project v3.2"
- type: "primary"
- - slug: "obs4mips.TropFlux-1-0"
- description: "TropFlux v1.0 - Surface fluxes"
- type: "primary"
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and SST v1.1"
- type: "primary"
- - slug: "obs4mips.CERES-EBAF-4-2"
- description: "CERES Energy Balanced and Filled v4.2 - Radiation fluxes"
- type: "primary"
- display_name: "ENSO Processes"
- description: >-
- Calculate the ENSO process-oriented metrics for a dataset. These metrics evaluate
- the physical processes that drive ENSO, including ocean-atmosphere coupling,
- thermocline feedbacks, and energy balance.
- tags: ["ocean", "atmosphere", "enso", "energy-balance"]
-
-# Extratropical Modes of Variability (Sea Surface Temperature modes)
-pmp/extratropical-modes-of-variability-pdo:
- reference_datasets:
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and SST v1.1 - SST for Pacific Decadal Oscillation"
- type: "primary"
- display_name: "Pacific Decadal Oscillation (PDO)"
- description: >-
- Calculate the extratropical modes of variability for the Pacific Decadal Oscillation (PDO),
- a pattern of Pacific climate variability characterized by SST anomalies in the North Pacific.
- tags: ["ocean", "variability", "pacific", "decadal"]
-
-pmp/extratropical-modes-of-variability-npgo:
- reference_datasets:
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and SST v1.1 - SST for North Pacific Gyre Oscillation"
- type: "primary"
- display_name: "North Pacific Gyre Oscillation (NPGO)"
- description: >-
- Calculate the extratropical modes of variability for the North Pacific Gyre Oscillation (NPGO),
- which tracks changes in the strength of the North Pacific subtropical and subpolar gyres.
- tags: ["ocean", "variability", "pacific"]
-
-pmp/extratropical-modes-of-variability-amo:
- reference_datasets:
- - slug: "obs4mips.HadISST-1-1"
- description: "Hadley Centre Sea Ice and SST v1.1 - SST for Atlantic Multidecadal Oscillation"
- type: "primary"
- display_name: "Atlantic Multidecadal Oscillation (AMO)"
- description: >-
- Calculate the extratropical modes of variability for the Atlantic Multidecadal Oscillation (AMO),
- a coherent pattern of variability in North Atlantic SSTs with a period of 60-80 years.
- tags: ["ocean", "variability", "atlantic", "decadal"]
-
-# Extratropical Modes of Variability (Sea Level Pressure modes)
-pmp/extratropical-modes-of-variability-nao:
- reference_datasets:
- - slug: "obs4mips.20CR"
- description: "20th Century Reanalysis - Sea level pressure for North Atlantic Oscillation"
- type: "primary"
- display_name: "North Atlantic Oscillation (NAO)"
- description: >-
- Calculate the extratropical modes of variability for the North Atlantic Oscillation (NAO),
- a large-scale pattern of sea level pressure variability between the Icelandic Low and Azores High.
- tags: ["atmosphere", "variability", "atlantic", "pressure"]
-
-pmp/extratropical-modes-of-variability-nam:
- reference_datasets:
- - slug: "obs4mips.20CR"
- description: "20th Century Reanalysis - Sea level pressure for Northern Annular Mode"
- type: "primary"
- display_name: "Northern Annular Mode (NAM)"
- description: >-
- Calculate the extratropical modes of variability for the Northern Annular Mode (NAM),
- the dominant mode of sea level pressure variability in the Northern Hemisphere extratropics.
- tags: ["atmosphere", "variability", "hemispheric", "pressure"]
-
-pmp/extratropical-modes-of-variability-pna:
- reference_datasets:
- - slug: "obs4mips.20CR"
- description: "20th Century Reanalysis - Sea level pressure for Pacific North American Pattern"
- type: "primary"
- display_name: "Pacific North American Pattern (PNA)"
- description: >-
- Calculate the extratropical modes of variability for the Pacific North American (PNA) pattern,
- a prominent mode of atmospheric variability influencing weather over the Pacific and North America.
- tags: ["atmosphere", "variability", "pacific", "pressure"]
-
-pmp/extratropical-modes-of-variability-npo:
- reference_datasets:
- - slug: "obs4mips.20CR"
- description: "20th Century Reanalysis - Sea level pressure for North Pacific Oscillation"
- type: "primary"
- display_name: "North Pacific Oscillation (NPO)"
- description: >-
- Calculate the extratropical modes of variability for the North Pacific Oscillation (NPO),
- defined by sea level pressure variability over the North Pacific.
- tags: ["atmosphere", "variability", "pacific", "pressure"]
-
-pmp/extratropical-modes-of-variability-sam:
- reference_datasets:
- - slug: "obs4mips.20CR"
- description: "20th Century Reanalysis - Sea level pressure for Southern Annular Mode"
- type: "primary"
- display_name: "Southern Annular Mode (SAM)"
- description: >-
- Calculate the extratropical modes of variability for the Southern Annular Mode (SAM),
- the dominant mode of sea level pressure variability in the Southern Hemisphere extratropics.
- tags: ["atmosphere", "variability", "southern-hemisphere", "pressure"]
-
-# ============================================================================
-# ESMValTool DIAGNOSTICS
-# ============================================================================
-
-esmvaltool/cloud-radiative-effects:
- reference_datasets:
- - slug: "esmvaltool.CERES-EBAF"
- description: "CERES Energy Balanced and Filled - TOA radiation fluxes (planned to move to obs4MIPs)"
- type: "primary"
- - slug: "esmvaltool.ESACCI-CLOUD"
- description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- - slug: "esmvaltool.ISCCP-FH"
- description: "ISCCP cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "Cloud Radiative Effects"
- description: >-
- Plot climatologies and zonal mean profiles of cloud radiative effects
- (shortwave + longwave) for a dataset.
- tags: ["atmosphere", "clouds", "radiation"]
-
-esmvaltool/cloud-scatterplots-cli-ta:
- reference_datasets:
- - slug: "obs4mips.ERA-5"
- description: "ERA5 - Air temperature (ta)"
- type: "primary"
- - slug: "esmvaltool.CALIPSO-ICECLOUD"
- description: "Ice Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "Cloud-Temperature Scatterplots (cli vs ta)"
- description: >-
- Scatterplot of ice water content of cloud (cli) vs air temperature (ta).
- Observational scatterplots for reference datasets are provided in the separate
- "Cloud Scatterplots for Reference dataset" diagnostic.
- tags: ["atmosphere", "clouds", "temperature"]
-
-esmvaltool/cloud-scatterplots-clivi-lwcre:
- reference_datasets:
- - slug: "esmvaltool.CERES-EBAF"
- description: "CERES Energy Balanced and Filled - TOA radiation fluxes (planned)"
- type: "primary"
- - slug: "esmvaltool.ESACCI-CLOUD"
- description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "Cloud-Radiation Scatterplots (clivi vs lwcre)"
- description: "Scatterplot of ice water path (clivi) vs longwave cloud radiative effect (lwcre)."
- tags: ["atmosphere", "clouds", "radiation"]
-
-esmvaltool/cloud-scatterplots-clt-swcre:
- reference_datasets:
- - slug: "esmvaltool.CERES-EBAF"
- description: "CERES Energy Balanced and Filled - TOA radiation fluxes (planned to move to obs4MIPs)"
- type: "primary"
- - slug: "esmvaltool.ESACCI-CLOUD"
- description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "Cloud-Radiation Scatterplots (clt vs swcre)"
- description: "Scatterplot of total cloud fraction (clt) vs shortwave cloud radiative effect (swcre)."
- tags: ["atmosphere", "clouds", "radiation"]
-
-esmvaltool/cloud-scatterplots-clwvi-pr:
- reference_datasets:
- - slug: "obs4MIPs.GPCPv2.3"
- description: "GPCP v2.3 - Precipitation relationship"
- type: "secondary"
- - slug: "esmvaltool.ESACCI-CLOUD"
- description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- - slug: "esmvaltool.CERES-EBAF"
- description: "CERES - Reference cloud-radiation relationships"
- type: "primary"
- display_name: "Cloud-Precipitation Scatterplots (clwvi vs pr)"
- description: "Scatterplot of condensed water path (clwvi) vs precipitation (pr)."
- tags: ["atmosphere", "clouds", "precipitation"]
-
-esmvaltool/cloud-scatterplots-reference:
- reference_datasets:
- - slug: "esmvaltool.CERES-EBAF"
- description: "CERES - Reference cloud-radiation relationships"
- type: "primary"
- - slug: "esmvaltool.ESACCI-CLOUD"
- description: "ESA CCI Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "Cloud Scatterplots for Reference dataset"
- description: >-
- Reference scatterplots of two cloud-relevant variables from observational datasets.
- These provide the observational baseline for comparison with model cloud scatterplot diagnostics.
- tags: ["atmosphere", "clouds"]
-
-esmvaltool/enso-basic-climatology:
- reference_datasets:
- - slug: "obs4MIPs.GPCPv2.3"
- description: "GPCP v2.3 - Precipitation relationship"
- type: "secondary"
- - slug: "esmvaltool.TROPFLUX"
- description: "TROPFLUX - Air-sea fluxes"
- type: "secondary"
- - slug: "esmvaltool.CALIPSO-ICECLOUD"
- description: "Ice Cloud properties (planned to move to obs4MIPs)"
- type: "secondary"
- display_name: "ENSO Basic Climatology"
- description: >-
- Calculate the ENSO CLIVAR metrics for background climatology, assessing the
- mean state of the tropical Pacific upon which ENSO events develop.
- tags: ["ocean", "enso", "climatology"]
-
-esmvaltool/enso-characteristics:
- reference_datasets:
- - slug: "obs4MIPs.GPCPv2.3"
- description: "GPCP v2.3 - Precipitation relationship"
- type: "secondary"
- - slug: "esmvaltool.TROPFLUX"
- description: "TROPFLUX - Air-sea fluxes"
- type: "secondary"
- display_name: "ENSO Characteristics"
- description: >-
- Calculate the ENSO CLIVAR metrics for basic ENSO characteristics,
- evaluating the amplitude, frequency, and spatial structure of ENSO events.
- tags: ["ocean", "enso", "variability"]
-
-esmvaltool/equilibrium-climate-sensitivity:
- reference_datasets: []
- display_name: "Equilibrium Climate Sensitivity (ECS)"
- description: >-
- Calculate the global mean equilibrium climate sensitivity for a dataset.
- ECS is defined as the long-term global mean surface temperature change
- in response to a doubling of atmospheric CO2 concentration, estimated
- using the Gregory regression method from abrupt-4xCO2 experiments.
- tags: ["climate-sensitivity", "feedback"]
-
-esmvaltool/transient-climate-response:
- reference_datasets: []
- display_name: "Transient Climate Response (TCR)"
- description: >-
- Calculate the global mean transient climate response for a dataset.
- TCR is defined as the global mean surface temperature change at the time
- of CO2 doubling in a 1pctCO2 experiment (1% per year CO2 increase).
- tags: ["climate-sensitivity", "transient"]
-
-esmvaltool/transient-climate-response-emissions:
- reference_datasets: []
- display_name: "Transient Climate Response to Emissions (TCRE)"
- description: >-
- Calculate the global mean Transient Climate Response to Cumulative CO2 Emissions.
- TCRE quantifies the relationship between global warming and total cumulative
- carbon emissions, a key metric for carbon budget estimation.
- tags: ["climate-sensitivity", "carbon-cycle"]
-
-esmvaltool/zero-emission-commitment:
- reference_datasets: []
- display_name: "Zero Emission Commitment (ZEC)"
- description: >-
- Calculate the global mean Zero Emission Commitment (ZEC) temperature.
- ZEC measures the additional warming (or cooling) that occurs after all
- CO2 emissions cease, reflecting the committed warming from past emissions.
- tags: ["climate-sensitivity", "commitment"]
-
-esmvaltool/regional-historical-annual-cycle:
- reference_datasets:
- - slug: "obs4mips.ERA-5"
- description: "ERA-5 - Regional winds (uas, vas), and sea level pressure (psl)"
- type: "primary"
- display_name: "Regional Historical Annual Cycle"
- description: >-
- Plot the regional historical annual cycle of climate variables. Currently limited to
- near-surface air temperature (tas) and precipitation (pr). Regions are defined using
- IPCC AR6 reference regions as described by Iturbide et al. (2020).
- tags: ["regional", "seasonal-cycle", "historical"]
-
-esmvaltool/regional-historical-timeseries:
- reference_datasets:
- - slug: "obs4mips.ERA-5"
- description: "ERA-5 - Regional winds (uas, vas), and sea level pressure (psl)"
- type: "primary"
- display_name: "Regional Historical Timeseries"
- description: >-
- Plot the regional historical mean and anomaly timeseries of climate variables. Currently
- limited to near-surface air temperature (tas) and precipitation (pr). Regions are defined
- using IPCC AR6 reference regions as described by Iturbide et al. (2020).
- tags: ["regional", "timeseries", "historical"]
-
-esmvaltool/regional-historical-trend:
- reference_datasets:
- - slug: "obs4mips.ERA-5"
- description: "ERA-5 - Regional winds (uas, vas), and sea level pressure (psl)"
- type: "primary"
- display_name: "Regional Historical Trends"
- description: >-
- Plot the regional historical trend of climate variables. Currently limited to near-surface
- air temperature (tas) and precipitation (pr). Regions are defined using IPCC AR6 reference
- regions as described by Iturbide et al. (2020).
- tags: ["regional", "trends", "historical"]
-
-esmvaltool/sea-ice-area-basic:
- reference_datasets:
- - slug: "esmvaltool.OSI-450"
- description: "OSI-450 - Sea ice area observations"
- type: "primary"
- display_name: "Arctic and Antarctic Sea Ice Area Seasonal Cycle"
- description: >-
- Calculate the seasonal cycle and time series of Northern Hemisphere and
- Southern Hemisphere sea ice area.
- tags: ["sea-ice", "cryosphere"]
-
-esmvaltool/sea-ice-sensitivity:
- reference_datasets:
- - slug: "esmvaltool.OSI-450"
- description: "OSI-450 - Sea ice area observations"
- type: "primary"
- - slug: "esmvaltool.HadCRUT5"
- description: "HadCRUT5 - Global mean surface temperature for sensitivity regression"
- type: "primary"
- display_name: "Sea Ice Sensitivity"
- description: "Calculate sea ice sensitivity, quantifying the relationship between sea ice area and global temperature change."
- tags: ["sea-ice", "cryosphere", "sensitivity"]
-
-esmvaltool/climate-at-global-warming-levels:
- reference_datasets: []
- display_name: "Climate at Global Warming Levels"
- description: "Calculate climate variables at global warming levels (e.g. 1.5C, 2C, 3C, 4C above pre-industrial)."
- tags: ["scenarios", "warming-levels"]
-
-esmvaltool/climate-drivers-for-fire:
- reference_datasets: []
- display_name: "Climate Drivers for Fire"
- description: "Calculate diagnostics regarding climate drivers for fire, including temperature, precipitation, and humidity conditions that influence fire risk."
- tags: ["fire", "climate-drivers", "impacts"]
-
-# ============================================================================
-# ILAMB DIAGNOSTICS (Land)
-# ============================================================================
-
-ilamb/gpp-wecann:
- reference_datasets:
- - slug: "obs4REF.WECANN"
- description: "WECANN GPP dataset - Primary gross primary productivity reference"
- type: "primary"
- - slug: "obs4MIPs.GPCPv2.3"
- description: "GPCP v2.3 - Precipitation relationship"
- type: "secondary"
- - slug: "obs4MIPs.CRU4.02"
- description: "CRU TS v4.02 - Temperature relationship"
- type: "secondary"
- display_name: "Gross Primary Productivity (WECANN)"
- description: "Apply the standard ILAMB analysis of gross primary productivity (gpp) with respect to the WECANN reference dataset."
- tags: ["land", "carbon-cycle", "vegetation"]
-
-ilamb/gpp-fluxnet2015:
- reference_datasets:
- - slug: "obs4REF.FLUXNET2015"
- description: "FLUXNET2015 - Tower-based GPP measurements"
- type: "primary"
- display_name: "Gross Primary Productivity (FLUXNET2015)"
- description: "Apply the standard ILAMB analysis of gross primary productivity (gpp) with respect to the FLUXNET2015 tower-based reference dataset."
- tags: ["land", "carbon-cycle", "vegetation", "flux-towers"]
-
-ilamb/mrro-lora:
- reference_datasets:
- - slug: "obs4REF.LORA"
- description: "LORA dataset - Runoff observations"
- type: "primary"
- display_name: "Runoff (LORA)"
- description: "Apply the standard ILAMB analysis of total runoff (mrro) with respect to the LORA reference dataset."
- tags: ["land", "hydrology", "runoff"]
-
-ilamb/mrsos-wangmao:
- reference_datasets:
- - slug: "obs4REF.WangMao"
- description: "Wang-Mao dataset - Surface soil moisture"
- type: "primary"
- display_name: "Surface Soil Moisture (WangMao)"
- description: "Apply the standard ILAMB analysis of moisture in upper portion of soil column (mrsos) with respect to the WangMao reference dataset."
- tags: ["land", "hydrology", "soil-moisture"]
-
-ilamb/csoil-hwsd2:
- reference_datasets:
- - slug: "obs4REF.HWSD2"
- description: "Harmonized World Soil Database v2 - Soil carbon content"
- type: "primary"
- display_name: "Soil Carbon (HWSD2)"
- description: "Apply the standard ILAMB analysis of carbon mass in soil pool (cSoil) with respect to the HWSD2 reference dataset."
- tags: ["land", "carbon-cycle", "soil"]
-
-ilamb/lai-avh15c1:
- reference_datasets:
- - slug: "obs4REF.AVH15C1"
- description: "AVHRR - Leaf area index observations"
- type: "primary"
- display_name: "Leaf Area Index (AVH15C1)"
- description: "Apply the standard ILAMB analysis of leaf area index (lai) with respect to the AVH15C1 (AVHRR) reference dataset."
- tags: ["land", "vegetation", "remote-sensing"]
-
-ilamb/nbp-hoffman:
- reference_datasets:
- - slug: "obs4REF.Hoffman"
- description: "Hoffman dataset - Net biome productivity"
- type: "primary"
- display_name: "Net Biome Productivity (Hoffman)"
- description: "Apply the standard ILAMB analysis of net biome productivity (nbp) with respect to the Hoffman reference dataset."
- tags: ["land", "carbon-cycle", "net-flux"]
-
-ilamb/snc-esacci:
- reference_datasets:
- - slug: "obs4REF.CCI-CryoClim-FSC"
- description: "ESA CCI Snow - Snow cover fraction"
- type: "primary"
- display_name: "Snow Cover (ESA CCI)"
- description: "Apply the standard ILAMB analysis of snow cover fraction (snc) with respect to the ESA CCI CryoClim fractional snow cover reference dataset."
- tags: ["land", "cryosphere", "snow"]
-
-ilamb/burntfractionall-gfed:
- reference_datasets:
- - slug: "obs4REF.GFED"
- description: "Global Fire Emissions Database - Burnt area fraction"
- type: "primary"
- display_name: "Burnt Fraction (GFED)"
- description: "Apply the standard ILAMB analysis of burnt area fraction (burntFractionAll) with respect to the GFED reference dataset."
- tags: ["land", "fire", "disturbance"]
-
-# ============================================================================
-# ILAMB/IOMB DIAGNOSTICS (Ocean)
-# ============================================================================
-
-ilamb/thetao-woa2023-surface:
- reference_datasets:
- - slug: "obs4REF.WOA2023"
- description: "World Ocean Atlas 2023 - Sea water potential temperature (surface)"
- type: "primary"
- display_name: "Sea Water Temperature (WOA2023 Surface)"
- description: "Apply the standard ILAMB analysis of sea water potential temperature (thetao) at the surface with respect to the WOA2023 reference dataset."
- tags: ["ocean", "temperature", "surface"]
-
-ilamb/so-woa2023-surface:
- reference_datasets:
- - slug: "obs4REF.WOA2023"
- description: "World Ocean Atlas 2023 - Sea water salinity (surface)"
- type: "primary"
- display_name: "Sea Water Salinity (WOA2023 Surface)"
- description: "Apply the standard ILAMB analysis of sea water salinity (so) at the surface with respect to the WOA2023 reference dataset."
- tags: ["ocean", "salinity", "surface"]
-
-ilamb/amoc-rapid:
- reference_datasets:
- - slug: "obs4REF.RAPID"
- description: "RAPID array - Atlantic Meridional Overturning Circulation observations"
- type: "primary"
- display_name: "Atlantic Meridional Overturning Circulation (RAPID)"
- description: "Apply the standard ILAMB analysis of the Atlantic Meridional Overturning Circulation (AMOC) with respect to the RAPID array reference dataset."
- tags: ["ocean", "circulation", "amoc"]
diff --git a/backend/static/diagnostics/pmp.yaml b/backend/static/diagnostics/pmp.yaml
new file mode 100644
index 0000000..0af9401
--- /dev/null
+++ b/backend/static/diagnostics/pmp.yaml
@@ -0,0 +1,168 @@
+# PMP Diagnostic Metadata
+#
+# Reference datasets and display metadata for PMP (PCMDI Metrics Package) diagnostics.
+
+pmp/annual-cycle:
+ reference_datasets:
+ - slug: "obs4MIPs-climatology.ERA-5"
+ description: "ERA5 Reanalysis - Primary climatology for surface temperature (ts), winds (uas, vas), sea level pressure (psl), air temperature (ta), zonal/meridional wind (ua, va), and geopotential height (zg). Period: 1981-2004."
+ type: "primary"
+ - slug: "obs4MIPs-climatology.GPCP-Monthly-3-2"
+ description: "Global Precipitation Climatology Project v3.2 - Precipitation reference (pr). Period: 1983-2004."
+ type: "primary"
+ - slug: "obs4MIPs-climatology.CERES-EBAF-4-2"
+ description: "CERES Energy Balanced and Filled v4.2 - Radiation fields (rlds, rlus, rlut, rsds, rsdt, rsus, rsut) and clear-sky variants (rldscs, rsdscs, rlutcs, rsutcs, rsuscs, rltcre, rstcre, rt). Period: 2001-2004."
+ type: "primary"
+ display_name: "Annual Cycle Analysis"
+ description: >-
+ Calculate the annual cycle for a dataset. Variables provided include surface temperature (ts),
+ precipitation (pr), winds (uas, vas), sea level pressure (psl), air temperature (ta),
+ zonal/meridional wind (ua, va), geopotential height (zg), and radiation fields
+ (rlds, rlus, rsds, rsdt, rsus, rlut, rsut) including clear-sky variants.
+ tags: ["atmosphere", "seasonal-cycle", "climatology"]
+
+pmp/enso_perf:
+ reference_datasets:
+ - slug: "obs4mips.GPCP-Monthly-3-2"
+ description: "Global Precipitation Climatology Project v3.2 - Precipitation (pr)"
+ type: "primary"
+ - slug: "obs4mips.TropFlux-1-0"
+ description: "TropFlux v1.0 - Wind stress (tauu)"
+ type: "primary"
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and Sea Surface Temperature v1.1 - Sea surface temperature (ts)"
+ type: "primary"
+ display_name: "ENSO Performance Metrics"
+ description: >-
+ Calculate the ENSO performance metrics for a dataset. These metrics evaluate
+ how well a model simulates key aspects of El Nino-Southern Oscillation variability.
+ tags: ["ocean", "atmosphere", "enso", "variability"]
+
+pmp/enso_tel:
+ reference_datasets:
+ - slug: "obs4mips.GPCP-Monthly-3-2"
+ description: "Global Precipitation Climatology Project v3.2 - Precipitation teleconnections (pr)"
+ type: "primary"
+ - slug: "obs4mips.TropFlux-1-0"
+ description: "TropFlux v1.0 - Wind stress (tauu)"
+ type: "primary"
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and SST v1.1 - Sea surface temperature teleconnections (ts)"
+ type: "primary"
+ display_name: "ENSO Teleconnections"
+ description: >-
+ Calculate the ENSO teleconnection metrics for a dataset. Teleconnection metrics
+ assess how well a model captures the remote climate impacts of ENSO events
+ on temperature and precipitation patterns worldwide.
+ tags: ["ocean", "atmosphere", "enso", "teleconnections"]
+
+pmp/enso_proc:
+ reference_datasets:
+ - slug: "obs4mips.GPCP-Monthly-3-2"
+ description: "Global Precipitation Climatology Project v3.2 - Precipitation (pr)"
+ type: "primary"
+ - slug: "obs4mips.TropFlux-1-0"
+ description: "TropFlux v1.0 - Wind stress (tauu)"
+ type: "primary"
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and SST v1.1 - Sea surface temperature (ts)"
+ type: "primary"
+ - slug: "obs4mips.CERES-EBAF-4-2"
+ description: "CERES Energy Balanced and Filled v4.2 - Surface and TOA radiation fluxes (hfls, hfss, rlds, rlus, rsds, rsus)"
+ type: "primary"
+ display_name: "ENSO Processes"
+ description: >-
+ Calculate the ENSO process-oriented metrics for a dataset. These metrics evaluate
+ the physical processes that drive ENSO, including ocean-atmosphere coupling,
+ thermocline feedbacks, and energy balance.
+ tags: ["ocean", "atmosphere", "enso", "energy-balance"]
+
+# Extratropical Modes of Variability (Sea Surface Temperature modes)
+pmp/extratropical-modes-of-variability-pdo:
+ reference_datasets:
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and SST v1.1 - SST for Pacific Decadal Oscillation"
+ type: "primary"
+ display_name: "Pacific Decadal Oscillation (PDO)"
+ description: >-
+ Calculate the extratropical modes of variability for the Pacific Decadal Oscillation (PDO),
+ a pattern of Pacific climate variability characterized by SST anomalies in the North Pacific.
+ tags: ["ocean", "variability", "pacific", "decadal"]
+
+pmp/extratropical-modes-of-variability-npgo:
+ reference_datasets:
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and SST v1.1 - SST for North Pacific Gyre Oscillation"
+ type: "primary"
+ display_name: "North Pacific Gyre Oscillation (NPGO)"
+ description: >-
+ Calculate the extratropical modes of variability for the North Pacific Gyre Oscillation (NPGO),
+ which tracks changes in the strength of the North Pacific subtropical and subpolar gyres.
+ tags: ["ocean", "variability", "pacific"]
+
+pmp/extratropical-modes-of-variability-amo:
+ reference_datasets:
+ - slug: "obs4mips.HadISST-1-1"
+ description: "Hadley Centre Sea Ice and SST v1.1 - SST for Atlantic Multidecadal Oscillation"
+ type: "primary"
+ display_name: "Atlantic Multidecadal Oscillation (AMO)"
+ description: >-
+ Calculate the extratropical modes of variability for the Atlantic Multidecadal Oscillation (AMO),
+ a coherent pattern of variability in North Atlantic SSTs with a period of 60-80 years.
+ tags: ["ocean", "variability", "atlantic", "decadal"]
+
+# Extratropical Modes of Variability (Sea Level Pressure modes)
+pmp/extratropical-modes-of-variability-nao:
+ reference_datasets:
+ - slug: "obs4mips.20CR"
+ description: "20th Century Reanalysis - Sea level pressure for North Atlantic Oscillation"
+ type: "primary"
+ display_name: "North Atlantic Oscillation (NAO)"
+ description: >-
+ Calculate the extratropical modes of variability for the North Atlantic Oscillation (NAO),
+ a large-scale pattern of sea level pressure variability between the Icelandic Low and Azores High.
+ tags: ["atmosphere", "variability", "atlantic", "pressure"]
+
+pmp/extratropical-modes-of-variability-nam:
+ reference_datasets:
+ - slug: "obs4mips.20CR"
+ description: "20th Century Reanalysis - Sea level pressure for Northern Annular Mode"
+ type: "primary"
+ display_name: "Northern Annular Mode (NAM)"
+ description: >-
+ Calculate the extratropical modes of variability for the Northern Annular Mode (NAM),
+ the dominant mode of sea level pressure variability in the Northern Hemisphere extratropics.
+ tags: ["atmosphere", "variability", "hemispheric", "pressure"]
+
+pmp/extratropical-modes-of-variability-pna:
+ reference_datasets:
+ - slug: "obs4mips.20CR"
+ description: "20th Century Reanalysis - Sea level pressure for Pacific North American Pattern"
+ type: "primary"
+ display_name: "Pacific North American Pattern (PNA)"
+ description: >-
+ Calculate the extratropical modes of variability for the Pacific North American (PNA) pattern,
+ a prominent mode of atmospheric variability influencing weather over the Pacific and North America.
+ tags: ["atmosphere", "variability", "pacific", "pressure"]
+
+pmp/extratropical-modes-of-variability-npo:
+ reference_datasets:
+ - slug: "obs4mips.20CR"
+ description: "20th Century Reanalysis - Sea level pressure for North Pacific Oscillation"
+ type: "primary"
+ display_name: "North Pacific Oscillation (NPO)"
+ description: >-
+ Calculate the extratropical modes of variability for the North Pacific Oscillation (NPO),
+ defined by sea level pressure variability over the North Pacific.
+ tags: ["atmosphere", "variability", "pacific", "pressure"]
+
+pmp/extratropical-modes-of-variability-sam:
+ reference_datasets:
+ - slug: "obs4mips.20CR"
+ description: "20th Century Reanalysis - Sea level pressure for Southern Annular Mode"
+ type: "primary"
+ display_name: "Southern Annular Mode (SAM)"
+ description: >-
+ Calculate the extratropical modes of variability for the Southern Annular Mode (SAM),
+ the dominant mode of sea level pressure variability in the Southern Hemisphere extratropics.
+ tags: ["atmosphere", "variability", "southern-hemisphere", "pressure"]
diff --git a/backend/tests/test_api/test_api_explorer.py b/backend/tests/test_api/test_api_explorer.py
new file mode 100644
index 0000000..e6bc981
--- /dev/null
+++ b/backend/tests/test_api/test_api_explorer.py
@@ -0,0 +1,255 @@
+"""Tests for the /api/v1/explorer/* endpoints."""
+
+from pathlib import Path
+
+import pytest
+import yaml
+from fastapi.testclient import TestClient
+
+from ref_backend.core.collections import load_all_collections, load_theme_mapping
+
+
+@pytest.fixture(autouse=True)
+def clear_collection_caches():
+ """Clear collection lru_caches before and after each test."""
+ load_all_collections.cache_clear()
+ load_theme_mapping.cache_clear()
+ yield
+ load_all_collections.cache_clear()
+ load_theme_mapping.cache_clear()
+
+
+def _write_yaml(path: Path, data) -> None:
+ with open(path, "w") as f:
+ yaml.dump(data, f)
+
+
+@pytest.fixture
+def collections_dir(tmp_path: Path, monkeypatch):
+ """Create a temp collections directory with fixture YAML files."""
+ cols_dir = tmp_path / "collections"
+ cols_dir.mkdir()
+
+ # Collection 1.2 — atmosphere collection with y_min/y_max
+ _write_yaml(
+ cols_dir / "1.2.yaml",
+ {
+ "id": "1.2",
+ "name": "Atmosphere Collection",
+ "theme": "Atmosphere",
+ "endorser": "WCRP",
+ "diagnostics": [{"provider_slug": "pmp", "diagnostic_slug": "mean-climate"}],
+ "explorer_cards": [
+ {
+ "title": "Temperature Bias",
+ "content": [
+ {
+ "type": "box-whisker-chart",
+ "provider": "pmp",
+ "diagnostic": "mean-climate",
+ "title": "Temperature Bias Chart",
+ "y_min": -5.0,
+ "y_max": 5.0,
+ "grouping_config": {"group_by": "model", "hue": "experiment"},
+ }
+ ],
+ }
+ ],
+ },
+ )
+
+ # Collection for sea ice with other_filters
+ _write_yaml(
+ cols_dir / "2.1.yaml",
+ {
+ "id": "2.1",
+ "name": "Sea Ice Collection",
+ "theme": "Ocean",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "Sea Ice Extent",
+ "content": [
+ {
+ "type": "series-chart",
+ "provider": "pmp",
+ "diagnostic": "sea-ice",
+ "title": "Sea Ice Series",
+ "other_filters": {
+ "isolate_ids": "CMIP6.ssp585.NH",
+ "exclude_ids": "CMIP6.piControl",
+ },
+ "grouping_config": {"group_by": "model", "hue": "experiment"},
+ }
+ ],
+ }
+ ],
+ },
+ )
+
+ # ENSO collection with placeholder card (empty content)
+ _write_yaml(
+ cols_dir / "3.1.yaml",
+ {
+ "id": "3.1",
+ "name": "ENSO Collection",
+ "theme": "Earth System",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "ENSO Placeholder",
+ "placeholder": True,
+ "content": [],
+ }
+ ],
+ },
+ )
+
+ # themes.yaml
+ _write_yaml(
+ cols_dir / "themes.yaml",
+ {
+ "ocean": {
+ "title": "Ocean Theme",
+ "description": "Ocean diagnostics",
+ "collections": ["2.1"],
+ },
+ "earth-system": {
+ "title": "Earth System Theme",
+ "collections": ["1.2", "3.1"],
+ },
+ },
+ )
+
+ monkeypatch.setattr(
+ "ref_backend.core.collections.get_collections_dir",
+ lambda: cols_dir,
+ )
+ monkeypatch.setattr(
+ "ref_backend.core.collections.get_themes_path",
+ lambda: cols_dir / "themes.yaml",
+ )
+
+ return cols_dir
+
+
+def test_list_collections_returns_all_ids(client: TestClient, settings, collections_dir):
+ """GET /explorer/collections/ returns all collection IDs."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/")
+ assert r.status_code == 200
+ data = r.json()
+ ids = [c["id"] for c in data]
+ assert "1.2" in ids
+ assert "2.1" in ids
+ assert "3.1" in ids
+
+
+def test_get_collection_12_returns_correct_cards(client: TestClient, settings, collections_dir):
+ """GET /explorer/collections/1.2 returns the atmosphere collection with y_min/y_max."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/1.2")
+ assert r.status_code == 200
+ data = r.json()
+ assert data["id"] == "1.2"
+ assert data["name"] == "Atmosphere Collection"
+ assert len(data["explorer_cards"]) == 1
+ content = data["explorer_cards"][0]["content"][0]
+ assert content["y_min"] == -5.0
+ assert content["y_max"] == 5.0
+
+
+def test_get_collection_nonexistent_returns_404(client: TestClient, settings, collections_dir):
+ """GET /explorer/collections/nonexistent returns 404."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/nonexistent")
+ assert r.status_code == 404
+
+
+def test_list_themes_returns_expected_slugs(client: TestClient, settings, collections_dir):
+ """GET /explorer/themes/ returns expected theme slugs."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/themes/")
+ assert r.status_code == 200
+ data = r.json()
+ slugs = [t["slug"] for t in data]
+ assert "ocean" in slugs
+ assert "earth-system" in slugs
+
+
+def test_get_theme_ocean_returns_aggregated_cards(client: TestClient, settings, collections_dir):
+ """GET /explorer/themes/ocean returns theme with aggregated sea-ice cards."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/themes/ocean")
+ assert r.status_code == 200
+ data = r.json()
+ assert data["slug"] == "ocean"
+ assert len(data["explorer_cards"]) == 1
+ card = data["explorer_cards"][0]
+ assert card["title"] == "Sea Ice Extent"
+
+
+def test_get_theme_nonexistent_returns_404(client: TestClient, settings, collections_dir):
+ """GET /explorer/themes/nonexistent returns 404."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/themes/nonexistent")
+ assert r.status_code == 404
+
+
+def test_response_field_names_are_snake_case(client: TestClient, settings, collections_dir):
+ """Response fields use snake_case, not camelCase."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/1.2")
+ assert r.status_code == 200
+ data = r.json()
+
+ card_content = data["explorer_cards"][0]["content"][0]
+
+ # These snake_case keys must be present
+ assert "y_min" in card_content
+ assert "y_max" in card_content
+ assert "grouping_config" in card_content
+ assert "group_by" in card_content["grouping_config"]
+
+ # No camelCase variants allowed
+ for key in ["yMin", "yMax", "groupingConfig", "groupBy", "showZeroLine"]:
+ assert key not in card_content
+ assert key not in card_content.get("grouping_config", {})
+
+ # Top-level collection fields
+ assert "explorer_cards" in data
+ assert "explorerCards" not in data
+
+
+def test_enso_placeholder_card_in_earth_system_theme(client: TestClient, settings, collections_dir):
+ """ENSO placeholder card appears in earth-system theme with empty content list."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/themes/earth-system")
+ assert r.status_code == 200
+ data = r.json()
+
+ all_cards = data["explorer_cards"]
+ enso_cards = [c for c in all_cards if c.get("placeholder") is True]
+ assert len(enso_cards) >= 1
+
+ enso_card = enso_cards[0]
+ assert enso_card["title"] == "ENSO Placeholder"
+ assert enso_card["content"] == []
+
+
+def test_sea_ice_cards_include_other_filters(client: TestClient, settings, collections_dir):
+ """Sea ice card content includes isolate_ids and exclude_ids in other_filters."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/2.1")
+ assert r.status_code == 200
+ data = r.json()
+
+ content = data["explorer_cards"][0]["content"][0]
+ assert "other_filters" in content
+ filters = content["other_filters"]
+ assert "isolate_ids" in filters
+ assert "exclude_ids" in filters
+ assert filters["isolate_ids"] == "CMIP6.ssp585.NH"
+ assert filters["exclude_ids"] == "CMIP6.piControl"
+
+
+def test_atmosphere_cards_include_y_min_y_max(client: TestClient, settings, collections_dir):
+ """Atmosphere collection cards include y_min and y_max values."""
+ r = client.get(f"{settings.API_V1_STR}/explorer/collections/1.2")
+ assert r.status_code == 200
+ data = r.json()
+
+ content = data["explorer_cards"][0]["content"][0]
+ assert content["y_min"] == -5.0
+ assert content["y_max"] == 5.0
diff --git a/backend/tests/test_api/test_routes/test_aft.py b/backend/tests/test_api/test_routes/test_aft.py
index b2c1271..46d779b 100644
--- a/backend/tests/test_api/test_routes/test_aft.py
+++ b/backend/tests/test_api/test_routes/test_aft.py
@@ -2,68 +2,93 @@
from fastapi.testclient import TestClient
from ref_backend.core.aft import (
+ _build_ref_to_aft_index,
+ get_aft_diagnostic_by_id,
+ get_aft_diagnostics_index,
load_official_aft_diagnostics,
- load_ref_mapping,
+)
+from ref_backend.core.collections import (
+ AFTCollectionContent,
+ AFTCollectionDetail,
+ AFTCollectionDiagnosticLink,
+ load_all_collections,
)
-@pytest.fixture
-def clear_aft_caches():
- """Clear AFT module caches before each test."""
- load_official_aft_diagnostics.cache_clear()
- load_ref_mapping.cache_clear()
+@pytest.fixture(autouse=True)
+def clear_caches():
+ """Clear all caches before and after each test."""
+ for fn in [
+ load_official_aft_diagnostics,
+ get_aft_diagnostics_index,
+ get_aft_diagnostic_by_id,
+ _build_ref_to_aft_index,
+ ]:
+ fn.cache_clear()
+ load_all_collections.cache_clear()
yield
- load_official_aft_diagnostics.cache_clear()
- load_ref_mapping.cache_clear()
-
+ for fn in [
+ load_official_aft_diagnostics,
+ get_aft_diagnostics_index,
+ get_aft_diagnostic_by_id,
+ _build_ref_to_aft_index,
+ ]:
+ fn.cache_clear()
+ load_all_collections.cache_clear()
+
+
+def _make_collection(
+ id: str,
+ name: str = "Test Diagnostic",
+ diagnostics: list[dict] | None = None,
+) -> AFTCollectionDetail:
+ diag_links = [AFTCollectionDiagnosticLink(**d) for d in (diagnostics or [])]
+ return AFTCollectionDetail(
+ id=id,
+ name=name,
+ theme="Climate",
+ endorser="Test Endorser",
+ version_control="1.0",
+ reference_dataset="ERA5",
+ provider_link="https://example.com",
+ content=AFTCollectionContent(
+ description="Test description",
+ short_description="Short desc",
+ ),
+ diagnostics=diag_links,
+ explorer_cards=[],
+ )
-def test_aft_diagnostics_empty_csv(client: TestClient, settings, clear_aft_caches, tmp_path, monkeypatch):
- """Test that GET /api/cmip7-aft-diagnostics returns [] when CSV has only headers."""
- # Create empty CSV with headers
- csv_content = "id,name,theme,version_control,reference_dataset,endorser,provider_link,description,short_description\n" # noqa
- csv_file = tmp_path / "official_diagnostics.csv"
- csv_file.write_text(csv_content)
- # Create empty YAML
- yaml_content = "# Empty mapping\n"
- yaml_file = tmp_path / "ref_mapping.yaml"
- yaml_file.write_text(yaml_content)
+@pytest.fixture
+def empty_collections(monkeypatch):
+ """Patch load_all_collections to return empty dict."""
+ monkeypatch.setattr("ref_backend.core.aft.load_all_collections", lambda: {})
- monkeypatch.setattr("ref_backend.core.aft.get_aft_paths", lambda: (csv_file, yaml_file))
+@pytest.fixture
+def sample_collections(monkeypatch):
+ """Patch load_all_collections to return sample data."""
+ collections = {
+ "AFT-001": _make_collection(
+ "AFT-001",
+ name="Test Diagnostic 1",
+ diagnostics=[{"provider_slug": "example", "diagnostic_slug": "global-mean-timeseries"}],
+ ),
+ "AFT-002": _make_collection("AFT-002", name="Test Diagnostic 2"),
+ }
+ monkeypatch.setattr("ref_backend.core.aft.load_all_collections", lambda: collections)
+
+
+def test_aft_diagnostics_empty(client: TestClient, settings, empty_collections):
+ """Test that GET /api/cmip7-aft-diagnostics returns [] when no collections exist."""
r = client.get(f"{settings.API_V1_STR}/cmip7-aft-diagnostics")
assert r.status_code == 200
- data = r.json()
- assert data == []
+ assert r.json() == []
-@pytest.fixture
-def aft_test_data(tmp_path, monkeypatch, clear_aft_caches):
- """Create test CSV and YAML files with sample data."""
- # CSV with 2 entries
- csv_content = """id,name,theme,version_control,reference_dataset,endorser,provider_link,description,short_description
-AFT-001,Test Diagnostic 1,Climate,1.0,CMIP6,Test Endorser,https://example.com,Test description,Short desc
-AFT-002,Test Diagnostic 2,Ocean,2.0,CMIP7,Test Endorser 2,https://example2.com,Test description 2,Short desc 2
-""" # noqa: E501
- csv_file = tmp_path / "official_diagnostics.csv"
- csv_file.write_text(csv_content)
-
- # YAML mapping one AFT ID to example/global-mean-timeseries
- yaml_content = """AFT-001:
- - provider_slug: example
- diagnostic_slug: global-mean-timeseries
-"""
- yaml_file = tmp_path / "ref_mapping.yaml"
- yaml_file.write_text(yaml_content)
-
- # Monkeypatch the paths
- monkeypatch.setattr("ref_backend.core.aft.get_aft_paths", lambda: (csv_file, yaml_file))
-
- return tmp_path
-
-
-def test_aft_diagnostics_with_data(client: TestClient, settings, aft_test_data):
- """Test AFT diagnostics with sample data."""
+def test_aft_diagnostics_with_data(client: TestClient, settings, sample_collections):
+ """Test AFT diagnostics list with sample data."""
r = client.get(f"{settings.API_V1_STR}/cmip7-aft-diagnostics")
assert r.status_code == 200
data = r.json()
@@ -72,7 +97,7 @@ def test_aft_diagnostics_with_data(client: TestClient, settings, aft_test_data):
assert data[1]["id"] == "AFT-002"
-def test_aft_diagnostic_detail(client: TestClient, settings, aft_test_data):
+def test_aft_diagnostic_detail(client: TestClient, settings, sample_collections):
"""Test getting AFT diagnostic detail."""
r = client.get(f"{settings.API_V1_STR}/cmip7-aft-diagnostics/AFT-001")
assert r.status_code == 200
@@ -84,43 +109,7 @@ def test_aft_diagnostic_detail(client: TestClient, settings, aft_test_data):
assert data["diagnostics"][0]["diagnostic_slug"] == "global-mean-timeseries"
-def test_aft_diagnostic_detail_404(client: TestClient, settings, aft_test_data):
+def test_aft_diagnostic_detail_404(client: TestClient, settings, sample_collections):
"""Test 404 for unknown AFT ID."""
r = client.get(f"{settings.API_V1_STR}/cmip7-aft-diagnostics/AFT-999")
assert r.status_code == 404
-
-
-@pytest.mark.xfail(reason="Need better test data")
-def test_diagnostic_detail_aft_augmentation(client: TestClient, settings, aft_test_data):
- """Test that diagnostic detail includes AFT summaries when mapping exists."""
- # Test diagnostic with mapping
- r = client.get(f"{settings.API_V1_STR}/diagnostics/example/global-mean-timeseries")
- assert r.status_code == 200
- data = r.json()
- assert "aft" in data
- data = r.json()
- assert "aft" in data
- assert len(data["aft"]) == 1
- assert data["aft"][0]["id"] == "AFT-001"
-
- # Test diagnostic without mapping (assuming another diagnostic exists)
- # Get list of diagnostics first
- r_list = client.get(f"{settings.API_V1_STR}/diagnostics/")
- assert r_list.status_code == 200
- diagnostics = r_list.json()["data"]
-
- # Find one that doesn't match our mapping
- unmapped_diagnostic = None
- for diag in diagnostics:
- if not (diag["provider"]["slug"] == "example" and diag["slug"] == "global-mean-timeseries"):
- unmapped_diagnostic = diag
- break
-
- if unmapped_diagnostic:
- r_unmapped = client.get(
- f"{settings.API_V1_STR}/diagnostics/{unmapped_diagnostic['provider']['slug']}/{unmapped_diagnostic['slug']}"
- )
- assert r_unmapped.status_code == 200
- data_unmapped = r_unmapped.json()
- assert "aft" in data_unmapped
- assert data_unmapped["aft"] == []
diff --git a/backend/tests/test_core/test_core_aft.py b/backend/tests/test_core/test_core_aft.py
index 633a963..b944156 100644
--- a/backend/tests/test_core/test_core_aft.py
+++ b/backend/tests/test_core/test_core_aft.py
@@ -1,49 +1,53 @@
-"""Tests for AFT diagnostic loading and mapping functionality."""
+"""Tests for AFT diagnostic functions backed by collection YAML files."""
-import csv
-import io
-from pathlib import Path
from unittest.mock import patch
import pytest
-import yaml
from ref_backend.core.aft import (
+ _build_ref_to_aft_index,
get_aft_diagnostic_by_id,
get_aft_diagnostics_index,
get_aft_for_ref_diagnostic,
- get_aft_paths,
load_official_aft_diagnostics,
- load_ref_mapping,
+)
+from ref_backend.core.collections import (
+ AFTCollectionContent,
+ AFTCollectionDetail,
+ AFTCollectionDiagnosticLink,
)
from ref_backend.models import (
AFTDiagnosticBase,
AFTDiagnosticDetail,
AFTDiagnosticSummary,
- RefDiagnosticLink,
)
-AFT_CSV_HEADERS = [
- "id",
- "name",
- "theme",
- "version_control",
- "reference_dataset",
- "endorser",
- "provider_link",
- "description",
- "short_description",
-]
-
-
-def _build_aft_csv(rows: list[list[str]]) -> str:
- """Build a CSV string with standard AFT headers."""
- output = io.StringIO()
- writer = csv.writer(output)
- writer.writerow(AFT_CSV_HEADERS)
- for row in rows:
- writer.writerow(row)
- return output.getvalue()
+
+def _make_collection( # noqa: PLR0913
+ id: str,
+ name: str = "Test Collection",
+ theme: str = "Climate",
+ description: str = "Full description",
+ short_description: str = "Short",
+ diagnostics: list[dict] | None = None,
+) -> AFTCollectionDetail:
+ """Build a minimal AFTCollectionDetail for testing."""
+ diag_links = [AFTCollectionDiagnosticLink(**d) for d in (diagnostics or [])]
+ return AFTCollectionDetail(
+ id=id,
+ name=name,
+ theme=theme,
+ endorser="WCRP",
+ version_control="v1",
+ reference_dataset="ERA5",
+ provider_link="https://example.com",
+ content=AFTCollectionContent(
+ description=description,
+ short_description=short_description,
+ ),
+ diagnostics=diag_links,
+ explorer_cards=[],
+ )
@pytest.fixture(autouse=True)
@@ -52,572 +56,197 @@ def clear_aft_caches():
yield
for fn in [
load_official_aft_diagnostics,
- load_ref_mapping,
get_aft_diagnostics_index,
get_aft_diagnostic_by_id,
get_aft_for_ref_diagnostic,
+ _build_ref_to_aft_index,
]:
if hasattr(fn, "cache_clear"):
fn.cache_clear()
-class TestGetAFTPaths:
- """Test the get_aft_paths function."""
-
- def test_returns_correct_paths(self):
- """Test that get_aft_paths returns Path objects."""
- csv_path, yaml_path = get_aft_paths()
-
- assert isinstance(csv_path, Path)
- assert isinstance(yaml_path, Path)
- assert csv_path.name == "AFT REF Diagnostics-v2_draft_clean.csv"
- assert yaml_path.name == "ref_mapping.yaml"
- assert csv_path.parent.name == "aft"
- assert yaml_path.parent.name == "aft"
+def _patch_collections(collections: dict[str, AFTCollectionDetail]):
+ """Patch load_all_collections to return the given dict."""
+ return patch("ref_backend.core.aft.load_all_collections", return_value=collections)
class TestLoadOfficialAFTDiagnostics:
"""Test the load_official_aft_diagnostics function."""
- def test_valid_csv_parsing(self, tmp_path: Path):
- """Test that a valid CSV is parsed correctly."""
- csv_content = _build_aft_csv(
- [
- [
- "AFT-001",
- "Test Diagnostic 1",
- "Climate",
- "https://github.com/test/repo",
- "ERA5",
- "WCRP",
- "https://example.com",
- "Full description",
- "Short",
- ],
- [
- "AFT-002",
- "Test Diagnostic 2",
- "Ocean",
- "https://gitlab.com/test",
- "WOA",
- "PCMDI",
- "https://example.org",
- "Another description",
- "Brief",
- ],
- ]
- )
+ def test_returns_diagnostics_from_collections(self):
+ """Test that collections are converted to AFTDiagnosticBase."""
+ col1 = _make_collection("1.1", name="Sea Ice", description="Sea ice desc")
+ col2 = _make_collection("2.1", name="Soil Carbon", description="Soil desc")
- csv_path = tmp_path / "test_aft.csv"
- csv_path.write_text(csv_content)
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
+ with _patch_collections({"1.1": col1, "2.1": col2}):
diagnostics = load_official_aft_diagnostics()
assert len(diagnostics) == 2
- diag1 = diagnostics[0]
- assert isinstance(diag1, AFTDiagnosticBase)
- assert diag1.id == "AFT-001"
- assert diag1.name == "Test Diagnostic 1"
- assert diag1.theme == "Climate"
- assert diag1.reference_dataset == "ERA5"
- assert diag1.endorser == "WCRP"
- assert diag1.description == "Full description"
- assert diag1.short_description == "Short"
-
- diag2 = diagnostics[1]
- assert diag2.id == "AFT-002"
- assert diag2.name == "Test Diagnostic 2"
-
- def test_duplicate_id_detection(self, tmp_path: Path):
- """Test that duplicate IDs in CSV raise a ValueError."""
- csv_content = _build_aft_csv(
- [
- [
- "AFT-001",
- "Test 1",
- "Climate",
- "https://github.com",
- "ERA5",
- "WCRP",
- "https://example.com",
- "Desc",
- "Short",
- ],
- [
- "AFT-001",
- "Duplicate",
- "Ocean",
- "https://gitlab.com",
- "WOA",
- "PCMDI",
- "https://example.org",
- "Desc",
- "Short",
- ],
- ]
- )
-
- csv_path = tmp_path / "duplicate.csv"
- csv_path.write_text(csv_content)
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
- with pytest.raises(
- ValueError,
- match="Duplicate 'id' values found in AFT CSV",
- ):
- load_official_aft_diagnostics()
-
- def test_missing_csv_file(self, tmp_path: Path):
- """Test that missing CSV file raises ValueError."""
- nonexistent_csv = tmp_path / "nonexistent.csv"
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(nonexistent_csv, tmp_path / "dummy.yaml"),
- ):
- with pytest.raises(ValueError, match="AFT CSV file not found"):
- load_official_aft_diagnostics()
-
- def test_invalid_csv_headers(self, tmp_path: Path):
- """Test that CSV with wrong headers raises ValueError."""
- csv_content = "id,wrong_header,theme\nAFT-001,Test,Climate\n"
-
- csv_path = tmp_path / "invalid_headers.csv"
- csv_path.write_text(csv_content)
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
- with pytest.raises(ValueError, match="CSV headers mismatch"):
- load_official_aft_diagnostics()
-
- def test_whitespace_stripped(self, tmp_path: Path):
- """Test that whitespace is stripped from values."""
- csv_content = _build_aft_csv(
- [
- [
- "AFT-001",
- " Test Diagnostic ",
- " Climate ",
- "https://github.com",
- "ERA5",
- "WCRP",
- "https://example.com",
- " Description ",
- " Short ",
- ],
- ]
- )
-
- csv_path = tmp_path / "whitespace.csv"
- csv_path.write_text(csv_content)
+ assert all(isinstance(d, AFTDiagnosticBase) for d in diagnostics)
+ assert diagnostics[0].id == "1.1"
+ assert diagnostics[0].name == "Sea Ice"
+ assert diagnostics[0].description == "Sea ice desc"
+ assert diagnostics[1].id == "2.1"
+
+ def test_sorted_by_id(self):
+ """Test that diagnostics are returned sorted by id."""
+ collections = {
+ "3.1": _make_collection("3.1", name="Third"),
+ "1.1": _make_collection("1.1", name="First"),
+ "2.1": _make_collection("2.1", name="Second"),
+ }
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
+ with _patch_collections(collections):
diagnostics = load_official_aft_diagnostics()
- assert len(diagnostics) == 1
- diag = diagnostics[0]
- assert diag.id == "AFT-001"
- assert diag.name == "Test Diagnostic"
- assert diag.theme == "Climate"
- assert diag.description == "Description"
- assert diag.short_description == "Short"
-
- def test_diagnostics_sorted_by_id(self, tmp_path: Path):
- """Test that diagnostics are sorted by ID."""
-
- def row(id, name):
- return [
- id,
- name,
- "Climate",
- "https://github.com",
- "ERA5",
- "WCRP",
- "https://example.com",
- "Desc",
- "Short",
- ]
-
- csv_content = _build_aft_csv(
- [
- row("AFT-003", "Third"),
- row("AFT-001", "First"),
- row("AFT-002", "Second"),
- ]
- )
-
- csv_path = tmp_path / "unsorted.csv"
- csv_path.write_text(csv_content)
+ assert [d.id for d in diagnostics] == ["1.1", "2.1", "3.1"]
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
+ def test_empty_collections(self):
+ """Test with no collections."""
+ with _patch_collections({}):
diagnostics = load_official_aft_diagnostics()
- assert len(diagnostics) == 3
- assert diagnostics[0].id == "AFT-001"
- assert diagnostics[1].id == "AFT-002"
- assert diagnostics[2].id == "AFT-003"
-
-
-def _setup_csv_and_yaml(tmp_path, rows, yaml_content=None):
- """Helper to write CSV + optional YAML and return paths."""
- csv_path = tmp_path / "test_aft.csv"
- csv_path.write_text(_build_aft_csv(rows))
-
- yaml_path = tmp_path / "test_mapping.yaml"
- if yaml_content is not None:
- with open(yaml_path, "w") as f:
- yaml.dump(yaml_content, f)
- else:
- yaml_path.write_text("{}")
-
- return csv_path, yaml_path
-
-
-# Standard single-row test data
-_STD_ROW = [
- "AFT-001",
- "Test 1",
- "Climate",
- "https://github.com",
- "ERA5",
- "WCRP",
- "https://example.com",
- "Desc",
- "Short",
-]
-
-_STD_ROW_2 = [
- "AFT-002",
- "Test 2",
- "Ocean",
- "https://github.com",
- "WOA",
- "PCMDI",
- "https://example.org",
- "Desc2",
- "Short2",
-]
-
-
-class TestLoadRefMapping:
- """Test the load_ref_mapping function."""
-
- def test_valid_yaml_parsing(self, tmp_path: Path):
- """Test that valid YAML is parsed correctly."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- {"provider_slug": "pmp", "diagnostic_slug": "mean-climate"},
- ],
- "AFT-002": [
- {"provider_slug": "ilamb", "diagnostic_slug": "biomass"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW, _STD_ROW_2],
- yaml_content,
- )
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- mapping = load_ref_mapping()
-
- assert len(mapping) == 2
- assert "AFT-001" in mapping
- assert "AFT-002" in mapping
-
- aft1_refs = mapping["AFT-001"]
- assert len(aft1_refs) == 2
- assert isinstance(aft1_refs[0], RefDiagnosticLink)
- assert aft1_refs[0].provider_slug == "pmp"
- assert aft1_refs[0].diagnostic_slug == "annual-cycle"
- assert aft1_refs[1].diagnostic_slug == "mean-climate"
-
- aft2_refs = mapping["AFT-002"]
- assert len(aft2_refs) == 1
- assert aft2_refs[0].provider_slug == "ilamb"
-
- def test_deduplication_logic(self, tmp_path: Path, caplog):
- """Test that duplicate entries are deduplicated."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- {"provider_slug": "ilamb", "diagnostic_slug": "biomass"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
- )
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- mapping = load_ref_mapping()
-
- assert len(mapping["AFT-001"]) == 2
- assert mapping["AFT-001"][0].provider_slug == "pmp"
- assert mapping["AFT-001"][1].provider_slug == "ilamb"
-
- def test_missing_yaml_file(self, tmp_path: Path):
- """Test that missing YAML file raises ValueError."""
- csv_path = tmp_path / "test_aft.csv"
- csv_path.write_text(_build_aft_csv([_STD_ROW]))
- nonexistent_yaml = tmp_path / "nonexistent.yaml"
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, nonexistent_yaml),
- ):
- with pytest.raises(ValueError, match="AFT mapping YAML file not found"):
- load_ref_mapping()
-
- def test_unknown_aft_id_warning(self, tmp_path: Path, caplog):
- """Test that unknown AFT IDs in YAML are ignored."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "valid"},
- ],
- "AFT-999": [
- {"provider_slug": "unknown", "diagnostic_slug": "invalid"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
- )
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- mapping = load_ref_mapping()
-
- assert len(mapping) == 1
- assert "AFT-001" in mapping
- assert "AFT-999" not in mapping
-
- def test_invalid_yaml_structure(self, tmp_path: Path):
- """Test that invalid YAML structure raises ValueError."""
- yaml_content = {"AFT-001": "not a list"}
+ assert diagnostics == []
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
+ def test_collection_without_content(self):
+ """Test that missing content fields map to None."""
+ col = AFTCollectionDetail(
+ id="1.1",
+ name="Test",
+ diagnostics=[],
+ explorer_cards=[],
)
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- with pytest.raises(ValueError, match="Expected list for AFT ID"):
- load_ref_mapping()
-
- def test_missing_required_fields(self, tmp_path: Path):
- """Test that missing fields raises ValueError."""
- yaml_content = {
- "AFT-001": [{"provider_slug": "pmp"}],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
- )
+ with _patch_collections({"1.1": col}):
+ diagnostics = load_official_aft_diagnostics()
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- with pytest.raises(
- ValueError,
- match="Missing provider_slug or diagnostic_slug",
- ):
- load_ref_mapping()
+ assert len(diagnostics) == 1
+ assert diagnostics[0].description is None
+ assert diagnostics[0].short_description is None
class TestGetAFTDiagnosticsIndex:
"""Test the get_aft_diagnostics_index function."""
- def test_converts_to_summary_list(self, tmp_path: Path):
+ def test_converts_to_summary_list(self):
"""Test conversion to AFTDiagnosticSummary instances."""
- csv_path, _ = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW, _STD_ROW_2],
- )
+ col1 = _make_collection("1.1")
+ col2 = _make_collection("2.1")
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, tmp_path / "dummy.yaml"),
- ):
+ with _patch_collections({"1.1": col1, "2.1": col2}):
summaries = get_aft_diagnostics_index()
assert len(summaries) == 2
assert all(isinstance(s, AFTDiagnosticSummary) for s in summaries)
- assert summaries[0].id == "AFT-001"
- assert summaries[1].id == "AFT-002"
+ assert summaries[0].id == "1.1"
+ assert summaries[1].id == "2.1"
class TestGetAFTDiagnosticByID:
"""Test the get_aft_diagnostic_by_id function."""
- def test_returns_detail_for_valid_id(self, tmp_path: Path):
- """Test that a valid ID returns AFTDiagnosticDetail."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- {"provider_slug": "pmp", "diagnostic_slug": "mean-climate"},
+ def test_returns_detail_for_valid_id(self):
+ """Test that a valid ID returns AFTDiagnosticDetail with diagnostics."""
+ col = _make_collection(
+ "1.1",
+ name="Sea Ice",
+ diagnostics=[
+ {"provider_slug": "esmvaltool", "diagnostic_slug": "sea-ice-sensitivity"},
+ {"provider_slug": "pmp", "diagnostic_slug": "sea-ice-area"},
],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
)
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- detail = get_aft_diagnostic_by_id("AFT-001")
+ with _patch_collections({"1.1": col}):
+ detail = get_aft_diagnostic_by_id("1.1")
assert detail is not None
assert isinstance(detail, AFTDiagnosticDetail)
- assert detail.id == "AFT-001"
- assert detail.name == "Test 1"
+ assert detail.id == "1.1"
+ assert detail.name == "Sea Ice"
assert len(detail.diagnostics) == 2
- assert detail.diagnostics[0].provider_slug == "pmp"
+ assert detail.diagnostics[0].provider_slug == "esmvaltool"
+ assert detail.diagnostics[0].diagnostic_slug == "sea-ice-sensitivity"
+ assert detail.diagnostics[1].provider_slug == "pmp"
- def test_returns_none_for_missing_id(self, tmp_path: Path):
+ def test_returns_none_for_missing_id(self):
"""Test that a missing ID returns None."""
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- )
-
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- detail = get_aft_diagnostic_by_id("AFT-999")
+ with _patch_collections({"1.1": _make_collection("1.1")}):
+ detail = get_aft_diagnostic_by_id("9.9")
assert detail is None
- def test_includes_empty_diagnostics_list(self, tmp_path: Path):
- """Test that AFT with no mapped diagnostics has empty list."""
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- )
+ def test_includes_empty_diagnostics_list(self):
+ """Test that collection with no linked diagnostics has empty list."""
+ col = _make_collection("1.1", diagnostics=[])
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- detail = get_aft_diagnostic_by_id("AFT-001")
+ with _patch_collections({"1.1": col}):
+ detail = get_aft_diagnostic_by_id("1.1")
assert detail is not None
assert detail.diagnostics == []
+ def test_maps_content_fields(self):
+ """Test that content.description and content.short_description are mapped correctly."""
+ col = _make_collection(
+ "1.1",
+ description="Long description here",
+ short_description="Brief summary",
+ )
+
+ with _patch_collections({"1.1": col}):
+ detail = get_aft_diagnostic_by_id("1.1")
+
+ assert detail is not None
+ assert detail.description == "Long description here"
+ assert detail.short_description == "Brief summary"
+
class TestGetAFTForRefDiagnostic:
"""Test the get_aft_for_ref_diagnostic function."""
- def test_returns_aft_id_for_valid_mapping(self, tmp_path: Path):
- """Test that a valid provider/diagnostic returns correct ID."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
+ def test_returns_aft_id_for_valid_mapping(self):
+ """Test that a valid provider/diagnostic returns correct AFT ID."""
+ col = _make_collection(
+ "1.1",
+ diagnostics=[{"provider_slug": "esmvaltool", "diagnostic_slug": "sea-ice-sensitivity"}],
)
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- aft_id = get_aft_for_ref_diagnostic("pmp", "annual-cycle")
+ with _patch_collections({"1.1": col}):
+ aft_id = get_aft_for_ref_diagnostic("esmvaltool", "sea-ice-sensitivity")
- assert aft_id == "AFT-001"
+ assert aft_id == "1.1"
- def test_returns_none_for_missing_mapping(self, tmp_path: Path):
+ def test_returns_none_for_missing_mapping(self):
"""Test that non-existent provider/diagnostic returns None."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW],
- yaml_content,
+ col = _make_collection(
+ "1.1",
+ diagnostics=[{"provider_slug": "esmvaltool", "diagnostic_slug": "sea-ice-sensitivity"}],
)
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
- aft_id = get_aft_for_ref_diagnostic(
- "nonexistent",
- "diagnostic",
- )
+ with _patch_collections({"1.1": col}):
+ aft_id = get_aft_for_ref_diagnostic("nonexistent", "diagnostic")
assert aft_id is None
- def test_multiple_matches_produce_warning(self, tmp_path: Path, caplog):
- """Test that multiple AFT IDs for same diagnostic warn."""
- yaml_content = {
- "AFT-001": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- ],
- "AFT-002": [
- {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
- ],
- }
-
- csv_path, yaml_path = _setup_csv_and_yaml(
- tmp_path,
- [_STD_ROW, _STD_ROW_2],
- yaml_content,
+ def test_multiple_matches_returns_first_and_warns(self, caplog):
+ """Test that multiple AFT IDs for same diagnostic returns first with warning."""
+ col1 = _make_collection(
+ "1.1",
+ diagnostics=[{"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"}],
+ )
+ col2 = _make_collection(
+ "2.1",
+ diagnostics=[{"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"}],
)
- with patch(
- "ref_backend.core.aft.get_aft_paths",
- return_value=(csv_path, yaml_path),
- ):
+ with _patch_collections({"1.1": col1, "2.1": col2}):
aft_id = get_aft_for_ref_diagnostic("pmp", "annual-cycle")
- assert aft_id in ["AFT-001", "AFT-002"]
+ assert aft_id in ["1.1", "2.1"]
+
+ def test_returns_none_for_empty_collections(self):
+ """Test with no collections at all."""
+ with _patch_collections({}):
+ aft_id = get_aft_for_ref_diagnostic("pmp", "annual-cycle")
+
+ assert aft_id is None
diff --git a/backend/tests/test_core/test_core_collections.py b/backend/tests/test_core/test_core_collections.py
new file mode 100644
index 0000000..187ff85
--- /dev/null
+++ b/backend/tests/test_core/test_core_collections.py
@@ -0,0 +1,529 @@
+"""Tests for collection loading and mapping functionality."""
+
+import json
+import logging
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+import yaml
+
+from ref_backend.core.collections import (
+ AFTCollectionCard,
+ AFTCollectionCardContent,
+ AFTCollectionDetail,
+ AFTCollectionGroupingConfig,
+ AFTCollectionSummary,
+ ThemeDetail,
+ get_collection_by_id,
+ get_collection_summaries,
+ get_theme_by_slug,
+ get_theme_summaries,
+ load_all_collections,
+ load_theme_mapping,
+)
+
+
+@pytest.fixture(autouse=True)
+def clear_collection_caches():
+ """Clear all lru_cache'd functions before and after each test."""
+ load_all_collections.cache_clear()
+ load_theme_mapping.cache_clear()
+ yield
+ load_all_collections.cache_clear()
+ load_theme_mapping.cache_clear()
+
+
+def _write_collection(tmp_path: Path, filename: str, data: dict) -> Path:
+ """Write a collection YAML file and return its path."""
+ p = tmp_path / filename
+ with open(p, "w") as f:
+ yaml.dump(data, f)
+ return p
+
+
+def _write_themes(tmp_path: Path, data: dict) -> Path:
+ """Write a themes.yaml file and return its path."""
+ p = tmp_path / "themes.yaml"
+ with open(p, "w") as f:
+ yaml.dump(data, f)
+ return p
+
+
+_MINIMAL_COLLECTION = {
+ "id": "1.1",
+ "name": "Test Collection",
+ "theme": "Ocean",
+ "endorser": "WCRP",
+ "version_control": "1.0",
+ "reference_dataset": "ERA5",
+ "provider_link": "https://example.com",
+ "content": {
+ "description": "Full description",
+ "short_description": "Short",
+ },
+ "diagnostics": [
+ {"provider_slug": "pmp", "diagnostic_slug": "annual-cycle"},
+ ],
+ "explorer_cards": [
+ {
+ "title": "Card One",
+ "description": "A card",
+ "content": [
+ {
+ "type": "box-whisker-chart",
+ "provider": "pmp",
+ "diagnostic": "annual-cycle",
+ "title": "Annual Cycle Chart",
+ "grouping_config": {
+ "group_by": "model",
+ "hue": "experiment",
+ },
+ }
+ ],
+ }
+ ],
+}
+
+
+class TestLoadAllCollections:
+ def test_valid_collection_loading(self, tmp_path: Path):
+ """Valid YAML loads into AFTCollectionDetail model correctly."""
+ _write_collection(tmp_path, "1.1.yaml", _MINIMAL_COLLECTION)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = load_all_collections()
+
+ assert "1.1" in result
+ col = result["1.1"]
+ assert isinstance(col, AFTCollectionDetail)
+ assert col.id == "1.1"
+ assert col.name == "Test Collection"
+ assert col.theme == "Ocean"
+ assert col.endorser == "WCRP"
+ assert col.version_control == "1.0"
+ assert col.reference_dataset == "ERA5"
+ assert str(col.provider_link) == "https://example.com/"
+ assert col.content is not None
+ assert col.content.description == "Full description"
+ assert col.content.short_description == "Short"
+ assert len(col.diagnostics) == 1
+ assert col.diagnostics[0].provider_slug == "pmp"
+ assert col.diagnostics[0].diagnostic_slug == "annual-cycle"
+ assert len(col.explorer_cards) == 1
+ card = col.explorer_cards[0]
+ assert isinstance(card, AFTCollectionCard)
+ assert card.title == "Card One"
+ assert len(card.content) == 1
+ content = card.content[0]
+ assert isinstance(content, AFTCollectionCardContent)
+ assert content.type == "box-whisker-chart"
+ assert content.grouping_config is not None
+ assert isinstance(content.grouping_config, AFTCollectionGroupingConfig)
+ assert content.grouping_config.group_by == "model"
+ assert content.grouping_config.hue == "experiment"
+
+ def test_all_optional_card_content_fields(self, tmp_path: Path):
+ """All optional fields on AFTCollectionCardContent deserialize correctly."""
+ data = {
+ "id": "2.1",
+ "name": "Full Fields Collection",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "Detailed Card",
+ "content": [
+ {
+ "type": "series-chart",
+ "provider": "ilamb",
+ "diagnostic": "biomass",
+ "title": "Biomass Series",
+ "description": "Desc",
+ "interpretation": "Some interpretation",
+ "span": 2,
+ "metric_units": "kg/m2",
+ "clip_min": -10.5,
+ "clip_max": 10.5,
+ "y_min": -5.0,
+ "y_max": 5.0,
+ "show_zero_line": True,
+ "symmetrical_axes": False,
+ "reference_stddev": 1.5,
+ "label_template": "{model} ({experiment})",
+ "other_filters": {
+ "isolate_ids": "CMIP6.ssp585",
+ "exclude_ids": "CMIP6.piControl",
+ },
+ "grouping_config": {
+ "group_by": "source_id",
+ "hue": "variant_label",
+ "style": "experiment_id",
+ },
+ }
+ ],
+ }
+ ],
+ }
+ _write_collection(tmp_path, "2.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = load_all_collections()
+
+ col = result["2.1"]
+ content = col.explorer_cards[0].content[0]
+ assert content.label_template == "{model} ({experiment})"
+ assert content.grouping_config is not None
+ assert content.grouping_config.style == "experiment_id"
+ assert content.other_filters == {
+ "isolate_ids": "CMIP6.ssp585",
+ "exclude_ids": "CMIP6.piControl",
+ }
+ assert content.y_min == -5.0
+ assert content.y_max == 5.0
+ assert content.clip_min == -10.5
+ assert content.clip_max == 10.5
+ assert content.span == 2
+ assert content.show_zero_line is True
+ assert content.symmetrical_axes is False
+ assert content.reference_stddev == 1.5
+
+ def test_missing_collections_directory_returns_empty(self, tmp_path: Path):
+ """Missing collections directory returns empty dict without crashing."""
+ nonexistent = tmp_path / "no_such_dir"
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=nonexistent):
+ result = load_all_collections()
+
+ assert result == {}
+
+ def test_invalid_yaml_file_is_skipped_with_warning(self, tmp_path: Path, caplog):
+ """A file with invalid YAML content is skipped and a warning is logged."""
+ bad_file = tmp_path / "bad.yaml"
+ bad_file.write_text("key: [unclosed bracket")
+
+ good_data = {**_MINIMAL_COLLECTION, "id": "1.1", "name": "Good"}
+ _write_collection(tmp_path, "1.1.yaml", good_data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ with caplog.at_level(logging.WARNING, logger="ref_backend.core.collections"):
+ result = load_all_collections()
+
+ assert "1.1" in result
+ assert "bad" not in str(result)
+ assert any("bad.yaml" in msg for msg in caplog.messages)
+
+ def test_invalid_content_type_rejected(self, tmp_path: Path, caplog):
+ """Invalid content type is rejected by Pydantic and file is skipped."""
+ data = {
+ "id": "3.1",
+ "name": "Bad Type Collection",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "Bad Card",
+ "content": [
+ {
+ "type": "not-a-valid-chart-type",
+ "provider": "pmp",
+ "diagnostic": "test",
+ "title": "Chart",
+ }
+ ],
+ }
+ ],
+ }
+ _write_collection(tmp_path, "3.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ with caplog.at_level(logging.WARNING, logger="ref_backend.core.collections"):
+ result = load_all_collections()
+
+ assert "3.1" not in result
+ assert any("3.1.yaml" in msg for msg in caplog.messages)
+
+ def test_json_serialization_uses_snake_case(self, tmp_path: Path):
+ """JSON serialization must use snake_case keys, not camelCase."""
+ data = {
+ "id": "4.1",
+ "name": "Snake Case Test",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "Chart Card",
+ "content": [
+ {
+ "type": "series-chart",
+ "provider": "pmp",
+ "diagnostic": "mean-climate",
+ "title": "Mean Climate",
+ "y_min": -1.0,
+ "y_max": 1.0,
+ "show_zero_line": True,
+ "grouping_config": {
+ "group_by": "model",
+ "hue": "experiment",
+ },
+ }
+ ],
+ }
+ ],
+ }
+ _write_collection(tmp_path, "4.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = load_all_collections()
+
+ col = result["4.1"]
+ serialized = json.loads(col.model_dump_json())
+
+ # Assert snake_case keys
+ card_content = serialized["explorer_cards"][0]["content"][0]
+ assert "y_min" in card_content
+ assert "y_max" in card_content
+ assert "show_zero_line" in card_content
+ assert "grouping_config" in card_content
+ assert "group_by" in card_content["grouping_config"]
+
+ # Assert no camelCase keys
+ camel_case_keys = ["yMin", "yMax", "showZeroLine", "groupingConfig", "groupBy"]
+ for key in camel_case_keys:
+ assert key not in card_content, f"Found camelCase key: {key}"
+ assert key not in card_content.get("grouping_config", {})
+
+ def test_summary_card_count(self, tmp_path: Path):
+ """AFTCollectionSummary.card_count matches actual number of explorer_cards."""
+ data = {**_MINIMAL_COLLECTION, "id": "5.1"}
+ # has 1 card in _MINIMAL_COLLECTION
+ _write_collection(tmp_path, "5.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ summaries = get_collection_summaries()
+
+ assert len(summaries) == 1
+ s = summaries[0]
+ assert isinstance(s, AFTCollectionSummary)
+ assert s.card_count == 1
+
+ def test_collection_with_empty_explorer_cards(self, tmp_path: Path):
+ """Collection with explorer_cards: [] loads without errors."""
+ data = {
+ "id": "6.1",
+ "name": "Empty Cards",
+ "diagnostics": [],
+ "explorer_cards": [],
+ }
+ _write_collection(tmp_path, "6.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = load_all_collections()
+
+ assert "6.1" in result
+ assert result["6.1"].explorer_cards == []
+
+ def test_placeholder_card_with_empty_content(self, tmp_path: Path):
+ """Card with content: [] and placeholder: true loads (ENSO case)."""
+ data = {
+ "id": "7.1",
+ "name": "ENSO Collection",
+ "diagnostics": [],
+ "explorer_cards": [
+ {
+ "title": "ENSO Placeholder",
+ "placeholder": True,
+ "content": [],
+ }
+ ],
+ }
+ _write_collection(tmp_path, "7.1.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = load_all_collections()
+
+ col = result["7.1"]
+ assert len(col.explorer_cards) == 1
+ card = col.explorer_cards[0]
+ assert card.placeholder is True
+ assert card.content == []
+
+ def test_duplicate_collection_ids_logs_warning(self, tmp_path: Path, caplog):
+ """Duplicate IDs across YAML files results in a warning for the second file."""
+ data1 = {**_MINIMAL_COLLECTION, "id": "dup-1", "name": "First"}
+ data2 = {**_MINIMAL_COLLECTION, "id": "dup-1", "name": "Second"}
+ _write_collection(tmp_path, "a.yaml", data1)
+ _write_collection(tmp_path, "b.yaml", data2)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ with caplog.at_level(logging.WARNING, logger="ref_backend.core.collections"):
+ result = load_all_collections()
+
+ # One file was skipped, so only one collection with dup-1
+ assert len([c for c in result.values() if c.id == "dup-1"]) == 1
+
+ def test_collections_sorted_by_id(self, tmp_path: Path):
+ """Collections are returned sorted by ID for stable ordering."""
+ for cid in ["3.1", "1.1", "2.1"]:
+ data = {**_MINIMAL_COLLECTION, "id": cid, "name": f"Col {cid}"}
+ _write_collection(tmp_path, f"{cid}.yaml", data)
+
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ summaries = get_collection_summaries()
+
+ ids = [s.id for s in summaries]
+ assert ids == sorted(ids)
+
+
+class TestLoadThemeMapping:
+ def test_theme_mapping_loads_and_resolves_collections(self, tmp_path: Path):
+ """Theme mapping loads correctly and resolves collection references."""
+ _write_collection(tmp_path, "1.1.yaml", _MINIMAL_COLLECTION)
+ col2 = {**_MINIMAL_COLLECTION, "id": "1.2", "name": "Second Collection"}
+ _write_collection(tmp_path, "1.2.yaml", col2)
+
+ themes = {
+ "ocean": {
+ "title": "Ocean Theme",
+ "description": "Ocean diagnostics",
+ "collections": ["1.1", "1.2"],
+ }
+ }
+ _write_themes(tmp_path, themes)
+
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=tmp_path / "themes.yaml"),
+ ):
+ mapping = load_theme_mapping()
+
+ assert "ocean" in mapping
+ theme = mapping["ocean"]
+ assert isinstance(theme, ThemeDetail)
+ assert theme.slug == "ocean"
+ assert theme.title == "Ocean Theme"
+ assert theme.description == "Ocean diagnostics"
+ assert len(theme.collections) == 2
+ assert theme.collections[0].id == "1.1"
+ assert theme.collections[1].id == "1.2"
+
+ def test_theme_aggregates_explorer_cards_in_order(self, tmp_path: Path):
+ """Theme.explorer_cards aggregates all cards from collections in order."""
+ col1 = {
+ **_MINIMAL_COLLECTION,
+ "id": "c1",
+ "explorer_cards": [
+ {"title": "Card A", "content": []},
+ {"title": "Card B", "content": []},
+ ],
+ }
+ col2 = {
+ **_MINIMAL_COLLECTION,
+ "id": "c2",
+ "explorer_cards": [
+ {"title": "Card C", "content": []},
+ ],
+ }
+ _write_collection(tmp_path, "c1.yaml", col1)
+ _write_collection(tmp_path, "c2.yaml", col2)
+
+ themes = {"climate": {"title": "Climate", "collections": ["c1", "c2"]}}
+ _write_themes(tmp_path, themes)
+
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=tmp_path / "themes.yaml"),
+ ):
+ theme = get_theme_by_slug("climate")
+
+ assert theme is not None
+ assert len(theme.explorer_cards) == 3
+ assert theme.explorer_cards[0].title == "Card A"
+ assert theme.explorer_cards[1].title == "Card B"
+ assert theme.explorer_cards[2].title == "Card C"
+
+ def test_unknown_collection_id_in_themes_logs_warning(self, tmp_path: Path, caplog):
+ """Unknown collection ID in themes.yaml is skipped with a warning."""
+ _write_collection(tmp_path, "1.1.yaml", _MINIMAL_COLLECTION)
+ themes = {
+ "ocean": {
+ "title": "Ocean",
+ "collections": ["1.1", "nonexistent-id"],
+ }
+ }
+ _write_themes(tmp_path, themes)
+
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=tmp_path / "themes.yaml"),
+ ):
+ with caplog.at_level(logging.WARNING, logger="ref_backend.core.collections"):
+ theme = get_theme_by_slug("ocean")
+
+ assert theme is not None
+ assert len(theme.collections) == 1 # nonexistent was skipped
+ assert any("nonexistent-id" in msg for msg in caplog.messages)
+
+ def test_collection_shared_across_themes(self, tmp_path: Path):
+ """A collection referenced in two themes appears in both theme details."""
+ _write_collection(tmp_path, "shared.yaml", {**_MINIMAL_COLLECTION, "id": "shared"})
+
+ themes = {
+ "theme-a": {"title": "Theme A", "collections": ["shared"]},
+ "theme-b": {"title": "Theme B", "collections": ["shared"]},
+ }
+ _write_themes(tmp_path, themes)
+
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=tmp_path / "themes.yaml"),
+ ):
+ theme_a = get_theme_by_slug("theme-a")
+ theme_b = get_theme_by_slug("theme-b")
+
+ assert theme_a is not None
+ assert theme_b is not None
+ assert any(c.id == "shared" for c in theme_a.collections)
+ assert any(c.id == "shared" for c in theme_b.collections)
+
+ def test_get_theme_summaries(self, tmp_path: Path):
+ """get_theme_summaries returns ThemeSummary with correct counts."""
+ col1 = {**_MINIMAL_COLLECTION, "id": "t1"} # 1 card
+ col2 = {
+ **_MINIMAL_COLLECTION,
+ "id": "t2",
+ "explorer_cards": [
+ {"title": "C1", "content": []},
+ {"title": "C2", "content": []},
+ ],
+ }
+ _write_collection(tmp_path, "t1.yaml", col1)
+ _write_collection(tmp_path, "t2.yaml", col2)
+
+ themes = {"big-theme": {"title": "Big Theme", "collections": ["t1", "t2"]}}
+ _write_themes(tmp_path, themes)
+
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=tmp_path / "themes.yaml"),
+ ):
+ summaries = get_theme_summaries()
+
+ assert len(summaries) == 1
+ s = summaries[0]
+ assert s.slug == "big-theme"
+ assert s.collection_count == 2
+ assert s.card_count == 3 # 1 from t1 + 2 from t2
+
+ def test_get_collection_by_id_returns_none_for_missing(self, tmp_path: Path):
+ """get_collection_by_id returns None for nonexistent ID."""
+ with patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path):
+ result = get_collection_by_id("does-not-exist")
+ assert result is None
+
+ def test_missing_themes_file_returns_empty(self, tmp_path: Path):
+ """Missing themes.yaml returns empty dict without crashing."""
+ nonexistent = tmp_path / "themes.yaml"
+ with (
+ patch("ref_backend.core.collections.get_collections_dir", return_value=tmp_path),
+ patch("ref_backend.core.collections.get_themes_path", return_value=nonexistent),
+ ):
+ result = load_theme_mapping()
+
+ assert result == {}
diff --git a/backend/tests/test_core/test_diagnostic_metadata.py b/backend/tests/test_core/test_diagnostic_metadata.py
index 454c09d..53b448a 100644
--- a/backend/tests/test_core/test_diagnostic_metadata.py
+++ b/backend/tests/test_core/test_diagnostic_metadata.py
@@ -177,6 +177,81 @@ def test_all_reference_dataset_types(self, tmp_path: Path):
assert refs[1].type == "secondary"
assert refs[2].type == "comparison"
+ def test_directory_loading(self, tmp_path: Path):
+ """Test that loading from a directory merges all YAML files."""
+ pmp_content = {
+ "pmp/annual-cycle": {
+ "display_name": "Annual Cycle",
+ "reference_datasets": [
+ {"slug": "obs4mips.ERA-5", "type": "primary"},
+ ],
+ },
+ }
+ ilamb_content = {
+ "ilamb/gpp-wecann": {
+ "display_name": "GPP (WECANN)",
+ "reference_datasets": [
+ {"slug": "obs4REF.WECANN", "type": "primary"},
+ ],
+ },
+ }
+
+ metadata_dir = tmp_path / "diagnostics"
+ metadata_dir.mkdir()
+ with open(metadata_dir / "pmp.yaml", "w") as f:
+ yaml.dump(pmp_content, f)
+ with open(metadata_dir / "ilamb.yaml", "w") as f:
+ yaml.dump(ilamb_content, f)
+
+ result = load_diagnostic_metadata(metadata_dir)
+
+ assert len(result) == 2
+ assert "pmp/annual-cycle" in result
+ assert "ilamb/gpp-wecann" in result
+ assert result["pmp/annual-cycle"].display_name == "Annual Cycle"
+ assert result["ilamb/gpp-wecann"].display_name == "GPP (WECANN)"
+
+ def test_directory_with_no_yaml_files(self, tmp_path: Path):
+ """Test that an empty directory returns an empty dict."""
+ metadata_dir = tmp_path / "empty_dir"
+ metadata_dir.mkdir()
+
+ result = load_diagnostic_metadata(metadata_dir)
+
+ assert result == {}
+
+ def test_directory_duplicate_keys_warns(self, tmp_path: Path, caplog):
+ """Test that duplicate keys across files produce a warning and last file wins."""
+ file1_content = {
+ "pmp/annual-cycle": {
+ "display_name": "From File 1",
+ "reference_datasets": [
+ {"slug": "dataset1", "type": "primary"},
+ ],
+ },
+ }
+ file2_content = {
+ "pmp/annual-cycle": {
+ "display_name": "From File 2",
+ "reference_datasets": [
+ {"slug": "dataset2", "type": "secondary"},
+ ],
+ },
+ }
+
+ metadata_dir = tmp_path / "diagnostics"
+ metadata_dir.mkdir()
+ # Files are loaded in sorted order, so b.yaml overrides a.yaml
+ with open(metadata_dir / "a.yaml", "w") as f:
+ yaml.dump(file1_content, f)
+ with open(metadata_dir / "b.yaml", "w") as f:
+ yaml.dump(file2_content, f)
+
+ result = load_diagnostic_metadata(metadata_dir)
+
+ assert len(result) == 1
+ assert result["pmp/annual-cycle"].display_name == "From File 2"
+
def test_optional_fields(self, tmp_path: Path):
"""Test that optional fields (display_name, tags, description) work correctly."""
yaml_content = {
diff --git a/backend/uv.lock b/backend/uv.lock
index cd5cafc..6edf55a 100644
--- a/backend/uv.lock
+++ b/backend/uv.lock
@@ -352,7 +352,7 @@ wheels = [
[[package]]
name = "climate-ref"
-version = "0.12.0"
+version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alembic" },
@@ -371,9 +371,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/db/99/61a08a649842d61af054f652d21827441c53cef80e85244116cd1bdd5903/climate_ref-0.12.0.tar.gz", hash = "sha256:01b03c50b437626d12a7ca83b4b60c4b9eda391c0fbef55ac1e2be9f42594a92", size = 262084, upload-time = "2026-03-03T10:48:30.624Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/97/c34988406577bf058e8da7e44233dc70cf84839006ba48e291fa1101ed2c/climate_ref-0.12.2.tar.gz", hash = "sha256:b54216a8359bb68ab5a3bfaabd05763694d806ad64b5c0beb158b15ac66534b4", size = 262109, upload-time = "2026-03-06T04:25:20.36Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/12/697c06649c762ef76623515e7855dc537f8d5df96323bcc901567316f02a/climate_ref-0.12.0-py3-none-any.whl", hash = "sha256:9998d6d7f7d79474ad9a0961b4d7b16537dc4704f286d4ef2bb76d534f440e5b", size = 162062, upload-time = "2026-03-03T10:48:29.303Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/bc/9543965269e1369a4ad34b86daffb98e1c9d07b3d3f4aa7311185b22ef28/climate_ref-0.12.2-py3-none-any.whl", hash = "sha256:8c149855a758c7c62e27564afbc51d6409cc12183bfd097e852ba6e603011680", size = 162065, upload-time = "2026-03-06T04:25:18.893Z" },
]
[package.optional-dependencies]
@@ -2022,16 +2022,16 @@ name = "parsl"
version = "2026.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "dill" },
- { name = "filelock" },
- { name = "psutil" },
- { name = "pyzmq" },
- { name = "requests" },
- { name = "setproctitle" },
- { name = "sortedcontainers" },
- { name = "tblib" },
- { name = "typeguard" },
- { name = "typing-extensions" },
+ { name = "dill", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "filelock", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "psutil", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "pyzmq", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "requests", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "setproctitle", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "sortedcontainers", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "tblib", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "typeguard", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
+ { name = "typing-extensions", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/25/c870b421fa5a703689883a7df104bb0728024400b0f62709c086ad6b4123/parsl-2026.3.2.tar.gz", hash = "sha256:5775edbdb3ef62b61dc6f4480fae50d454d414f3b439747b1f8d6e5e39238889", size = 375527, upload-time = "2026-03-02T22:50:18.961Z" }
wheels = [
@@ -2847,7 +2847,7 @@ name = "pyzmq"
version = "27.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi", marker = "implementation_name == 'pypy'" },
+ { name = "cffi", marker = "(implementation_name == 'pypy' and platform_machine != 'ARM64') or (implementation_name == 'pypy' and sys_platform != 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
wheels = [
@@ -2918,7 +2918,7 @@ dev = [
[package.metadata]
requires-dist = [
- { name = "climate-ref", extras = ["aft-providers", "postgres"], specifier = ">=0.12.0" },
+ { name = "climate-ref", extras = ["aft-providers", "postgres"], specifier = ">=0.12.2" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.114.2,<1.0.0" },
{ name = "fastapi-sqlalchemy-monitor", specifier = ">=1.1.3" },
{ name = "loguru" },
@@ -3633,7 +3633,7 @@ name = "typeguard"
version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions" },
+ { name = "typing-extensions", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" }
wheels = [
diff --git a/frontend/biome.json b/frontend/biome.json
index 085e5c7..e9e9b8d 100644
--- a/frontend/biome.json
+++ b/frontend/biome.json
@@ -12,14 +12,21 @@
"!**/dist",
"!**/src/client",
"!**/*.gen.ts",
- "!**/openapi.json"
+ "!**/openapi.json",
+ "!**/.omc"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space"
},
- "assist": { "actions": { "source": { "organizeImports": "on" } } },
+ "assist": {
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ },
"linter": {
"enabled": true,
"rules": {
diff --git a/frontend/src/client/@tanstack/react-query.gen.ts b/frontend/src/client/@tanstack/react-query.gen.ts
index 1601de3..4ae037d 100644
--- a/frontend/src/client/@tanstack/react-query.gen.ts
+++ b/frontend/src/client/@tanstack/react-query.gen.ts
@@ -1,8 +1,8 @@
// This file is auto-generated by @hey-api/openapi-ts
-import { type Options, cmip7AssessmentFastTrackAftListAftDiagnostics, cmip7AssessmentFastTrackAftGetAftDiagnostic, datasetsList, datasetsGet, datasetsExecutions, diagnosticsList, diagnosticsFacets, diagnosticsGet, diagnosticsListExecutionGroups, diagnosticsListExecutions, diagnosticsListMetricValues, executionsGetExecutionStatistics, executionsListRecentExecutionGroups, executionsGet, executionsExecution, executionsExecutionDatasets, executionsExecutionLogs, executionsMetricBundle, executionsListMetricValues, executionsExecutionArchive, resultsGetResult, utilsHealthCheck } from '../sdk.gen';
+import { type Options, cmip7AssessmentFastTrackAftListAftDiagnostics, cmip7AssessmentFastTrackAftGetAftDiagnostic, datasetsList, datasetsGet, datasetsExecutions, diagnosticsList, diagnosticsFacets, diagnosticsGet, diagnosticsListExecutionGroups, diagnosticsListExecutions, diagnosticsListMetricValues, executionsGetExecutionStatistics, executionsListRecentExecutionGroups, executionsGet, executionsExecution, executionsExecutionDatasets, executionsExecutionLogs, executionsMetricBundle, executionsListMetricValues, executionsExecutionArchive, explorerListCollections, explorerGetCollection, explorerListThemes, explorerGetTheme, resultsGetResult, utilsHealthCheck } from '../sdk.gen';
import { queryOptions, infiniteQueryOptions, type InfiniteData } from '@tanstack/react-query';
-import type { Cmip7AssessmentFastTrackAftListAftDiagnosticsData, Cmip7AssessmentFastTrackAftGetAftDiagnosticData, DatasetsListData, DatasetsListError, DatasetsListResponse, DatasetsGetData, DatasetsExecutionsData, DatasetsExecutionsError, DatasetsExecutionsResponse, DiagnosticsListData, DiagnosticsFacetsData, DiagnosticsGetData, DiagnosticsListExecutionGroupsData, DiagnosticsListExecutionsData, DiagnosticsListMetricValuesData, ExecutionsGetExecutionStatisticsData, ExecutionsListRecentExecutionGroupsData, ExecutionsListRecentExecutionGroupsError, ExecutionsListRecentExecutionGroupsResponse, ExecutionsGetData, ExecutionsExecutionData, ExecutionsExecutionDatasetsData, ExecutionsExecutionLogsData, ExecutionsMetricBundleData, ExecutionsListMetricValuesData, ExecutionsExecutionArchiveData, ResultsGetResultData, UtilsHealthCheckData } from '../types.gen';
+import type { Cmip7AssessmentFastTrackAftListAftDiagnosticsData, Cmip7AssessmentFastTrackAftGetAftDiagnosticData, DatasetsListData, DatasetsListError, DatasetsListResponse, DatasetsGetData, DatasetsExecutionsData, DatasetsExecutionsError, DatasetsExecutionsResponse, DiagnosticsListData, DiagnosticsFacetsData, DiagnosticsGetData, DiagnosticsListExecutionGroupsData, DiagnosticsListExecutionsData, DiagnosticsListMetricValuesData, ExecutionsGetExecutionStatisticsData, ExecutionsListRecentExecutionGroupsData, ExecutionsListRecentExecutionGroupsError, ExecutionsListRecentExecutionGroupsResponse, ExecutionsGetData, ExecutionsExecutionData, ExecutionsExecutionDatasetsData, ExecutionsExecutionLogsData, ExecutionsMetricBundleData, ExecutionsListMetricValuesData, ExecutionsExecutionArchiveData, ExplorerListCollectionsData, ExplorerGetCollectionData, ExplorerListThemesData, ExplorerGetThemeData, ResultsGetResultData, UtilsHealthCheckData } from '../types.gen';
import { client as _heyApiClient } from '../client.gen';
export type QueryKey = [
@@ -38,6 +38,10 @@ const createQueryKey = (id: string, options?: TOptions
export const cmip7AssessmentFastTrackAftListAftDiagnosticsQueryKey = (options?: Options) => createQueryKey('cmip7AssessmentFastTrackAftListAftDiagnostics', options);
+/**
+ * List Aft Diagnostics
+ * Get all AFT diagnostics.
+ */
export const cmip7AssessmentFastTrackAftListAftDiagnosticsOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -55,6 +59,10 @@ export const cmip7AssessmentFastTrackAftListAftDiagnosticsOptions = (options?: O
export const cmip7AssessmentFastTrackAftGetAftDiagnosticQueryKey = (options: Options) => createQueryKey('cmip7AssessmentFastTrackAftGetAftDiagnostic', options);
+/**
+ * Get Aft Diagnostic
+ * Get detailed AFT diagnostic by ID.
+ */
export const cmip7AssessmentFastTrackAftGetAftDiagnosticOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -72,6 +80,10 @@ export const cmip7AssessmentFastTrackAftGetAftDiagnosticOptions = (options: Opti
export const datasetsListQueryKey = (options?: Options) => createQueryKey('datasetsList', options);
+/**
+ * List
+ * Paginated list of currently ingested datasets
+ */
export const datasetsListOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -118,6 +130,10 @@ const createInfiniteParams = [0], 'body' | 'hea
export const datasetsListInfiniteQueryKey = (options?: Options): QueryKey> => createQueryKey('datasetsList', options, true);
+/**
+ * List
+ * Paginated list of currently ingested datasets
+ */
export const datasetsListInfiniteOptions = (options?: Options) => {
return infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
@@ -144,6 +160,10 @@ export const datasetsListInfiniteOptions = (options?: Options)
export const datasetsGetQueryKey = (options: Options) => createQueryKey('datasetsGet', options);
+/**
+ * Get
+ * Get a single dataset by slug
+ */
export const datasetsGetOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -161,6 +181,10 @@ export const datasetsGetOptions = (options: Options) => {
export const datasetsExecutionsQueryKey = (options: Options) => createQueryKey('datasetsExecutions', options);
+/**
+ * Executions
+ * List the currently registered diagnostics
+ */
export const datasetsExecutionsOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -178,6 +202,10 @@ export const datasetsExecutionsOptions = (options: Options): QueryKey> => createQueryKey('datasetsExecutions', options, true);
+/**
+ * Executions
+ * List the currently registered diagnostics
+ */
export const datasetsExecutionsInfiniteOptions = (options: Options) => {
return infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
@@ -204,6 +232,10 @@ export const datasetsExecutionsInfiniteOptions = (options: Options) => createQueryKey('diagnosticsList', options);
+/**
+ * List
+ * List the currently registered diagnostics
+ */
export const diagnosticsListOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -221,6 +253,10 @@ export const diagnosticsListOptions = (options?: Options) =
export const diagnosticsFacetsQueryKey = (options?: Options) => createQueryKey('diagnosticsFacets', options);
+/**
+ * Facets
+ * Query the unique dimensions and metrics for all diagnostics (both scalar and series)
+ */
export const diagnosticsFacetsOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -238,6 +274,10 @@ export const diagnosticsFacetsOptions = (options?: Options) => createQueryKey('diagnosticsGet', options);
+/**
+ * Get
+ * Fetch a result using the slug
+ */
export const diagnosticsGetOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -255,6 +295,10 @@ export const diagnosticsGetOptions = (options: Options) => {
export const diagnosticsListExecutionGroupsQueryKey = (options: Options) => createQueryKey('diagnosticsListExecutionGroups', options);
+/**
+ * List Execution Groups
+ * Fetch execution groups for a diagnostic.
+ */
export const diagnosticsListExecutionGroupsOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -272,6 +316,12 @@ export const diagnosticsListExecutionGroupsOptions = (options: Options) => createQueryKey('diagnosticsListExecutions', options);
+/**
+ * List Executions
+ * Fetch executions for a specific diagnostic, with arbitrary filters on the dataset.
+ *
+ * e.g. `?source_id=MIROC6&experiment_id=ssp585`
+ */
export const diagnosticsListExecutionsOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -289,6 +339,13 @@ export const diagnosticsListExecutionsOptions = (options: Options) => createQueryKey('diagnosticsListMetricValues', options);
+/**
+ * List Metric Values
+ * Get all the diagnostic values for a given diagnostic (both scalar and series)
+ *
+ * - `value_type`: Type of metric values - 'scalar', 'series', or 'all' (required)
+ * - `format`: Return format - 'json' (default) or 'csv'
+ */
export const diagnosticsListMetricValuesOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -306,6 +363,13 @@ export const diagnosticsListMetricValuesOptions = (options: Options) => createQueryKey('executionsGetExecutionStatistics', options);
+/**
+ * Get Execution Statistics
+ * Get execution statistics for the dashboard.
+ *
+ * Returns counts of total, successful, and failed execution groups,
+ * plus recent activity count.
+ */
export const executionsGetExecutionStatisticsOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -323,6 +387,18 @@ export const executionsGetExecutionStatisticsOptions = (options?: Options) => createQueryKey('executionsListRecentExecutionGroups', options);
+/**
+ * List Recent Execution Groups
+ * List the most recent execution groups
+ *
+ * Supports filtering by:
+ * - diagnostic_name_contains
+ * - provider_name_contains
+ * - dirty
+ * - successful (filters by latest execution success)
+ * - source_id (filters groups that include an execution whose datasets
+ * include a CMIP6 dataset with this source_id)
+ */
export const executionsListRecentExecutionGroupsOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -340,6 +416,18 @@ export const executionsListRecentExecutionGroupsOptions = (options?: Options): QueryKey> => createQueryKey('executionsListRecentExecutionGroups', options, true);
+/**
+ * List Recent Execution Groups
+ * List the most recent execution groups
+ *
+ * Supports filtering by:
+ * - diagnostic_name_contains
+ * - provider_name_contains
+ * - dirty
+ * - successful (filters by latest execution success)
+ * - source_id (filters groups that include an execution whose datasets
+ * include a CMIP6 dataset with this source_id)
+ */
export const executionsListRecentExecutionGroupsInfiniteOptions = (options?: Options) => {
return infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
@@ -366,6 +454,10 @@ export const executionsListRecentExecutionGroupsInfiniteOptions = (options?: Opt
export const executionsGetQueryKey = (options: Options) => createQueryKey('executionsGet', options);
+/**
+ * Get
+ * Inspect a specific execution
+ */
export const executionsGetOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -383,6 +475,12 @@ export const executionsGetOptions = (options: Options) => {
export const executionsExecutionQueryKey = (options: Options) => createQueryKey('executionsExecution', options);
+/**
+ * Execution
+ * Inspect a specific execution
+ *
+ * Gets the latest result if no execution_id is provided
+ */
export const executionsExecutionOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -400,6 +498,10 @@ export const executionsExecutionOptions = (options: Options) => createQueryKey('executionsExecutionDatasets', options);
+/**
+ * Execution Datasets
+ * Query the datasets that were used for a specific execution
+ */
export const executionsExecutionDatasetsOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -417,6 +519,10 @@ export const executionsExecutionDatasetsOptions = (options: Options) => createQueryKey('executionsExecutionLogs', options);
+/**
+ * Execution Logs
+ * Fetch the logs for an execution result
+ */
export const executionsExecutionLogsOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -434,6 +540,10 @@ export const executionsExecutionLogsOptions = (options: Options) => createQueryKey('executionsMetricBundle', options);
+/**
+ * Metric Bundle
+ * Fetch a result using the slug
+ */
export const executionsMetricBundleOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -451,6 +561,13 @@ export const executionsMetricBundleOptions = (options: Options) => createQueryKey('executionsListMetricValues', options);
+/**
+ * List Metric Values
+ * Fetch metric values for a specific execution (both scalar and series)
+ *
+ * - `value_type`: Type of metric values - 'scalar', 'series', or 'all' (required)
+ * - `format`: Return format - 'json' (default) or 'csv'
+ */
export const executionsListMetricValuesOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -468,6 +585,12 @@ export const executionsListMetricValuesOptions = (options: Options) => createQueryKey('executionsExecutionArchive', options);
+/**
+ * Execution Archive
+ * Stream a tar.gz archive of the execution results
+ *
+ * The archive is created on-the-fly and streamed directly to the client.
+ */
export const executionsExecutionArchiveOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -483,8 +606,92 @@ export const executionsExecutionArchiveOptions = (options: Options) => createQueryKey('explorerListCollections', options);
+
+/**
+ * List Collections
+ */
+export const explorerListCollectionsOptions = (options?: Options) => {
+ return queryOptions({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await explorerListCollections({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: explorerListCollectionsQueryKey(options)
+ });
+};
+
+export const explorerGetCollectionQueryKey = (options: Options) => createQueryKey('explorerGetCollection', options);
+
+/**
+ * Get Collection
+ */
+export const explorerGetCollectionOptions = (options: Options) => {
+ return queryOptions({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await explorerGetCollection({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: explorerGetCollectionQueryKey(options)
+ });
+};
+
+export const explorerListThemesQueryKey = (options?: Options) => createQueryKey('explorerListThemes', options);
+
+/**
+ * List Themes
+ */
+export const explorerListThemesOptions = (options?: Options) => {
+ return queryOptions({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await explorerListThemes({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: explorerListThemesQueryKey(options)
+ });
+};
+
+export const explorerGetThemeQueryKey = (options: Options) => createQueryKey('explorerGetTheme', options);
+
+/**
+ * Get Theme
+ */
+export const explorerGetThemeOptions = (options: Options) => {
+ return queryOptions({
+ queryFn: async ({ queryKey, signal }) => {
+ const { data } = await explorerGetTheme({
+ ...options,
+ ...queryKey[0],
+ signal,
+ throwOnError: true
+ });
+ return data;
+ },
+ queryKey: explorerGetThemeQueryKey(options)
+ });
+};
+
export const resultsGetResultQueryKey = (options: Options) => createQueryKey('resultsGetResult', options);
+/**
+ * Get Result
+ * Fetch a result
+ */
export const resultsGetResultOptions = (options: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
@@ -502,6 +709,9 @@ export const resultsGetResultOptions = (options: Options)
export const utilsHealthCheckQueryKey = (options?: Options) => createQueryKey('utilsHealthCheck', options);
+/**
+ * Health Check
+ */
export const utilsHealthCheckOptions = (options?: Options) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
diff --git a/frontend/src/client/schemas.gen.ts b/frontend/src/client/schemas.gen.ts
index 14178c4..2dc5c55 100644
--- a/frontend/src/client/schemas.gen.ts
+++ b/frontend/src/client/schemas.gen.ts
@@ -1,5 +1,551 @@
// This file is auto-generated by @hey-api/openapi-ts
+export const AFTCollectionCardSchema = {
+ properties: {
+ title: {
+ type: 'string',
+ title: 'Title'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ placeholder: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Placeholder'
+ },
+ content: {
+ items: {
+ '$ref': '#/components/schemas/AFTCollectionCardContent'
+ },
+ type: 'array',
+ title: 'Content'
+ }
+ },
+ type: 'object',
+ required: ['title', 'content'],
+ title: 'AFTCollectionCard'
+} as const;
+
+export const AFTCollectionCardContentSchema = {
+ properties: {
+ type: {
+ type: 'string',
+ enum: ['box-whisker-chart', 'figure-gallery', 'series-chart', 'taylor-diagram'],
+ title: 'Type'
+ },
+ provider: {
+ type: 'string',
+ title: 'Provider'
+ },
+ diagnostic: {
+ type: 'string',
+ title: 'Diagnostic'
+ },
+ title: {
+ type: 'string',
+ title: 'Title'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ interpretation: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Interpretation'
+ },
+ span: {
+ anyOf: [
+ {
+ type: 'integer',
+ enum: [1, 2]
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Span'
+ },
+ placeholder: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Placeholder'
+ },
+ metric_units: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Metric Units'
+ },
+ clip_min: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Clip Min'
+ },
+ clip_max: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Clip Max'
+ },
+ y_min: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Y Min'
+ },
+ y_max: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Y Max'
+ },
+ show_zero_line: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Show Zero Line'
+ },
+ symmetrical_axes: {
+ anyOf: [
+ {
+ type: 'boolean'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Symmetrical Axes'
+ },
+ reference_stddev: {
+ anyOf: [
+ {
+ type: 'number'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Reference Stddev'
+ },
+ label_template: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Label Template'
+ },
+ other_filters: {
+ anyOf: [
+ {
+ additionalProperties: {
+ type: 'string'
+ },
+ type: 'object'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Other Filters'
+ },
+ grouping_config: {
+ anyOf: [
+ {
+ '$ref': '#/components/schemas/AFTCollectionGroupingConfig'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ },
+ reference_datasets: {
+ anyOf: [
+ {
+ items: {
+ '$ref': '#/components/schemas/ReferenceDatasetLink'
+ },
+ type: 'array'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Reference Datasets'
+ }
+ },
+ type: 'object',
+ required: ['type', 'provider', 'diagnostic', 'title'],
+ title: 'AFTCollectionCardContent'
+} as const;
+
+export const AFTCollectionContentSchema = {
+ properties: {
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ short_description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Short Description'
+ },
+ why_it_matters: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Why It Matters'
+ },
+ takeaway: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Takeaway'
+ },
+ plain_language: {
+ anyOf: [
+ {
+ '$ref': '#/components/schemas/AFTCollectionPlainLanguage'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ }
+ },
+ type: 'object',
+ title: 'AFTCollectionContent'
+} as const;
+
+export const AFTCollectionDetailSchema = {
+ properties: {
+ id: {
+ type: 'string',
+ title: 'Id'
+ },
+ name: {
+ type: 'string',
+ title: 'Name'
+ },
+ theme: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Theme'
+ },
+ endorser: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Endorser'
+ },
+ version_control: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Version Control'
+ },
+ reference_dataset: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Reference Dataset'
+ },
+ provider_link: {
+ anyOf: [
+ {
+ type: 'string',
+ maxLength: 2083,
+ minLength: 1,
+ format: 'uri'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Provider Link'
+ },
+ content: {
+ anyOf: [
+ {
+ '$ref': '#/components/schemas/AFTCollectionContent'
+ },
+ {
+ type: 'null'
+ }
+ ]
+ },
+ diagnostics: {
+ items: {
+ '$ref': '#/components/schemas/AFTCollectionDiagnosticLink'
+ },
+ type: 'array',
+ title: 'Diagnostics'
+ },
+ explorer_cards: {
+ items: {
+ '$ref': '#/components/schemas/AFTCollectionCard'
+ },
+ type: 'array',
+ title: 'Explorer Cards'
+ }
+ },
+ type: 'object',
+ required: ['id', 'name', 'diagnostics', 'explorer_cards'],
+ title: 'AFTCollectionDetail'
+} as const;
+
+export const AFTCollectionDiagnosticLinkSchema = {
+ properties: {
+ provider_slug: {
+ type: 'string',
+ title: 'Provider Slug'
+ },
+ diagnostic_slug: {
+ type: 'string',
+ title: 'Diagnostic Slug'
+ },
+ provider_link: {
+ anyOf: [
+ {
+ type: 'string',
+ maxLength: 2083,
+ minLength: 1,
+ format: 'uri'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Provider Link'
+ }
+ },
+ type: 'object',
+ required: ['provider_slug', 'diagnostic_slug'],
+ title: 'AFTCollectionDiagnosticLink'
+} as const;
+
+export const AFTCollectionGroupingConfigSchema = {
+ properties: {
+ group_by: {
+ type: 'string',
+ title: 'Group By'
+ },
+ hue: {
+ type: 'string',
+ title: 'Hue'
+ },
+ style: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Style'
+ }
+ },
+ type: 'object',
+ required: ['group_by', 'hue'],
+ title: 'AFTCollectionGroupingConfig'
+} as const;
+
+export const AFTCollectionPlainLanguageSchema = {
+ properties: {
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ why_it_matters: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Why It Matters'
+ },
+ takeaway: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Takeaway'
+ }
+ },
+ type: 'object',
+ title: 'AFTCollectionPlainLanguage'
+} as const;
+
+export const AFTCollectionSummarySchema = {
+ properties: {
+ id: {
+ type: 'string',
+ title: 'Id'
+ },
+ name: {
+ type: 'string',
+ title: 'Name'
+ },
+ theme: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Theme'
+ },
+ endorser: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Endorser'
+ },
+ card_count: {
+ type: 'integer',
+ title: 'Card Count'
+ }
+ },
+ type: 'object',
+ required: ['id', 'name', 'card_count'],
+ title: 'AFTCollectionSummary'
+} as const;
+
export const AFTDiagnosticDetailSchema = {
properties: {
id: {
@@ -598,13 +1144,10 @@ export const ExecutionSchema = {
},
type: 'array',
title: 'Outputs'
- },
- execution_group: {
- '$ref': '#/components/schemas/ExecutionGroupSummary'
}
},
type: 'object',
- required: ['id', 'dataset_hash', 'dataset_count', 'successful', 'retracted', 'created_at', 'updated_at', 'outputs', 'execution_group'],
+ required: ['id', 'dataset_hash', 'dataset_count', 'successful', 'retracted', 'created_at', 'updated_at', 'outputs'],
title: 'Execution'
} as const;
@@ -624,7 +1167,7 @@ export const ExecutionGroupSchema = {
},
executions: {
items: {
- '$ref': '#/components/schemas/ExecutionSummary'
+ '$ref': '#/components/schemas/Execution'
},
type: 'array',
title: 'Executions'
@@ -632,7 +1175,7 @@ export const ExecutionGroupSchema = {
latest_execution: {
anyOf: [
{
- '$ref': '#/components/schemas/ExecutionSummary'
+ '$ref': '#/components/schemas/Execution'
},
{
type: 'null'
@@ -678,59 +1221,6 @@ export const ExecutionGroupSchema = {
title: 'ExecutionGroup'
} as const;
-export const ExecutionGroupSummarySchema = {
- properties: {
- id: {
- type: 'integer',
- title: 'Id'
- },
- key: {
- type: 'string',
- title: 'Key'
- },
- dirty: {
- type: 'boolean',
- title: 'Dirty'
- },
- selectors: {
- additionalProperties: {
- items: {
- prefixItems: [
- {
- type: 'string'
- },
- {
- type: 'string'
- }
- ],
- type: 'array',
- maxItems: 2,
- minItems: 2
- },
- type: 'array'
- },
- type: 'object',
- title: 'Selectors'
- },
- diagnostic: {
- '$ref': '#/components/schemas/DiagnosticSummary'
- },
- created_at: {
- type: 'string',
- format: 'date-time',
- title: 'Created At'
- },
- updated_at: {
- type: 'string',
- format: 'date-time',
- title: 'Updated At'
- }
- },
- type: 'object',
- required: ['id', 'key', 'dirty', 'selectors', 'diagnostic', 'created_at', 'updated_at'],
- title: 'ExecutionGroupSummary'
-} as const;
-
export const ExecutionOutputSchema = {
properties: {
id: {
@@ -823,51 +1313,6 @@ export const ExecutionStatsSchema = {
description: 'Statistics for execution groups and their success rates.'
} as const;
-export const ExecutionSummarySchema = {
- properties: {
- id: {
- type: 'integer',
- title: 'Id'
- },
- dataset_hash: {
- type: 'string',
- title: 'Dataset Hash'
- },
- dataset_count: {
- type: 'integer',
- title: 'Dataset Count'
- },
- successful: {
- type: 'boolean',
- title: 'Successful'
- },
- retracted: {
- type: 'boolean',
- title: 'Retracted'
- },
- created_at: {
- type: 'string',
- format: 'date-time',
- title: 'Created At'
- },
- updated_at: {
- type: 'string',
- format: 'date-time',
- title: 'Updated At'
- },
- outputs: {
- items: {
- '$ref': '#/components/schemas/ExecutionOutput'
- },
- type: 'array',
- title: 'Outputs'
- }
- },
- type: 'object',
- required: ['id', 'dataset_hash', 'dataset_count', 'successful', 'retracted', 'created_at', 'updated_at', 'outputs'],
- title: 'ExecutionSummary'
-} as const;
-
export const FacetSchema = {
properties: {
key: {
@@ -1300,6 +1745,82 @@ export const SeriesValueSchema = {
This includes the dimensions, values array, index array, and index name`
} as const;
+export const ThemeDetailSchema = {
+ properties: {
+ slug: {
+ type: 'string',
+ title: 'Slug'
+ },
+ title: {
+ type: 'string',
+ title: 'Title'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ collections: {
+ items: {
+ '$ref': '#/components/schemas/AFTCollectionDetail'
+ },
+ type: 'array',
+ title: 'Collections'
+ },
+ explorer_cards: {
+ items: {
+ '$ref': '#/components/schemas/AFTCollectionCard'
+ },
+ type: 'array',
+ title: 'Explorer Cards'
+ }
+ },
+ type: 'object',
+ required: ['slug', 'title', 'collections', 'explorer_cards'],
+ title: 'ThemeDetail'
+} as const;
+
+export const ThemeSummarySchema = {
+ properties: {
+ slug: {
+ type: 'string',
+ title: 'Slug'
+ },
+ title: {
+ type: 'string',
+ title: 'Title'
+ },
+ description: {
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
+ title: 'Description'
+ },
+ collection_count: {
+ type: 'integer',
+ title: 'Collection Count'
+ },
+ card_count: {
+ type: 'integer',
+ title: 'Card Count'
+ }
+ },
+ type: 'object',
+ required: ['slug', 'title', 'collection_count', 'card_count'],
+ title: 'ThemeSummary'
+} as const;
+
export const ValidationErrorSchema = {
properties: {
loc: {
@@ -1323,6 +1844,13 @@ export const ValidationErrorSchema = {
type: {
type: 'string',
title: 'Error Type'
+ },
+ input: {
+ title: 'Input'
+ },
+ ctx: {
+ type: 'object',
+ title: 'Context'
}
},
type: 'object',
diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts
index 53f1358..f5d11b6 100644
--- a/frontend/src/client/sdk.gen.ts
+++ b/frontend/src/client/sdk.gen.ts
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
-import type { Cmip7AssessmentFastTrackAftListAftDiagnosticsData, Cmip7AssessmentFastTrackAftListAftDiagnosticsResponse, Cmip7AssessmentFastTrackAftGetAftDiagnosticData, Cmip7AssessmentFastTrackAftGetAftDiagnosticResponse, Cmip7AssessmentFastTrackAftGetAftDiagnosticError, DatasetsListData, DatasetsListResponse, DatasetsListError, DatasetsGetData, DatasetsGetResponse, DatasetsGetError, DatasetsExecutionsData, DatasetsExecutionsResponse, DatasetsExecutionsError, DiagnosticsListData, DiagnosticsListResponse, DiagnosticsFacetsData, DiagnosticsFacetsResponse, DiagnosticsGetData, DiagnosticsGetResponse, DiagnosticsGetError, DiagnosticsListExecutionGroupsData, DiagnosticsListExecutionGroupsResponse, DiagnosticsListExecutionGroupsError, DiagnosticsListExecutionsData, DiagnosticsListExecutionsResponse, DiagnosticsListExecutionsError, DiagnosticsListMetricValuesData, DiagnosticsListMetricValuesResponse, DiagnosticsListMetricValuesError, ExecutionsGetExecutionStatisticsData, ExecutionsGetExecutionStatisticsResponse, ExecutionsListRecentExecutionGroupsData, ExecutionsListRecentExecutionGroupsResponse, ExecutionsListRecentExecutionGroupsError, ExecutionsGetData, ExecutionsGetResponse, ExecutionsGetError, ExecutionsExecutionData, ExecutionsExecutionResponse, ExecutionsExecutionError, ExecutionsExecutionDatasetsData, ExecutionsExecutionDatasetsResponse, ExecutionsExecutionDatasetsError, ExecutionsExecutionLogsData, ExecutionsExecutionLogsError, ExecutionsMetricBundleData, ExecutionsMetricBundleResponse, ExecutionsMetricBundleError, ExecutionsListMetricValuesData, ExecutionsListMetricValuesResponse, ExecutionsListMetricValuesError, ExecutionsExecutionArchiveData, ExecutionsExecutionArchiveError, ResultsGetResultData, ResultsGetResultError, UtilsHealthCheckData, UtilsHealthCheckResponse } from './types.gen';
+import type { Cmip7AssessmentFastTrackAftListAftDiagnosticsData, Cmip7AssessmentFastTrackAftListAftDiagnosticsResponse, Cmip7AssessmentFastTrackAftGetAftDiagnosticData, Cmip7AssessmentFastTrackAftGetAftDiagnosticResponse, Cmip7AssessmentFastTrackAftGetAftDiagnosticError, DatasetsListData, DatasetsListResponse, DatasetsListError, DatasetsGetData, DatasetsGetResponse, DatasetsGetError, DatasetsExecutionsData, DatasetsExecutionsResponse, DatasetsExecutionsError, DiagnosticsListData, DiagnosticsListResponse, DiagnosticsFacetsData, DiagnosticsFacetsResponse, DiagnosticsGetData, DiagnosticsGetResponse, DiagnosticsGetError, DiagnosticsListExecutionGroupsData, DiagnosticsListExecutionGroupsResponse, DiagnosticsListExecutionGroupsError, DiagnosticsListExecutionsData, DiagnosticsListExecutionsResponse, DiagnosticsListExecutionsError, DiagnosticsListMetricValuesData, DiagnosticsListMetricValuesResponse, DiagnosticsListMetricValuesError, ExecutionsGetExecutionStatisticsData, ExecutionsGetExecutionStatisticsResponse, ExecutionsListRecentExecutionGroupsData, ExecutionsListRecentExecutionGroupsResponse, ExecutionsListRecentExecutionGroupsError, ExecutionsGetData, ExecutionsGetResponse, ExecutionsGetError, ExecutionsExecutionData, ExecutionsExecutionResponse, ExecutionsExecutionError, ExecutionsExecutionDatasetsData, ExecutionsExecutionDatasetsResponse, ExecutionsExecutionDatasetsError, ExecutionsExecutionLogsData, ExecutionsExecutionLogsError, ExecutionsMetricBundleData, ExecutionsMetricBundleResponse, ExecutionsMetricBundleError, ExecutionsListMetricValuesData, ExecutionsListMetricValuesResponse, ExecutionsListMetricValuesError, ExecutionsExecutionArchiveData, ExecutionsExecutionArchiveError, ExplorerListCollectionsData, ExplorerListCollectionsResponse, ExplorerGetCollectionData, ExplorerGetCollectionResponse, ExplorerGetCollectionError, ExplorerListThemesData, ExplorerListThemesResponse, ExplorerGetThemeData, ExplorerGetThemeResponse, ExplorerGetThemeError, ResultsGetResultData, ResultsGetResultError, UtilsHealthCheckData, UtilsHealthCheckResponse } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options = ClientOptions & {
@@ -261,6 +261,46 @@ export const executionsExecutionArchive =
});
};
+/**
+ * List Collections
+ */
+export const explorerListCollections = (options?: Options) => {
+ return (options?.client ?? _heyApiClient).get({
+ url: '/api/v1/explorer/collections/',
+ ...options
+ });
+};
+
+/**
+ * Get Collection
+ */
+export const explorerGetCollection = (options: Options) => {
+ return (options.client ?? _heyApiClient).get({
+ url: '/api/v1/explorer/collections/{collection_id}',
+ ...options
+ });
+};
+
+/**
+ * List Themes
+ */
+export const explorerListThemes = (options?: Options) => {
+ return (options?.client ?? _heyApiClient).get({
+ url: '/api/v1/explorer/themes/',
+ ...options
+ });
+};
+
+/**
+ * Get Theme
+ */
+export const explorerGetTheme = (options: Options) => {
+ return (options.client ?? _heyApiClient).get({
+ url: '/api/v1/explorer/themes/{theme_slug}',
+ ...options
+ });
+};
+
/**
* Get Result
* Fetch a result
diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts
index 14c228a..a9ad98d 100644
--- a/frontend/src/client/types.gen.ts
+++ b/frontend/src/client/types.gen.ts
@@ -1,5 +1,84 @@
// This file is auto-generated by @hey-api/openapi-ts
+export type AftCollectionCard = {
+ title: string;
+ description?: string | null;
+ placeholder?: boolean | null;
+ content: Array;
+};
+
+export type AftCollectionCardContent = {
+ type: 'box-whisker-chart' | 'figure-gallery' | 'series-chart' | 'taylor-diagram';
+ provider: string;
+ diagnostic: string;
+ title: string;
+ description?: string | null;
+ interpretation?: string | null;
+ span?: (1 | 2) | null;
+ placeholder?: boolean | null;
+ metric_units?: string | null;
+ clip_min?: number | null;
+ clip_max?: number | null;
+ y_min?: number | null;
+ y_max?: number | null;
+ show_zero_line?: boolean | null;
+ symmetrical_axes?: boolean | null;
+ reference_stddev?: number | null;
+ label_template?: string | null;
+ other_filters?: {
+ [key: string]: string;
+ } | null;
+ grouping_config?: AftCollectionGroupingConfig | null;
+ reference_datasets?: Array | null;
+};
+
+export type AftCollectionContent = {
+ description?: string | null;
+ short_description?: string | null;
+ why_it_matters?: string | null;
+ takeaway?: string | null;
+ plain_language?: AftCollectionPlainLanguage | null;
+};
+
+export type AftCollectionDetail = {
+ id: string;
+ name: string;
+ theme?: string | null;
+ endorser?: string | null;
+ version_control?: string | null;
+ reference_dataset?: string | null;
+ provider_link?: string | null;
+ content?: AftCollectionContent | null;
+ diagnostics: Array;
+ explorer_cards: Array;
+};
+
+export type AftCollectionDiagnosticLink = {
+ provider_slug: string;
+ diagnostic_slug: string;
+ provider_link?: string | null;
+};
+
+export type AftCollectionGroupingConfig = {
+ group_by: string;
+ hue: string;
+ style?: string | null;
+};
+
+export type AftCollectionPlainLanguage = {
+ description?: string | null;
+ why_it_matters?: string | null;
+ takeaway?: string | null;
+};
+
+export type AftCollectionSummary = {
+ id: string;
+ name: string;
+ theme?: string | null;
+ endorser?: string | null;
+ card_count: number;
+};
+
export type AftDiagnosticDetail = {
id: string;
name: string;
@@ -161,30 +240,14 @@ export type Execution = {
created_at: string;
updated_at: string;
outputs: Array;
- execution_group: ExecutionGroupSummary;
};
export type ExecutionGroup = {
id: number;
key: string;
dirty: boolean;
- executions: Array;
- latest_execution: ExecutionSummary | null;
- selectors: {
- [key: string]: Array<[
- string,
- string
- ]>;
- };
- diagnostic: DiagnosticSummary;
- created_at: string;
- updated_at: string;
-};
-
-export type ExecutionGroupSummary = {
- id: number;
- key: string;
- dirty: boolean;
+ executions: Array;
+ latest_execution: Execution | null;
selectors: {
[key: string]: Array<[
string,
@@ -239,17 +302,6 @@ export type ExecutionStatsWritable = {
total_files: number;
};
-export type ExecutionSummary = {
- id: number;
- dataset_hash: string;
- dataset_count: number;
- successful: boolean;
- retracted: boolean;
- created_at: string;
- updated_at: string;
- outputs: Array;
-};
-
export type Facet = {
key: string;
values: Array;
@@ -387,10 +439,30 @@ export type SeriesValue = {
execution_id: number;
};
+export type ThemeDetail = {
+ slug: string;
+ title: string;
+ description?: string | null;
+ collections: Array;
+ explorer_cards: Array;
+};
+
+export type ThemeSummary = {
+ slug: string;
+ title: string;
+ description?: string | null;
+ collection_count: number;
+ card_count: number;
+};
+
export type ValidationError = {
loc: Array;
msg: string;
type: string;
+ input?: unknown;
+ ctx?: {
+ [key: string]: unknown;
+ };
};
export type Cmip7AssessmentFastTrackAftListAftDiagnosticsData = {
@@ -966,6 +1038,92 @@ export type ExecutionsExecutionArchiveResponses = {
200: unknown;
};
+export type ExplorerListCollectionsData = {
+ body?: never;
+ path?: never;
+ query?: never;
+ url: '/api/v1/explorer/collections/';
+};
+
+export type ExplorerListCollectionsResponses = {
+ /**
+ * Successful Response
+ */
+ 200: Array;
+};
+
+export type ExplorerListCollectionsResponse = ExplorerListCollectionsResponses[keyof ExplorerListCollectionsResponses];
+
+export type ExplorerGetCollectionData = {
+ body?: never;
+ path: {
+ collection_id: string;
+ };
+ query?: never;
+ url: '/api/v1/explorer/collections/{collection_id}';
+};
+
+export type ExplorerGetCollectionErrors = {
+ /**
+ * Validation Error
+ */
+ 422: HttpValidationError;
+};
+
+export type ExplorerGetCollectionError = ExplorerGetCollectionErrors[keyof ExplorerGetCollectionErrors];
+
+export type ExplorerGetCollectionResponses = {
+ /**
+ * Successful Response
+ */
+ 200: AftCollectionDetail;
+};
+
+export type ExplorerGetCollectionResponse = ExplorerGetCollectionResponses[keyof ExplorerGetCollectionResponses];
+
+export type ExplorerListThemesData = {
+ body?: never;
+ path?: never;
+ query?: never;
+ url: '/api/v1/explorer/themes/';
+};
+
+export type ExplorerListThemesResponses = {
+ /**
+ * Successful Response
+ */
+ 200: Array;
+};
+
+export type ExplorerListThemesResponse = ExplorerListThemesResponses[keyof ExplorerListThemesResponses];
+
+export type ExplorerGetThemeData = {
+ body?: never;
+ path: {
+ theme_slug: string;
+ };
+ query?: never;
+ url: '/api/v1/explorer/themes/{theme_slug}';
+};
+
+export type ExplorerGetThemeErrors = {
+ /**
+ * Validation Error
+ */
+ 422: HttpValidationError;
+};
+
+export type ExplorerGetThemeError = ExplorerGetThemeErrors[keyof ExplorerGetThemeErrors];
+
+export type ExplorerGetThemeResponses = {
+ /**
+ * Successful Response
+ */
+ 200: ThemeDetail;
+};
+
+export type ExplorerGetThemeResponse = ExplorerGetThemeResponses[keyof ExplorerGetThemeResponses];
+
export type ResultsGetResultData = {
body?: never;
path: {
diff --git a/frontend/src/components/app/welcomeModal.tsx b/frontend/src/components/app/welcomeModal.tsx
index 64a2d72..9872502 100644
--- a/frontend/src/components/app/welcomeModal.tsx
+++ b/frontend/src/components/app/welcomeModal.tsx
@@ -75,7 +75,7 @@ export function WelcomeModal() {
The results presented here focus on CMIP6 datasets, but this will be
- updated as new CMIP7 datasets become available We have some example
+ updated as new CMIP7 datasets become available. We have some example
figures available in the Data Explorer{" "}
or you can browse the full{" "}
Diagnostic Catalog.
diff --git a/frontend/src/components/diagnostics/ensembleChart.tsx b/frontend/src/components/diagnostics/ensembleChart.tsx
index 475db20..d0d6f4f 100644
--- a/frontend/src/components/diagnostics/ensembleChart.tsx
+++ b/frontend/src/components/diagnostics/ensembleChart.tsx
@@ -22,6 +22,48 @@ import {
import useMousePositionAndWidth from "@/hooks/useMousePosition";
import { createScaledTickFormatter } from "../execution/values/series/utils";
+// Well-known category orderings for common climate dimensions
+const KNOWN_CATEGORY_ORDERS: Record = {
+ // Meteorological seasons
+ season: ["DJF", "MAM", "JJA", "SON"],
+};
+
+/**
+ * Sort chart categories using a known ordering if one exists,
+ * otherwise preserve the original order.
+ */
+function sortCategories(
+ items: T[],
+ categoryOrder?: string[],
+): T[] {
+ const order = categoryOrder;
+ if (!order) {
+ // Auto-detect: check if all category names match a known ordering
+ const names = new Set(items.map((item) => item.name));
+ for (const knownOrder of Object.values(KNOWN_CATEGORY_ORDERS)) {
+ if (
+ names.size <= knownOrder.length &&
+ [...names].every((n) => knownOrder.includes(n))
+ ) {
+ return [...items].sort(
+ (a, b) => knownOrder.indexOf(a.name) - knownOrder.indexOf(b.name),
+ );
+ }
+ }
+ return items;
+ }
+
+ return [...items].sort((a, b) => {
+ const aIdx = order.indexOf(a.name);
+ const bIdx = order.indexOf(b.name);
+ // Items not in the order go to the end, preserving relative order
+ if (aIdx === -1 && bIdx === -1) return 0;
+ if (aIdx === -1) return 1;
+ if (bIdx === -1) return -1;
+ return aIdx - bIdx;
+ });
+}
+
// Color palette for different groups
const COLORS = [
"#8884d8",
@@ -51,6 +93,8 @@ interface EnsembleChartProps {
symmetricalAxes?: boolean;
yMin?: number;
yMax?: number;
+ /** Explicit category ordering for the x-axis (e.g., ["DJF", "MAM", "JJA", "SON"]) */
+ categoryOrder?: string[];
}
interface GroupStatistics {
@@ -93,6 +137,7 @@ export const EnsembleChart = ({
symmetricalAxes,
yMin,
yMax,
+ categoryOrder,
}: EnsembleChartProps) => {
const { mousePosition, windowSize } = useMousePositionAndWidth();
const [highlightedPoint, setHighlightedPoint] = useState<{
@@ -208,19 +253,22 @@ export const EnsembleChart = ({
},
);
+ // Sort categories using explicit order or well-known orderings (e.g., seasons)
+ const sortedChartData = sortCategories(chartData, categoryOrder);
+
// Get all unique group names for rendering multiple bars
const allGroupNames = useMemo(() => {
const names = new Set();
- chartData.forEach((d) => {
+ sortedChartData.forEach((d) => {
Object.keys(d.groups).forEach((groupName) => {
names.add(groupName);
});
});
return Array.from(names).sort();
- }, [chartData]);
+ }, [sortedChartData]);
const scale = useMemo(() => {
- const allFiniteValues = chartData
+ const allFiniteValues = sortedChartData
.flatMap((d) =>
Object.values(d.groups)
.filter((group) => group !== null)
@@ -261,7 +309,7 @@ export const EnsembleChart = ({
yMax !== undefined ? yMax : maxVal + padding,
])
.nice();
- }, [chartData, symmetricalAxes, yMin, yMax]);
+ }, [sortedChartData, symmetricalAxes, yMin, yMax]);
const yDomain = scale.domain() as [number, number];
// Get color for a group
@@ -272,18 +320,22 @@ export const EnsembleChart = ({
const fmt = valueFormatter ?? createScaledTickFormatter(yDomain);
// Hide x-axis when there are too many items (threshold: 6)
- const shouldShowXAxis = chartData.length <= 6;
+ const shouldShowXAxis = sortedChartData.length <= 6;
// Adjust spacing based on number of items
// make them closer together when there are many
const barCategoryGap =
- chartData.length > 20 ? "2%" : chartData.length > 10 ? "5%" : "20%";
+ sortedChartData.length > 20
+ ? "2%"
+ : sortedChartData.length > 10
+ ? "5%"
+ : "20%";
return (
diff --git a/frontend/src/components/diagnostics/executionsTable.tsx b/frontend/src/components/diagnostics/executionsTable.tsx
index 1d47267..894da9d 100644
--- a/frontend/src/components/diagnostics/executionsTable.tsx
+++ b/frontend/src/components/diagnostics/executionsTable.tsx
@@ -6,7 +6,7 @@ import {
} from "@tanstack/react-table";
import { format } from "date-fns";
import { SquareArrowOutUpRight } from "lucide-react";
-import type { ExecutionSummary } from "@/client";
+import type { Execution } from "@/client";
import { DataTable } from "@/components/dataTable/dataTable";
import { Badge } from "@/components/ui/badge";
import {
@@ -18,13 +18,13 @@ import {
} from "@/components/ui/card";
import { Route } from "@/routes/_app/executions.$groupId/index";
-const columnHelper = createColumnHelper();
+const columnHelper = createColumnHelper();
function OpenCell({
row: {
original: { id },
},
-}: CellContext) {
+}: CellContext) {
const navigate = useNavigate({ from: Route.fullPath });
return (