climate-pipeline¶
The modules appear in pipeline order: normalize a raw source, fetch periods from it, ingest them one commit at a time, derive indices, and publish.
normalize
¶
Normalizing source data into the service's own conventions.
Real sources disagree about everything: dimension names (lat/lon vs
latitude/longitude), units (Kelvin vs Celsius, m vs mm), and axis
direction (south-up vs north-up). open-climate-service resolves all of that at
ingest so that every stored dataset looks identical to everything downstream,
which is what makes its API uniform across sources.
This module is that step: source in, canonical (time, y, x) in degrees
Celsius or millimetres out.
Functions:¶
rename_dims(ds)
¶
Rename whatever the source calls its axes to (time, y, x).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset using any of the recognized dimension spellings. |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
The dataset with its dimensions and coordinates renamed. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a spatial or time axis cannot be identified. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/normalize.py
orient_north_up(ds)
¶
Ensure the y axis descends, so row 0 is the northernmost.
GeoZarr places a raster with an affine whose y step is negative for a north-up grid. A south-up source silently renders upside down, so the orientation is fixed here rather than trusted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
The dataset, reversed along y if it was ascending. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/normalize.py
convert_units(ds)
¶
Convert known variables to the service's canonical units.
Kelvin becomes Celsius and metres of precipitation become millimetres.
Attributes are inert in xarray, so the units attribute is rewritten by
hand -- forgetting that is how a dataset ends up labelled K while
holding Celsius.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset whose variables carry a |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
A new dataset with converted values and corrected unit attributes. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/normalize.py
sort_time(ds)
¶
Sort along time and drop duplicate timestamps, keeping the last.
A re-fetched period arrives with timestamps the store may already hold. Appending it blindly produces a store with duplicate coordinates that cannot be indexed sanely, so duplicates are resolved here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
The dataset sorted by time with unique timestamps. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/normalize.py
normalize(ds)
¶
Run the full normalization pipeline on a source dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Raw source data in whatever conventions it arrived with. |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
A dataset with dims |
Dataset
|
units, and a sorted, duplicate-free time axis. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dataset's axes cannot be identified. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/normalize.py
sources
¶
Synthetic data sources, deliberately messy.
Each source stands in for a real one and arrives in its own conventions, so the normalization step has something to actually do. Periods are enumerated the way open-climate-service's streaming ingest does it: the plugin lists the periods it can supply, and the framework fetches them one at a time.
Classes¶
Period
dataclass
¶
One ingestable period.
Attributes:
| Name | Type | Description |
|---|---|---|
period_id |
str
|
Stable identifier, such as |
start |
str
|
First day of the period, as an ISO date string. |
days |
int
|
Number of daily steps in the period. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/sources.py
Functions:¶
enumerate_periods(year=2024, months=6)
¶
List the monthly periods a source can supply.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
year
|
int
|
Calendar year to enumerate. |
2024
|
months
|
int
|
How many months from January to include; must be 1..12. |
6
|
Returns:
| Name | Type | Description |
|---|---|---|
One |
list[Period]
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If months is outside 1..12. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/sources.py
fetch_temperature(period, ny=24, nx=24, seed=0)
¶
Fetch one period of temperature, in a deliberately awkward source format.
This source publishes Kelvin on a south-up lat/lon grid -- exactly
the kind of thing normalization exists to fix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
Period
|
The period to fetch. |
required |
ny
|
int
|
Grid height. |
24
|
nx
|
int
|
Grid width. |
24
|
seed
|
int
|
Base seed; the period id is mixed in so periods differ. |
0
|
Returns:
| Type | Description |
|---|---|
Dataset
|
A dataset with variable |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/sources.py
fetch_precipitation(period, ny=24, nx=24, seed=1)
¶
Fetch one period of precipitation, in metres on a south-up grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
period
|
Period
|
The period to fetch. |
required |
ny
|
int
|
Grid height. |
24
|
nx
|
int
|
Grid width. |
24
|
seed
|
int
|
Base seed; the period id is mixed in so periods differ. |
1
|
Returns:
| Type | Description |
|---|---|
Dataset
|
A dataset with variable |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/sources.py
ingest
¶
Streaming ingest: one period at a time, committed as it lands.
This is the heart of the open-climate-service ingestion contract. The source enumerates periods; the framework fetches each one, normalizes it, appends it to the store, and commits. Because every period is its own transaction, an interrupted ingest leaves a store that is complete up to the last commit -- never half a period -- and resuming is a matter of asking the store what it already holds.
Classes¶
IngestReport
dataclass
¶
What one ingest run did.
Attributes:
| Name | Type | Description |
|---|---|---|
ingested |
list[str]
|
Period ids written during this run. |
skipped |
list[str]
|
Period ids already present and therefore skipped. |
failed |
dict[str, str]
|
Period ids that raised, with the error message. |
snapshots |
dict[str, str]
|
Snapshot id produced by each ingested period. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
Functions:¶
chunking_for(ds)
¶
Choose chunk sizes for a dataset, capping the spatial dimensions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
The dataset about to be written. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
A chunk mapping suitable for |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
committed_periods(repo, *, period_type='month')
¶
Return the period ids already committed to a store.
The store's time coordinate is authoritative: whatever is committed is what exists, regardless of what any external bookkeeping claims. That is what makes resume safe after a crash.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo
|
Any
|
An icechunk repository. |
required |
period_type
|
str
|
Granularity of the period ids; only |
'month'
|
Returns:
| Type | Description |
|---|---|
set[str]
|
The set of period ids present, empty if the store has no data yet. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If period_type is not supported. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
ingest_period(repo, period, fetch)
¶
Fetch, normalize, and commit a single period.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo
|
Any
|
An icechunk repository to write into. |
required |
period
|
Period
|
The period to ingest. |
required |
fetch
|
Fetcher
|
Callable returning the raw source dataset for the period. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The snapshot id of the commit. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
ingest(repo, periods, fetch, *, resume=True, stop_after=None)
¶
Ingest a list of periods, one commit each.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo
|
Any
|
An icechunk repository to write into. |
required |
periods
|
list[Period]
|
Periods to ingest, in chronological order. |
required |
fetch
|
Fetcher
|
Callable returning the raw source dataset for a period. |
required |
resume
|
bool
|
When True, skip periods the store already holds. |
True
|
stop_after
|
int | None
|
Stop once this many periods have been ingested, simulating an interrupted run. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
An |
IngestReport
|
class: |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
store_path(base, dataset_id)
¶
Return the on-disk path for a dataset's store.
Mirrors the open-climate-service layout,
{data_dir}/downloads/{dataset_id}.icechunk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
Path | str
|
The instance's data directory. |
required |
dataset_id
|
str
|
Public identifier of the dataset. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The store path. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/ingest.py
indices
¶
Climate indices: the derived products a service actually publishes.
Raw temperature and rainfall are inputs, not answers. What a health ministry or planning office asks for is "how many hot days", "was this month unusually dry", "when does the rainy season start" -- indices computed from the stored series. These are the shape of the processes open-climate-service exposes over openEO, implemented directly here so the arithmetic is visible.
Functions:¶
climatological_normal(ds, variable='t2m')
¶
Return the per-month mean over all years: the climatological normal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a daily |
required |
variable
|
str
|
Which variable to summarize. |
't2m'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
A |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
monthly_anomaly(ds, variable='t2m')
¶
Return each timestep's departure from its month's normal.
Anomalies, not absolute values, are what make two places or two years comparable -- which is why nearly every published climate product is one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a daily |
required |
variable
|
str
|
Which variable to anomalize. |
't2m'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
An array shaped like the input, in the same units. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
hot_days(ds, threshold=30.0, variable='t2m')
¶
Count days per month above a temperature threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with daily temperature in degrees Celsius. |
required |
threshold
|
float
|
The temperature above which a day counts as hot. |
30.0
|
variable
|
str
|
Which variable to threshold. |
't2m'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
A |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
wet_days(ds, threshold=1.0, variable='tp')
¶
Count days per month with rainfall at or above a threshold.
One millimetre is the conventional cutoff for a "wet day": below it, the reading is indistinguishable from dew or gauge noise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with daily precipitation in millimetres. |
required |
threshold
|
float
|
Millimetres at or above which a day counts as wet. |
1.0
|
variable
|
str
|
Which variable to threshold. |
'tp'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
A |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
monthly_total(ds, variable='tp')
¶
Sum a variable per month -- the right reduction for rainfall.
Temperature is intensive and gets averaged; rainfall is extensive and gets summed. Using the wrong one is a classic and silent error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a daily |
required |
variable
|
str
|
Which variable to total. |
'tp'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
A |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
spi_like(ds, variable='tp')
¶
Standardize monthly rainfall totals against their own month's history.
A simplified standardized precipitation index: for each calendar month, subtract that month's long-run mean and divide by its standard deviation, so -2 means "far drier than this month usually is". The real SPI fits a gamma distribution first; the standardization idea is the same.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with daily precipitation. |
required |
variable
|
str
|
Which variable to standardize. |
'tp'
|
Returns:
| Type | Description |
|---|---|
DataArray
|
A |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the variable is absent. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
pyramid_levels(ds, levels=3)
¶
Build coarser resolutions by repeated 2x2 mean downsampling.
This is how open-climate-service builds the multiscale GeoZarr pyramid a map viewer needs: level 0 is full resolution, each subsequent level halves both spatial dimensions so a zoomed-out tile reads a small array instead of the whole grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with |
required |
levels
|
int
|
Total number of levels including level 0; must be at least 1. |
3
|
Returns:
| Type | Description |
|---|---|
list[Dataset]
|
A list of datasets, coarsest last. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If levels is less than 1. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/indices.py
publish
¶
Publishing: GeoZarr attributes and STAC metadata.
Storing the data is not the same as publishing it. A client that finds this store needs to know where on Earth the grid sits, in which CRS, what time range it covers, and what the variables mean. GeoZarr answers the first two with root attributes; STAC answers the rest with a collection document that a catalogue can index.
Functions:¶
grid_transform(ds)
¶
Return the affine transform placing a north-up grid on Earth.
The six values are [stepX, rotX, originX, rotY, stepY, originY] with
the origin on the OUTER EDGE of the first cell -- pixel registration, not
cell centres -- and a negative y step for a north-up grid. Getting the
half-cell offset wrong shifts every rendered tile by half a pixel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
The six affine coefficients. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the grid has fewer than two cells on an axis. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/publish.py
bounding_box(ds)
¶
Return [west, south, east, north] covering the grid's outer edges.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
The bounding box in the stored CRS. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the grid has fewer than two cells on an axis. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/publish.py
geozarr_attrs(ds)
¶
Build the GeoZarr root attributes for a dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A normalized dataset with dims |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The attribute mapping to write at the store root. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the grid is too small to place. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/publish.py
temporal_extent(ds)
¶
Return the ISO 8601 start and end of a dataset's time axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
A dataset with a |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
A two-element list of ISO timestamps. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dataset has no time values. |
Source code in climate-pipeline/src/ocs_stack_climate_pipeline/publish.py
stac_collection(ds, dataset_id, *, title=None, description='', zarr_href=None, now=None)
¶
Build a STAC Collection document describing a published dataset.
STAC is how a client discovers what an instance holds without knowing anything about its internals: one document per dataset, with spatial and temporal extent, variable summaries, and a link to the actual store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
The published dataset. |
required |
dataset_id
|
str
|
Stable public identifier, used as the collection id. |
required |
title
|
str | None
|
Human-readable title; defaults to the id. |
None
|
description
|
str
|
Longer prose description. |
''
|
zarr_href
|
str | None
|
URL where the store is served, if it is served. |
None
|
now
|
datetime | None
|
Timestamp to record as the publication time; defaults to now. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A STAC Collection as a plain dict, ready to serialize as JSON. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dataset lacks the extents STAC requires. |