Climate Risk Commons crc-sdk Install

An open framework for physical climate risk.

Climate Risk Commons, or CRC, turns hazard curves, asset locations and damage functions into comparable, auditable risk numbers. It runs on a laptop, over open data, in a pipeline you can read end to end.

uv add crc-sdk
python -m pip install crc-sdk
git clone https://github.com/RiskThinking/crc-docs
cd crc-docs && uv sync

Installing crc-sdk pulls in crc-framework, the Rust computation core. Both are AGPL-3.0-or-later.

A hazard raster resolved to H3 cells. cell_index is the join key between hazard curves and your assets.

What you can install today

The standard is no longer only a specification. Three pieces are published, versioned and runnable.

PieceWhat it isVersion
crc-frameworkRust computation core with typed Python bindings. Distributions, explicit curve fitting, impact transforms, microscores, VaR and CVaR, H3 geography lookups.See PyPI
crc-sdkThe higher-level Python interface: DuckDB and Arrow query engine, storage providers, hazard ingest, geometry and H3 tooling, portfolio workflows.See PyPI
crc-docsWorked notebooks and headless pipeline twins. The narrative entry point, and the fastest way to see a whole analysis at once.main

The shape of an analysis

Every analysis in the framework is the same three inputs meeting in one place. Learning where each one lives is most of the learning curve.

InputWhat it looks like in codeWhere it comes from
Climate hazard dataA canonical Parquet file of fitted curves, one row per hazard, horizon, pathway and H3 cellIngested from open rasters, or supplied by any conforming provider
Physical assetsAn Arrow table or Parquet file with an id and either coordinates or an H3 cell_indexYour portfolio, loan book or asset register
Impact functionsA callable or a registry-backed transform mapping exposure to damageThe open starter library, the registry, or one you write

The output is one row per asset, hazard, horizon and pathway, with a value column for each return period you asked for, and metadata recording exactly how each number was produced.

Where to go next

Quickstart

Clone the docs repo and get flood depths for a two-asset portfolio. Runs fully offline on checked-in fixtures, no cloud credentials.

Probability and distributions

Read this before you trust a number. Return periods, non-exceedance probability, and why a 100-year event is queried at q=0.99.

Learning path

Seven notebooks in two tracks, each with a headless pipeline twin. The ordered portfolio track is the one to start with.

Troubleshooting

Resolve installation issues, missing extras, multiprocessing errors and extrapolation warnings.

Names you will meet

A few names recur across these docs, the repositories and the project's public writing. This is what each one covers.

Climate Risk Commons
The whole of it: the specification, the reference implementation, the community that maintains them and the not-for-profit that holds them. Shortened to CRC, which is where every package name comes from.
The CRC specification
The standard itself. Input contracts for climate hazard data, physical assets and impact functions, plus the canonical hazard schema, the output model and the conformance rules.
Reference pipeline
An end-to-end implementation of the spec. The runnable scripts live in crc-docs/pipelines/ and are built on crc-sdk.
crc_framework
The import name of the computation core. Distributions and metrics live here.
crc_sdk
The import name of the SDK. Data access, ingest and workflows live here, and it re-exports the stable framework API through crc_sdk.core, .fitting and .impacts.
Climate Digital Twin
RiskThinking.AI's commercial high-fidelity data and enterprise platform. Separate from these packages, and reached through a commercial agreement.

Getting started / Installation

Installation

Baseline install is one command. The extras exist because format adapters are heavy and most callers need none of them.

Pick an entry point

There are two reasonable ways in, and they answer different questions.

Run the worked examples

Best if you want to see a complete analysis before writing any code. Clone crc-docs and let uv resolve everything, including the notebook stack.

Add it to your own project

Best if you already know which surface you need. Install crc-sdk and add extras as the import errors tell you to.

git clone https://github.com/RiskThinking/crc-docs
cd crc-docs
uv sync

# notebooks expect notebooks/ as the working directory,
# so their data/ cache lands next to them
uv run jupyter lab notebooks/

# pipelines run from the repo root
uv run python pipelines/asset_portfolio_pipeline.py
uv add crc-sdk
# or, with the adapters for OS-Climate Zarr and H3 geometry
uv add "crc-sdk[zarr,geometry]"
# distributions, fitting and risk metrics, no data access layer
python -m pip install crc-framework

What comes in the box

DuckDB, Arrow, psutil and the remote-storage transports (fsspec, s3fs, gcsfs) are baseline dependencies, not extras. Every connector and workflow in the SDK is built on that stack, so gating it would just move the same install onto every real caller.

Everything else is a specific format adapter or a pure-geometry dependency. Importing crc_sdk or any subpackage never needs more than the baseline.

ExtraAddsYou need it for
zarrzarrOSClimateProvider and ZarrRaster — OS-Climate Zarr raster ingest
rasterrasterioGeoTiffRaster — GeoTIFF and COG ingest, streamed through GDAL's VSI layer
netcdfh5netcdf, h5pyNetCDFRaster — NetCDF and CF ingest
geometryh3, h3ronpy, shapelyH3Indexer, intersecting_cells, cell_polygon, vectorised batch H3 over Arrow, raster-to-H3 sampling
agricultureicechunk, Zarr v3, pyprojUSDA CDL Icechunk scans. Check the Python and platform guidance for extra requirements
testmypy, pytest, ruffDevelopment only

Extras announce themselves

Every function that needs an extra-gated dependency imports it lazily and raises an ImportError naming the extra to install. You do not have to guess up front: install the baseline, run your code, and add what it asks for.

One dependency pip cannot give you

PMTiles generation shells out to tippecanoe. Both tippecanoe and tile-join must be on PATH; they are expected to be part of your runtime image rather than a Python install, so there is no extra for them.

from crc_sdk.geometry.pmtiles import require_tippecanoe, require_tile_join

require_tippecanoe()   # raises with install instructions if missing
require_tile_join()

Platforms and Python versions

We recommend Python 3.12 or later for new environments. Choose a version supported by your selected packages and extras.

For current Python requirements and prebuilt wheels, check crc-sdk on PyPI and crc-framework on PyPI. Wheel availability depends on your Python version, operating system and architecture.

A compatible prebuilt wheel needs no Rust toolchain. For source builds and extra-specific dependencies, follow the SDK and framework instructions on GitHub.

Verify the install

python -c "import crc_framework; print(crc_framework.__doc__)"

Then check that the SDK's engine layer imports and that the framework API is reachable through it:

import crc_sdk
from crc_sdk.workflows import HazardDataset
from crc_sdk.core import TabulatedDistribution   # re-exported from crc_framework

curve = TabulatedDistribution.from_return_periods(
    [10, 50, 100], [0.4, 1.1, 1.6], tail="upper",
)
print(curve.ppf(0.99))   # the 100-year value

Environment variables

The SDK detects sensible DuckDB limits when asked, and every one of them can be overridden. See Resources and tuning for what the defaults actually compute.

CRC_DUCKDB_THREADS
Override the detected thread count.
CRC_DUCKDB_MEMORY
Override the hard process memory limit.
CRC_DUCKDB_BYTES_PER_THREAD_GIB
Override the memory-per-thread assumption used to derive the thread count. Defaults to about 2.5 GiB.
CRC_DUCKDB_WORK_DIR
Set the spill and temp directory globally. Defaults to a stable location under the system temp directory.
CRC_DUCKDB_PROFILE
Set to 1 for detailed query profiling around the enrich and coverage stages.

Getting started / Quickstart

Quickstart

Flood depths for a small portfolio, offline, in about ten minutes. No cloud credentials, no account, no hazard data to download.

Why this path first

The asset-portfolio track reads checked-in Parquet fixtures for Cologne under fixtures/os_climate/. It is fully offline and it is ordered, so each notebook builds on the one before. The spatial track is just as interesting but it fetches boundaries and rasters over the network, which is a worse first experience.

  1. Get the repository and the environment

    git clone https://github.com/RiskThinking/crc-docs
    cd crc-docs
    uv sync

    uv sync reads the lockfile, so you get the exact dependency set the notebooks were executed against.

  2. Run a whole analysis before reading any of it

    uv run python pipelines/asset_portfolio_pipeline.py

    Outputs land in pipeline_output/. Every notebook in the ordered track has a headless twin like this one, which makes the examples usable as regression checks and as a starting point for your own scripts.

  3. Open the notebook that matches it

    uv run jupyter lab notebooks/

    Start with asset_portfolio_evaluation.ipynb. Launch from the repo root as shown so the notebook's data/ cache lands beside it rather than in your working directory.

  4. Point the same workflow at your own assets

    This is the whole portfolio surface. An Arrow table in, a Parquet file out, one value column per return period.

    import pyarrow as pa
    from crc_sdk.workflows import HazardDataset
    
    assets = pa.table({
        "asset_id":  ["warehouse-a", "warehouse-b"],
        "longitude": [6.9603, 7.5010],
        "latitude":  [50.9375, 51.0030],
        "sector":    ["logistics", "manufacturing"],
    })
    
    result = (
        HazardDataset.local("flood.parquet")
        .for_assets(assets)
        .select(horizons=[2050], pathways=["ssp585"])
        .return_periods([25, 50, 100, 250, 500, 1000])
        .write_parquet("portfolio-flood.parquet")
    )

    You get value_rp25 through value_rp1000. The column names asset_id, longitude, latitude and cell_index are inferred, so a conventional schema needs no configuration at all.

  5. Turn depth into damage

    An impact function replaces the sampled hazard values with event-aligned impact values before writing.

    from crc_sdk.impacts import PiecewiseLinearImpact
    
    damage_curve = PiecewiseLinearImpact(
        exposure=[0.0, 0.2, 1.0, 2.0],
        impact=[0.0, 0.0, 0.25, 1.0],
    )
    
    impacted = (
        HazardDataset.local("flood.parquet")
        .for_assets(assets)
        .return_periods([25, 100, 250])
        .impact(
            damage_curve,
            name="flood_damage_ratio",
            value_unit="fraction",
            value_semantics="damage ratio",
        )
        .write_parquet("portfolio-impact.parquet")
    )

    Now value_rp100 is the damage ratio at the 100-year flood, not the depth. The return period still identifies the source hazard event.

Read these two things next

Two pages stop most early mistakes. Probability and distributions explains what a return period means to this framework and why q=0.99 is the 100-year query. Impact functions explains the deliberate difference between evaluating an event and transforming a distribution, which is the single easiest thing to get subtly wrong.

Getting started / Learning path

Learning path

Seven notebooks in two tracks, sharing the same packages. Each has a headless pipeline twin that does the same work without Jupyter.

Asset portfolios

Ordered, offline, and the right place to start. It reads the checked-in Cologne fixtures under fixtures/os_climate/, so it runs without AWS access or any other credentials.

#NotebookPipeline twinWhat it shows
1hurdle_fit_primerReconstruct a canonical hurdle curve and read its sample diagnostics
2asset_portfolio_evaluationasset_portfolio_pipeline.pyFluent HazardDataset portfolio evaluation at return periods
3portfolio_impactportfolio_impact_pipeline.pyEvent-aligned PiecewiseLinearImpact through to currency damage
4portfolio_risk_metricsportfolio_risk_pipeline.pyMicroscores through to portfolio VaR and CVaR, with attribution
5multi_scenario_portfoliomulti_scenario_pipeline.pyHistorical against transparent local tail stresses in one evaluation

The multi-scenario cases are stresses, not projections

Notebook 5 runs sensitivity stresses on local tails. They are deliberately transparent and deliberately not calibrated climate projections. Do not present them as scenario forecasts.

Spatial

Unordered, and it fetches public inputs over the network from geoBoundaries, JRC, OS-Climate and Overture. Best read once you know what a canonical hazard dataset is.

NotebookPipeline twinWhat it shows
flood_risk_by_provinceflood_admin_pipeline.pyOS-Climate to H3, joined to administrative boundaries, enriched with Overture places, written out as PMTiles
jrc_global_flood_hazardjrc_flood_pipeline.pyStreamed GeoTIFF and COG sampled to H3, using JRC LISFLOOD

Running them

The two working directories differ, and it matters.

# notebooks: run with notebooks/ as the working directory
uv run jupyter lab notebooks/

# pipelines: run from the repo root
uv run python pipelines/flood_admin_pipeline.py
notebooks/data/
Cached notebook inputs and outputs. Gitignored.
pipeline_output/
Everything the headless pipelines write. Gitignored.
fixtures/os_climate/
Checked-in Cologne Parquet fixtures. This is what makes the portfolio track offline.

Plotly figures on GitHub

GitHub's notebook preview cannot run JavaScript, so notebooks executed locally before commit emit a static PNG fallback through kaleido. Figures will look interactive locally and flat in the browser preview. Nothing is wrong.

Core concepts / Probability and distributions

Probability and distributions

Everything the framework computes passes through a distribution interface. Two conventions do most of the work, and getting them straight prevents an entire class of quiet error.

Non-exceedance probability is the currency

Every metric API takes a non-exceedance probability q in [0, 1]. Return periods are accepted in exactly one place: when constructing a tabulated distribution.

For an upper-tail hazard, a return period T converts as q = 1 − 1/T. So the 100-year return level is queried at q=0.99, and the 500-year at q=0.998.

Return periodq (upper tail)Output column
250.96value_rp25
500.98value_rp50
1000.99value_rp100
2500.996value_rp250
5000.998value_rp500
10000.999value_rp1000

Lower-tail hazards invert this

Drought is a lower-tail hazard: the damaging outcome is a small value, not a large one. Canonical metadata records the probability convention and the source support for the dataset, so evaluation selects the correct tail automatically. If you are writing your own ingest, you are the one who records that convention.

One interface, five shapes

All distributions expose pdf, cdf, ppf, quantiles and sample. Evaluation methods accept both scalars and NumPy arrays. Choose the shape by the form of the data you actually have, not by what you wish you had.

ClassUse it when
EmpiricalDistributionYou hold raw observations
TabulatedDistributionYou hold explicit probability and value pairs, such as return-period knots from a published hazard map
FittedDistributionYou know the parametric family and its parameters
HurdleDistributionA point mass at one value, then a truncated parametric tail. The usual shape for a zero-heavy hazard such as inundation depth over dry land
PointMassDistributionThe value is constant across the whole probability support

Tabulated curves refuse to guess

TabulatedDistribution does not extrapolate. Querying outside its declared probability range raises, unless you opt in explicitly.

from crc_framework import TabulatedDistribution

curve = TabulatedDistribution.from_return_periods(
    [5, 10, 25, 50, 100],
    [0.2, 0.6, 1.1, 1.5, 1.9],
    tail="upper",
)

curve.ppf(0.99)    # fine: the 100-year knot is the edge of support
curve.ppf(0.999)   # raises: beyond the declared range

loose = TabulatedDistribution.from_return_periods(
    [5, 10, 25, 50, 100],
    [0.2, 0.6, 1.1, 1.5, 1.9],
    tail="upper", extrapolate=True,
)

This is a deliberate design choice, and it is the behaviour you want. A silent extrapolation from a 100-year source map to a 1000-year answer is exactly the kind of number nobody can defend later.

Interpolation, extrapolation, and what your source supports

Portfolio evaluation warns when a requested return period falls outside the source support recorded in the dataset metadata. With EFAS source rasters covering RP10 to RP500, asking for RP250 is interpolation, and asking for RP1000 is extrapolation. Both return numbers. Only one of them is defensible without a footnote.

Core concepts / Fitting curves

Fitting curves

Fitting is always explicit. Metrics sample the distribution you hand them and never quietly substitute a fitted curve, which is what makes the modelling choice auditable.

Three entry points

FunctionInputFamilyDiagnostics
fit_distributionRaw observations or an EmpiricalDistributionSelected by KS p-value, or namedKS, RMSE, R-squared
fit_quantilesProbability and value knotsExplicit, requiredResiduals in value space
fit_hurdle_quantilesKnots plus a known point massExplicit, requiredResiduals in value space

Knots are not observations

Passing a tabulated or fitted distribution to fit_distribution is an error, not a convenience. Return-period knots are not independent samples, so a KS statistic computed against them would be meaningless. Use fit_quantiles for knots.

Supported families

genextremeweibull_minweibull_max skewnormgumbel_rgumbel_lgenpareto

Pass family="auto", the default, to select among them. Pass a name to fit one family. Use fit_all to inspect every candidate, and quality_metrics to compute diagnostics for a distribution you have already chosen.

import numpy as np
from crc_framework import EmpiricalDistribution, FitConstraints, fit_distribution

observations = np.array([0.1, 0.2, 0.4, 0.7, 1.1, 1.8])
empirical = EmpiricalDistribution(observations)

fit = fit_distribution(
    empirical,
    family="auto",
    constraints=FitConstraints(probability=0.99, maximum_value=3.0),
)

print(fit.distribution.family, fit.diagnostics.ks_pvalue)
print(fit.distribution.ppf(0.5))

FitConstraints is an acceptance constraint, not a hint. It lets you reject a fit that puts an implausible value at a probability you care about.

Fitting published return-period knots

This is the common case when you are canonicalising a public hazard map. fit_quantiles minimises weighted differences in value space between your knots and the family's PPF.

from crc_framework import TabulatedDistribution, fit_quantiles

curve = TabulatedDistribution.from_return_periods(
    [5, 10, 25, 50, 100],
    [0.2, 0.6, 1.1, 1.5, 1.9],
    tail="upper",
)
fit = fit_quantiles(curve, family="gumbel_r")

Zero-heavy hazards need a stated point mass

A flood-depth map over mostly dry land is mostly zeros. The framework will not infer an exact point mass from sparse zero-valued knots, because the inference is not sound. Supply the probability yourself.

from crc_framework import fit_hurdle_quantiles

fit = fit_hurdle_quantiles(
    curve,
    family="gumbel_r",
    atom_probability=0.5,   # half the support sits on the atom
    atom_location=0.0,      # and the atom is at zero depth
)

Notebook 1 in the ordered track, hurdle_fit_primer, reconstructs one of these from a canonical row and walks its diagnostics. It is worth an hour.

Lossless against lossy

TabulatedDistribution is the lossless interpolation of the knots you supplied. Parametric and hurdle fits are lossy. If your system has to reconstruct its source exactly, persist the original probability and value pairs separately.

What this means for canonical files

Source knots and fit diagnostics are transient ingest inputs, not a second persisted contract. Values reported at source return periods are fitted curve evaluations, not guaranteed bit-for-bit reproductions of source pixels. Schema 1.2 does let you persist a tabulated curve directly when you need the lossless form.

Core concepts / Impact functions

Impact functions

An impact function maps exposure to damage. It has two deliberately distinct operations, and choosing the wrong one is the most common subtle error in the whole framework.

Two operations, two meanings

CallWhat it doesUse it when
impact.evaluate(values)Evaluates event-aligned exposure values. Input shape and order are preserved, including for decreasing or non-monotonic callablesReturn periods continue to identify the source hazard event
impact(distribution)Transforms an exposure distribution into an impact distribution. Decreasing built-in transforms reorder the output quantiles so the result stays a valid distributionYou are doing risk analysis on the impact distribution itself

Why the portfolio workflow picks the first one

The SDK samples each hazard return period, then calls impact.evaluate(...) on that row's value vector. So value_rp100 = impact(hazard_rp100) and the return period still names the hazard event. This differs on purpose from transforming a full distribution and then taking an impact quantile, which can reorder decreasing or non-monotonic impacts. If you want that interpretation, use the distribution interface in crc-framework directly.

Built-in transforms

LinearImpact, SigmoidImpact and PiecewiseLinearImpact support both operations. CallableImpact adapts a vectorised NumPy callable for point evaluation; CallableTransform also supports distribution transformation.

import numpy as np
from crc_framework import CallableImpact, LinearImpact

impact_function = LinearImpact(slope=0.25, maximum=1.0)

# event-aligned: two events in, two impacts out, same order
event_impacts = impact_function.evaluate(np.array([0.5, 2.0]))

# distributional: an exposure distribution in, an impact distribution out
impact = impact_function(exposure)

custom = CallableImpact(lambda values: np.clip(values / 2.0, 0.0, 1.0))

The registry, keyed by risk factor and context

impacts.for_factor(...) selects a registry-backed climate transform. Supply a TransformContext with the geography, building type and any historical values it needs; pass overrides when your application must replace transform parameters.

from crc_framework import RiskFactor, TransformContext, impacts

impact = impacts.for_factor(
    RiskFactor.CFLOOD,
    context=TransformContext(
        continent="Europe",
        building_type="Commercial buildings",
    ),
)(exposure)

Wiring context from asset columns

In a portfolio run, the context usually varies per asset. ImpactContextColumns maps asset columns onto context fields.

from crc_sdk.impacts import impacts
from crc_sdk.workflows import ImpactContextColumns

registry_request = request.impact(
    impacts.for_factor("inundation"),
    context=ImpactContextColumns(
        country="country",
        continent="continent",
        building_type="building_type",
        historic_mean="historic_mean",
    ),
    name="inundation_impact",
    value_unit="fraction",
    value_semantics="damage ratio",
)
  • The generated H3 cell_index is always supplied to the framework impact context.
  • Configured context columns are read from every asset, including when they are not kept as output passthrough columns.
  • Stored registry context provides fallback values for fields with no asset value.
  • Impact metadata records the event-aligned interpretation, source units and semantics, output units and semantics, function name and type, and the context-column mapping.

Lambdas cannot be parallelised

Framework impact objects and top-level Python callables run in the existing process pool. Lambdas and closures are not picklable, so they run serially when the worker count is implicit. Asking for more than one worker with a lambda raises rather than silently degrading.

from crc_sdk.workflows import ExecutionOptions

# a lambda plus explicit parallelism is an error
.impact(lambda depth: np.clip(depth / 2.0, 0.0, 1.0), name="ratio")
.write_parquet("out.parquet", execution=ExecutionOptions(max_workers=1))

# promote it to a module-level function to use the pool
def depth_to_ratio(values):
    return np.clip(values / 2.0, 0.0, 1.0)

Core concepts / Microscores, VaR, CVaR

Microscores, VaR and CVaR

The path from a hazard distribution to a portfolio risk number runs through binary outcomes. It is a small API with one sharp edge: branch count grows as two to the power of the number of factors.

Microscores turn quantiles into outcomes

A microscore samples exposure and impact at the probabilities you name, and reduces them to a binary risk outcome.

from crc_framework import (
    RiskFactor, ScenarioMetadata, TabulatedDistribution,
    TransformContext, generate_microscores, impacts,
)

exposure = TabulatedDistribution.from_return_periods(
    periods=[10, 20, 50, 100, 200, 500],
    values=[0.1, 0.2, 0.5, 0.9, 1.4, 2.0],
    tail="upper",
)

impact = impacts.for_factor(
    RiskFactor.CFLOOD,
    context=TransformContext(
        continent="Europe",
        building_type="Commercial buildings",
    ),
)(exposure)

suite = generate_microscores(
    exposure,
    impact=impact,
    probabilities=[0.95, 0.99],
    metadata=ScenarioMetadata(factor=RiskFactor.CFLOOD.value),
)
suite.scores
The sampled exposure and impact values behind every score.
suite.at(q)
The binary outcome at a requested non-exceedance probability.
suite.exposure_statistics
Summary diagnostics for the exposure side.
suite.impact_statistics
Summary diagnostics for the impact side.

Aggregating factors into VaR and CVaR

Each microscore becomes a BinaryOutcome. Combining outcomes produces the full set of independent downside and upside branches, and risk is computed at one or more confidence levels.

from crc_framework import BinaryOutcome, compute_risk, compute_spanning_set

outcomes = [
    BinaryOutcome("flood", downside_probability=0.05, downside_impact=0.4),
    BinaryOutcome("fire",  downside_probability=0.10, downside_impact=0.2),
]

branches = compute_spanning_set(outcomes)
risk = compute_risk(outcomes, levels=[0.80, 0.95])
level_95 = risk.at(0.95)

print(len(branches))                  # 4: two outcomes per factor
print(level_95.var, level_95.cvar)

Cap the branches before they cap you

Branch generation grows as 2**n for n factors. Ten factors is a thousand branches; twenty is a million. Set max_branches on compute_spanning_set or compute_risk to impose a limit your application can actually defend.

FactorsBranchesComfort
416Trivial
101,024Fine
1665,536Set a limit
2416,777,216Rethink the decomposition

RiskLevel.attribution reports each factor's contribution to VaR and CVaR, which is usually the number a committee actually wants to see.

Spatial lookups

Spatial helpers take an H3 cell as an integer or a string, normalise it to H3 resolution 4, and return None when no reference mapping exists. That None is meaningful: handle it rather than asserting.

from crc_framework import lookup_geography, lookup_ipcc_region

cell = 600550049193132031
geography = lookup_geography(cell)
region = lookup_ipcc_region(cell)

if geography is not None:
    print(geography.continent, geography.countries)

lookup_continent returns a continent name, lookup_ipcc_region returns the IPCC region, and lookup_geography returns a Geography with the continent and the intersecting ISO3 country codes.

Data / Canonical hazard datasets

Canonical hazard datasets

One versioned Arrow and Parquet contract holds every fitted hazard. Understanding this file is understanding the standard: it is the thing two teams can compare.

One self-describing file

Each dataset is a single Parquet file, expanded by H3 cell so it can be joined spatially. You choose the full destination path and filename. Writes go through DuckDB, so supplying a configured connection lets the same API use its filesystems, extensions, secrets and settings.

external raster/table
  -> source curves and geometry
  -> selected family fit
  -> conservative intersecting H3 cells
  -> canonical Arrow
  -> Parquet

The row key

The logical row key is (hazard_name, horizon, pathway, cell_index, source_id). Canonical files are sorted by that key by default, which enables predicate pruning and merge joins.

cell_index is a join key, not an identifier

It is the spatial join key, not a globally unique row identifier. Several source curves can land in one cell. When that happens for a single asset, hazard, horizon and pathway, evaluation raises rather than silently picking or averaging one.

Curve columns are a tagged union

Rows carry the parameters needed to reconstruct a framework distribution. curve_kind tells you which shape you are holding, and which columns are populated.

curve_kindcurve_typeWhat is populatedSince
fittedThe parametric familyScalar curve parameters. curve_shape is null for Gumbel families, which have no shape parameter1.0
hurdleThe parametric familyScalar parameters plus atom probability and location1.0
point_masspoint_massZero scale, probability one at curve_location. Constant across the whole source support1.1
tabulatedlinear_probabilityThe list columns curve_probabilities and curve_values. Scalar parameters are null1.2
no_dataAn explicit scientific reason codeNothing. All parameter fields are null, and batch quantile evaluation returns nulls for these rows1.2

This layout avoids a sprawl of redundant status, fit-stage, interpolation and reason columns. Dataset metadata and run manifests carry ordered-family and aggregate treatment provenance once, rather than on every row.

Metadata carries the meaning

Dataset-wide facts are stored once as a complete JSON payload under the Parquet key crc.hazard.metadata. Without it the numbers are ambiguous, so read it before you trust a file someone hands you.

RecordedWhy you need it
Schema versionTells you which curve kinds can appear
H3 resolutionOne uncompacted resolution per dataset. Asset points are converted to it
Non-exceedance probability conventionWhich tail is the damaging one
Source probability supportWhere interpolation ends and extrapolation begins
Value unit and semanticsMetres of depth, an index value, a ratio
WKB CRSThe coordinate reference system of any stored source geometry
Producer and source provenanceWho made it and from what
Curve-fit policy and creation versionHow the fit was chosen, and by which version of the code

Evaluation writes its own record under crc.hazard.evaluation: value unit, value semantics, and the complete mapping from return period to probability to column name.

Cells are a conservative superset

Boundary candidate generation uses H3 overlap coverage, not centre polyfill. The integer join therefore returns a conservative superset, which is then refined exactly.

  • Where source WKB is present, the join is refined with ST_Covers(source_geometry, asset_point).
  • Rows without WKB keep cell-level precision.
  • source_id and spatial_match, which is either exact_geometry or h3_cell, stay in the output so you can see which happened.

Resolution estimates report measured coverage error and expanded row count; ingest policy selects and records the dataset resolution.

Writing a canonical stream

write_hazard_stream has two modes, and the choice is a memory-against-storage trade.

ordered=Trueordered=False
DuckDB materialises the stream, rejects duplicate keys, globally sorts, and may spill to the work directory after its memory limit. Keeps engine memory predictable, compresses better, and can speed up scans that benefit from physical key clustering. This is the default contract. Appends validated Arrow batches straight to a local Parquet staging file, scans only the projected row-key columns for duplicates, and publishes atomically after validation. An explicit low-memory alternative for unusually large partitions or tight containers.

Canonical schema, metadata, uniqueness and every downstream curve and percentile API are identical either way. Only the physical row order differs.

Measured on one machine

These figures measure the persistence pass only, on one machine. Treat them as evidence of the trade-off, not as throughput guarantees.

SampleModePeak RSSFile sizeTime
2,148,497 rowsstreaming656 MB45 MB16.03 s
2,148,497 rowsordered1.69 GB35 MB16.38 s
300,000 rowsstreaming282 MB6.3 MB3.01 s
300,000 rowsordered525 MB4.9 MB2.78 s

Storage reduction from ordering was consistent across both samples. The small timing difference changed direction between them, so do not plan around it. Whichever mode you choose, reserve process headroom beyond the engine limit for Arrow and Python buffers and the final Parquet write.

Data / Ingesting public hazards

Ingesting public hazards

Acquisition is an immutable, lazy workflow. Builder calls do no network work at all, so you can print a plan and read it before anything is fetched or fitted.

Plan first, then materialise

Builder methods and explain() never resolve a release or fetch a raster. materialize() resolves latest once, records the pinned version in the cache manifest and in canonical provenance, caches the AOI crops, fits the curves, and hands back an ordinary HazardDataset.

from crc_sdk.workflows import HazardDataset, JRCFloodPolicy

plan = (
    HazardDataset.efas(version="latest")
    .for_area((7.75, 49.75, 8.45, 50.25))
    .cache("cache/efas", mode="reuse")
    .source_periods("all")
    .canonicalize(policy=JRCFloodPolicy.curated(h3_resolution=10))
)

print(plan.explain())          # no network access yet
hazard = plan.materialize("hazards/efas-rhine.parquet")
mode="reuse"
Use the cache if it is there, fetch what is missing.
mode="offline"
Fail rather than touch the network. Pair it with prefetch().
mode="refresh"
Re-resolve the release and replace cached crops.
mode="stream"
Read remotely with no persistent source cache.

Cache manifests pin the resolved version, years, bounds, source URLs, local objects and checksums. Run prefetch(), then switch the same plan to mode="offline", and the run is reproducible without network access.

Source periods are not evaluation periods

This distinction catches people. Source periods choose which rasters are used for fitting, and default to every period in the resolved release. Evaluation periods are what you ask the fitted curve for afterwards.

result = (
    plan.for_assets(assets)
    .select(hazard_names=["RiverineInundation"], pathways=["historical"])
    .return_periods([50, 100, 250, 500])
    .write_parquet("outputs/flood-depth.parquet")
)

The compact chain lazily materialises a deterministic canonical file inside the configured cache, then delegates to the same local portfolio evaluator that HazardDataset.local(...) uses.

Watch the support boundary

With EFAS source rasters covering RP10 to RP500, requesting RP250 is interpolation and RP1000 is extrapolation. Evaluation warns you, because canonical metadata records the source return-period support. The warning is the one you want to read.

JRC flood: GLOFAS and EFAS

The two releases are laid out differently, and the dataset descriptions encode that difference so an AOI workflow never exposes tiles or filenames to you.

DatasetVersionLayout
GLOFAS2.1.2JRC's tiled global layout
EFAS3.1.1Nine continental return-period rasters

JRC and EDO drought

EDO Soil Moisture Index data uses the same lazy workflow, with complete years reduced to compact annual-minimum AOI cache objects before fitting.

from crc_sdk.workflows import EDODroughtPolicy, HazardDataset

plan = (
    HazardDataset.smi(version="latest")
    .for_area((9.5, 50.5, 10.5, 51.5))
    .years("all_complete")
    .cache("cache/edo-smi", mode="reuse")
    .canonicalize(policy=EDODroughtPolicy.curated(h3_resolution=6))
)

hazard = plan.materialize("hazards/edo-smi.parquet")

The curated policy uses the lower return-period tail and requires at least twenty complete years. Metadata stores both that tail and the Gringorten support of the selected annual record, so evaluation picks the correct lower tail on its own and warns on extrapolation.

OS-Climate return-period rasters

OS-Climate rasters are canonicalised with OSClimateIngestPolicy and canonicalize_os_climate. You must choose the distribution family, and for a zero-heavy hazard you must provide an explicit HurdleFitPolicy — the SDK will not infer an exact point mass from sparse knots.

from crc_sdk.workflows import OSClimateIngestPolicy, canonicalize_os_climate

Plain curves are fitted with fit_quantiles; hurdle curves with fit_hurdle_quantiles. Persisted rows are queried back through LocalProvider and HazardQuery.

The zarr extra is required for OSClimateProvider and ZarrRaster.

Private buckets

Authenticated remote sources are configured the idiomatic DuckDB way: raw secret DDL, passed as setup_sql so it runs on connect, right after extensions load. There is deliberately no secret-builder type in the SDK, because DuckDB's own secret DDL is already the documented interface.

import os
from crc_sdk.connectors.duckdb import DuckDBConnection, sql_quote

setup_sql = []
key_id, secret = os.getenv("GCS_ACCESS_KEY"), os.getenv("GCS_ACCESS_SECRET")
if key_id and secret:
    setup_sql.append(
        f"CREATE OR REPLACE SECRET gcs (TYPE GCS, KEY_ID {sql_quote(key_id)}, "
        f"SECRET {sql_quote(secret)})"
    )

con = DuckDBConnection.for_analytics(work_dir, setup_sql=setup_sql).connect()

One DuckDB quirk worth knowing

PROVIDER is a bare keyword — config, credential_chain and so on — not a quoted string literal, unlike every other secret option. sql_quote and sql_identifier are exported for building that SQL safely.

Data / Connectors and sources

Connectors and sources

Connectors are source-format readers and query engines. Ingest adapters do the conversion into the canonical contract; connectors themselves stay dumb on purpose.

Where things live

crc_sdk.core
The stable public API of crc_framework, re-exported.
crc_sdk.fitting
Fitting entry points, re-exported from the framework.
crc_sdk.impacts
Impact transforms and the registry, re-exported from the framework.
crc_sdk.connectors
External formats and query engines. DuckDB connection helpers, Zarr and GeoTIFF ingest, the lazy pipeline seam.
crc_sdk.providers
Storage and dataset discovery.
crc_sdk.geometry
Geometry conversion, H3 indexing and polyfill, raster-to-H3 sampling, coverage writers, PMTiles.
crc_sdk.schema
Columnar data contracts.
crc_sdk.types
SDK-owned Pydantic configuration and metadata.
crc_sdk.workflows
The coordination layer, where data access meets computation. Most of what you will call day to day.

Import the stable surface

Use crc_framework's public API rather than reaching into crc_framework._core. The underscore module is an implementation detail and may change between releases.

The lazy process seam

DuckDBRelationSource, ArrowBatchSource and DuckDBPipeline form one common seam. Native SQL and Parquet adapters return relations; chunk stores yield bounded Arrow batches. Both feed the same immutable filter, project, aggregate and write pipeline.

ReaderExtraNotes
GeoTiffRasterrasterGeoTIFF and COG, streamed from local paths or gs://, s3:// and http(s):// URIs through GDAL's own range-request support. No local download by default
ZarrRasterzarrZarr stores, including OS-Climate
NetCDFRasternetcdfNetCDF and CF
DuckDBConnectionbaselineConnection helpers, RuntimeResources, streaming Parquet writes

Agricultural layers

Agricultural requests are immutable and bounded before they can scan. Builder calls perform no network I/O; relation(), to_arrow_reader() and write_parquet() are the execution points.

from crc_sdk.workflows import AgriculturalLayer

crop_mix = (
    AgriculturalLayer.usda_cdl()
    .resolution("30m")
    .for_area((-93.46, 42.14, -93.45, 42.15))
    .years(2025)
    .classes([1, 5])   # corn and soybeans
    .scan()
    .pipeline()
    .aggregate("count(*) AS sampled_pixels", groups="year, crop_code, crop_name")
)

for batch in crop_mix.to_arrow_reader():
    process(batch)

For global predicted field units, swap the source and keep the same surface:

fields = (
    AgriculturalLayer.ftw_fields()
    .in_country("FR")
    .for_area((2.0, 47.5, 3.0, 48.5))
    .years(2024)
    .confidence_at_least(80)
    .scan()
    .pipeline()
)
fields.write_parquet("outputs/france-fields.parquet")

Do not overclaim these layers

FTW fields are remote-sensing units. They are not cadastral parcels and not evidence of ownership. CDL crop pixels are land-cover observations, not acreage, yield or financial exposure.

The two sources take different routes for a real reason. DuckDB's community duckdb_zarr extension can scan ordinary remote Zarr v2 and v3 stores, but it cannot open Icechunk's versioned repository and session model. USDA CDL therefore uses the official Icechunk client for version resolution and chunk reads, then exposes bounded Arrow batches to DuckDB — it never materialises the full raster or a full AOI in memory. FTW GeoParquet stays native in DuckDB, applying bbox pruning before exact spatial filtering.

PMTiles

PMTilesBuild streams a GeoParquet source, either a single file or a Hive-partitioned dataset glob, into one .pmtiles archive in a single tiling pass. The GeoParquet to GeoJSON bridge is built in DuckDB spatial SQL — ST_AsGeoJSON, ST_ReducePrecision, ST_Transform — rather than shelling out to an external converter.

  • tippecanoe_threads and duckdb_threads default to every detected core, with no conservative per-thread cap, because tippecanoe's tile building has no documented per-thread memory ceiling.
  • A pre-flight budget check raises a clear error if a source is estimated to exceed available scratch disk, instead of silently degrading into a slower multi-batch fallback.
  • Provisioning more disk, or narrowing the run's scope, is left to you.

Public inputs used by the examples

SourceUsed for
OS-ClimateReturn-period hazard rasters, in Zarr
JRC LISFLOOD, GLOFAS, EFASGlobal and European flood hazard
JRC EDOSoil Moisture Index, for drought
geoBoundariesAdministrative boundaries for spatial joins
OverturePlaces, to enrich spatial results
USDA CDLUnited States crop land cover, through Icechunk
Fields of The WorldGlobal predicted field units, as GeoParquet

Workflows / Asset portfolios

Asset portfolios

Evaluate canonical curve parameters for a whole portfolio without returning to the source format and without refitting. One row per asset, hazard, horizon and pathway; one value column per return period.

The whole surface

import pyarrow as pa
from crc_sdk.workflows import HazardDataset

assets = pa.table({
    "asset_id":  ["warehouse-a", "warehouse-b"],
    "longitude": [6.9603, 7.5010],
    "latitude":  [50.9375, 51.0030],
    "sector":    ["logistics", "manufacturing"],
})

result = (
    HazardDataset.local("flood.parquet")
    .for_assets(assets)
    .select(horizons=[2050], pathways=["ssp585"])
    .return_periods([25, 50, 100, 250, 500, 1000])
    .write_parquet("portfolio-flood.parquet")
)

Four ways to hand over assets

You passWhat happens
An Arrow tableRegistered directly with the engine
A PathRead as an asset Parquet file
A stringTreated as caller-supplied DuckDB SQL
Assets with H3 cellsPass cell_index_column="cell_index" instead of coordinates. Skips point conversion and exact source-geometry refinement

The column names asset_id, longitude, latitude and cell_index are inferred. Reach for AssetPortfolio, PointColumns or CellColumn only when your asset schema is unconventional.

(
    HazardDataset.local("flood.parquet")
    .for_assets(assets_with_cells)
    .return_periods([25, 50, 100, 250, 500, 1000])
    .write_parquet("portfolio-flood.parquet")
)

How an asset meets a curve

  1. Point assets are converted to the H3 resolution recorded by the canonical dataset.
  2. The integer H3 join produces a conservative superset of candidates.
  3. Where source WKB is present, the join is refined with ST_Covers(source_geometry, asset_point).
  4. Rows with no WKB keep cell-level precision, and say so through spatial_match.

Ambiguity raises, it does not resolve itself

Multiple source curves for one asset, hazard, horizon and pathway raise an error rather than being silently selected or aggregated. Missing asset or scenario matches raise rather than being dropped from the output. A short portfolio is a bug, so the framework refuses to produce one quietly.

Adding an impact function

Sampling happens first, then impact.evaluate(...) runs on the row's value vector, so value_rp100 = impact(hazard_rp100).

import numpy as np
from crc_sdk.workflows import ExecutionOptions

impact_result = (
    HazardDataset.local("flood.parquet")
    .for_assets(assets)
    .return_periods([25, 100, 250])
    .impact(
        lambda depth: np.clip(depth / 2.0, 0.0, 1.0),
        name="depth_damage_ratio",
        value_unit="fraction",
        value_semantics="damage ratio",
    )
    .write_parquet(
        "portfolio-impact.parquet",
        execution=ExecutionOptions(max_workers=1),
    )
)

Always pass value_unit and value_semantics. They are written into the evaluation metadata, and they are the difference between a column of numbers and a column somebody else can interpret.

Execution options

Worker, batch and connection controls are grouped under ExecutionOptions on write_parquet, deliberately kept out of the analytical chain. Output evaluation is streamed in bounded Arrow batches to compressed Parquet, so a large portfolio does not have to fit in memory.

For rows you have already selected, distribution_from_hazard_row remains available as the low-level curve reconstruction utility.

Workflows / Spatial

Spatial

The other track. Instead of asking what a portfolio is exposed to, it asks what a place is exposed to — and renders the answer as tiles you can put on a map.

The spatial flow

raster / H3 sampling / admin join / place enrichment / PMTiles

Two notebooks cover it. flood_risk_by_province runs OS-Climate to H3, joins administrative boundaries, enriches with Overture places and writes PMTiles. jrc_global_flood_hazard streams a GeoTIFF or COG and samples it to H3 using JRC LISFLOOD. Both fetch their public inputs over the network.

H3 indexing

Two paths exist because two situations exist. H3Indexer does DuckDB-native polyfill; polyfill_wkb does vectorised batch polyfill over Arrow data and needs the geometry extra for h3ronpy.

HelperDoes
H3IndexerDuckDB-native H3 polyfill
polyfill_wkbVectorised batch polyfill over Arrow
expand_polygon_candidatesCandidate expansion for boundary coverage
intersecting_cellsCells intersecting a geometry
cell_polygonThe polygon for a cell
pixel_grid_resolutionChoose an H3 resolution for a pixel grid
sample_grid_to_h3Sample a raster grid onto cells
write_exploded_coverageWrite exploded coverage output
LookupCatalogOptional nested lookup derivation, with write_lookup_contract and write_partitioned_lookup

Coverage, not centres

Candidate generation uses overlap coverage rather than centre polyfill, so the integer join is a conservative superset before exact refinement. This is why an spatial result can look like it has more cells than you expected: the extras are dropped at the refinement step, not before it.

Constructors tune themselves

OSClimateProvider, ZarrRaster and H3Indexer have no natural caller-supplied directory of their own, so they build a resource-tuned connection by default through DuckDBConnection.for_analytics rather than a bare untuned one. This scales out of the box with no configuration.

Passing an explicit connection or con always wins and skips that entirely. Otherwise the spill directory defaults to a stable location under the system temp directory through default_work_dir() — not a fresh one per call — and can be set per call with each constructor's work_dir parameter, or globally with CRC_DUCKDB_WORK_DIR.

Running it well / Resources and tuning

Resources and tuning

The SDK detects DuckDB limits when asked and relays them through the connection config. The defaults are deliberately conservative about threads, for a reason worth understanding.

How the thread count is derived

Resource limits are detected on request, through RuntimeResources.detect or DuckDBConnection.for_analytics.

threads = min(cpus, usable_RAM / GiB_per_thread)

usable_RAM     ~= 60% of detected memory
GiB_per_thread  = 2.5 by default

Spatial work through GEOS often gets slower when over-threaded, which is why the ceiling is memory-derived rather than simply the core count. memory_limit and max_temp_directory_size remain hard process caps.

VariableOverrides
CRC_DUCKDB_THREADSThe derived thread count
CRC_DUCKDB_MEMORYThe memory limit
CRC_DUCKDB_BYTES_PER_THREAD_GIBThe per-thread memory assumption
CRC_DUCKDB_WORK_DIRThe spill and temp directory
CRC_DUCKDB_PROFILESet to 1 for query profiling around the enrich and coverage stages

Or skip the detection entirely and pass an explicit config dict.

PMTiles threading is different on purpose

Unlike DuckDB's GEOS-throttled default, tippecanoe_threads and duckdb_threads in PMTilesBuild default to every detected core, because tippecanoe's tile building has no documented per-thread memory ceiling to protect against.

The pre-flight disk check will raise before a long run rather than degrade into a slow fallback. If it raises, give it more scratch disk or narrow the scope.

Choosing a write mode

See Canonical hazard datasets for the measured trade-off. In short: keep the default ordered=True unless you are memory-bound, and reserve headroom above the engine limit for Arrow and Python buffers and the final Parquet write.

Before a long run

  • Call explain() on the plan and read it. Builder calls do no I/O, so this costs nothing.
  • prefetch() the source cache, then switch to mode="offline" so a network wobble cannot kill hour three.
  • Point work_dir at a disk with room to spill, not at a small container overlay.
  • Check that requested return periods sit inside the source support, or accept the extrapolation warning knowingly.
  • If you are using an impact function, make sure it is a module-level function rather than a lambda before asking for workers.

Running it well / Troubleshooting

Troubleshooting

Common setup and analysis issues, with explanations and practical fixes.

Setting up

Python and platform support
We recommend Python 3.12 or later for new environments. Check Platforms and Python versions for upstream requirements, wheel availability and source-build instructions.
Optional dependencies
Extras can have stricter requirements than the base SDK. Follow the SDK installation instructions for the extras you need.
ImportError naming an extra
Not a broken install. Extras are imported lazily and the error names exactly what to add.
tippecanoe is not pip-installable
PMTiles needs tippecanoe and tile-join on PATH. Check with require_tippecanoe() before a long job, not after.
Notebook working directory
Launch as uv run jupyter lab notebooks/. Pipelines run from the repo root. Getting this backwards scatters cache directories.

Getting numbers

A return period is not a probability
Metrics take non-exceedance q. Return periods are accepted only when constructing a tabulated distribution. Upper tail: q = 1 − 1/T.
Tabulated curves raise outside their range
By design. Pass extrapolate=True only when you mean it.
Requested period outside source support
You get a warning, not an error, and you get a number. Read the warning: RP1000 from RP500 rasters is extrapolation.
Knots are not observations
Passing a tabulated or fitted distribution to fit_distribution is an error. Use fit_quantiles.
No point mass is inferred for you
Zero-heavy hazards need an explicit HurdleFitPolicy or atom_probability. Sparse zero knots are not enough evidence.
Fitted values are not source pixels
Values at source return periods are fitted curve evaluations. If you need bit-for-bit reproduction, persist the knots yourself or use a tabulated curve.
Gumbel has no shape
curve_shape is legitimately null for Gumbel families. It is not a missing value to impute.

Impacts and portfolios

Two operations, one object
impact.evaluate(values) is event-aligned; impact(distribution) transforms a distribution. The portfolio workflow uses the first. They differ for decreasing or non-monotonic impacts.
Lambdas are not picklable
They run serially. Asking for more than one worker with a lambda raises. Promote it to a module-level function.
Ambiguous matches raise
Multiple curves for one asset, hazard, horizon and pathway is an error, not something to average. Missing matches raise too, rather than shortening your output.
Context columns are read even when hidden
Columns configured in ImpactContextColumns are read from every asset whether or not they are kept as output passthrough columns.
Units are yours to declare
value_unit and value_semantics are not decoration. They land in metadata and they are how the next person reads your column.

At scale

Branches are exponential
compute_spanning_set grows as 2**n. Set max_branches.
Ordered writes cost memory
A global sort roughly tripled peak RSS on a two-million-row sample. Use ordered=False when containers are tight.
Engine limits are not process limits
memory_limit caps DuckDB, not Arrow buffers, Python objects or the Parquet writer. Leave headroom.
Thread count may be below your core count
That is the memory-derived ceiling, and GEOS work often slows when over-threaded. Override with CRC_DUCKDB_THREADS if you know better.
Spill directory defaults to system temp
Fine on a workstation, often wrong in a container. Set CRC_DUCKDB_WORK_DIR.

Claims about the outputs

Multi-scenario notebooks are stresses
Sensitivity stresses on local tails, deliberately transparent, deliberately not calibrated projections.
FTW fields are not parcels
Remote-sensing units, not cadastral boundaries, not evidence of ownership.
CDL pixels are not exposure
Land-cover observations, not acreage, yield or financial exposure.
Open data changes precision, not credibility
The engine, schema and conformance suite are the same as the commercial stack. Precision depends on input resolution — so report the resolution, as good science already requires.

Project / Access and licence

Access and licence

The packages are on PyPI and the code is AGPL. Membership is a separate thing: it governs the hosted platform and the reference data, and it is free for non-commercial research.

Two different questions

The code

crc-sdk and crc-framework are licensed AGPL-3.0-or-later. Install them from PyPI today; no application, no account.

Parts of the numerical distribution implementation derive from or are informed by SciPy and Cephes, whose notices are preserved in the package's third-party notices.

Membership

Free for non-commercial research only, and it covers the platform environment and the open reference datasets. Commercial use of the platform or of RiskThinking.AI's data needs a commercial licence.

AGPL obligations are worth reading with your legal office before you embed either package in a service you distribute.

Joining

Individual researchers
Apply directly with proof of your institution and a sentence or two on what you intend to use it for.
Faculties and universities
Enrol all bona fide students, researchers and faculty at once. A short letter of intent from the institution formalises it.
Early access preview
Send your GitHub handle and contact email, and confirm the early access terms. You are onboarded to the repositories, the community channel and the spec.

What members can use

  • The open reference datasets provided inside the Commons environment.
  • Any open dataset you upload yourself.
  • Any data, models or damage functions of your own.

Results stay conformant with the standard regardless of whose data powers them. What is held back commercially is bias-corrected and downscaled hazard data including derivative hazards, physical-asset discovery data, proprietary impact functions beyond the open starter set, an enterprise edition of the pipeline running a step ahead of the open release, and support with SLAs. What differs is the data flowing through the machinery, not the machinery.

What to expect from a preview

Rough edges are expected

Specs, APIs and data schemas may change during the preview, sometimes with little notice. Availability is a plan rather than a promise, and the order and timing of drops may shift. Package releases can change independently. Lock the versions resolved for your environment.

Resolve compatible releases together and commit uv.lock alongside pyproject.toml. Use the lockfile to reproduce the environment:

uv add crc-sdk crc-framework
uv lock
uv sync --locked

Contributing

Fixes, tooling, docs and open impact functions go through the published contribution process. The most useful early contribution is unglamorous: run the reference pipeline on the sample data and report what you found. Spec gaps you hit feed into the roadmap directly.

If you are aThe surface to build on
Data providerPublish hazard, asset or exposure data against the CRC input contracts
ModellerContribute sector, peril or region-specific damage functions to the open library, or offer them commercially against the spec
Tool builderConsume outputs through the APIs; build integrations into risk platforms, GIS, portfolio and disclosure workflows
ImplementerAlternative pipelines, validation and conformance tooling, benchmarking suites
ResearcherTest, extend and improve the methods encoded in the spec, published so others can reproduce it

If your component works with the reference pipeline on reference data, it works with the ecosystem. Conformance is what makes it plug-compatible, and it is the claim you can put in front of your own users.

Repositories

Every published package and repository is listed under Community and resources, together with the contact routes and the institutions already enrolled.

About the project / Why CRC

Why CRC exists

Measuring physical climate risk has moved from a voluntary exercise to a supervised obligation. The harder test is not reporting a number. It is owning the method behind it.

The obligation is already here

IFRS S1 and S2, the CSRD, the EU Taxonomy, TCFD and TNFD have made climate and nature risk a required part of corporate and financial reporting. Prudential supervisors from the EBA to BaFin and FINMA now expect institutions to identify, measure, manage and monitor physical climate-risk drivers.

Reporting a figure is the easy half. Auditors and supervisors increasingly expect you to understand and own the data and models behind a tool, rather than accept a vendor's black box. A closed, proprietary model cannot meet that bar by design.

You cannot defend a methodology you are not permitted to see

Independent reviews of the field, including UNEP FI's Climate Risk Landscape work, keep finding the same thing: provider methodologies differ substantially and remain largely opaque, so results cannot be benchmarked. The same asset, scored by two providers, can yield materially different risk scores. Relying on one closed vendor produces a false sense of precision, exactly when supervisors are asking you to understand model uncertainty rather than look past it.

Four failures of the closed model

FailureWhat it costs you
Numbers you cannot defendMethodology sits inside a vendor black box, closed to the regulators, auditors and boards now asking how the figures were produced
Results that do not compareEvery vendor encodes assets, hazards and scenarios differently, so similar portfolios diverge and supervisors cannot aggregate across the system
A costly door, both waysEvaluation takes months and six figures, and proprietary formats make exit expensive, pricing smaller players out before they begin
Science that keeps restartingWithout a shared foundation, researchers rebuild the same infrastructure instead of advancing stochastic and multi-hazard methods

What an open standard changes

The closed model isCRC is
Black-box methodology you are not permitted to inspectInspectable methodology, so you own your models
Assets, hazards and scenarios encoded differently by every vendorOne shared schema, so results compare across teams and firms
Single-vendor point estimates that manufacture false precisionA stochastic, full-distribution engine that makes uncertainty legible
Cost and expertise walls that shut out smaller institutions and emerging marketsOpen, no-cost access for them and for the researchers studying them

Four things follow from conforming to the same contracts. Results from different vendors, models and internal teams can be placed side by side and compared. Every assumption is inspectable, so an output can be traced through an open pipeline. Open contracts let you swap providers or bring your own functions without rebuilding your workflow, so your methodology attaches to the standard rather than to a vendor. And the cost of starting drops to an afternoon, because the spec, the pipeline, the reference data and the starter functions are free to run.

Why now: CMIP7

The climate model intercomparison data that virtually every physical-risk system is built on has just been upgraded. CMIP7 replaces CMIP6, whose data stopped at 2014. CRC is among the first production systems to ship on CMIP7, so you start on the current standard rather than the last decade's.

This is what the project means by Climate 2.0: stochastic, multi-hazard, coherent across hazards and countries, and built for the extreme tail rather than the average case.

What people build on it

See real exposure

Load your asset or loan-book addresses, and get flood, heat and wildfire exposure for the assets you actually hold rather than a headline figure.

Stand up a CMIP7 stress test

Run a stochastic, multi-pathway test in a structure a supervisor can read and an auditor can follow.

Bring your own data

Load your own datasets, models and damage functions, and still produce a result that compares cleanly with everyone else's.

Screen a city or development portfolio

Development banks, ministries and city networks screen assets for priority physical risks, and share the method with anyone who needs to use it.

The underserved long tail

Commercial vendors cluster around large, listed, global portfolios. The long tail — agriculture and supply chains, real estate and unlisted assets, single river basins, specific hazards and geographies — is exactly where researchers work. Building that science on the open standard adds the coverage the market has left out and folds it into a shared, comparable foundation rather than another silo.

That is not charity, it is how a standard earns legitimacy. Researchers are the people who pressure-test a standard, validate it and extend it into the corners no vendor's marketing can reach.

About the project / Roadmap and governance

Roadmap and governance

A standard is only worth building on if it outlives the company that started it. That is a governance question before it is a technical one.

Four phases

  1. Groundwork

    Resolve the patent-rights path, select the licence, prepare the repositories, finalise the version 1 specification, and onboard pilot users.

  2. Ecosystem ignition

    Public release of the reference pipeline, the specification and the schema. The open tier goes live, and design partners including universities and innovation labs join.

  3. Standard expansion

    Community contributions, academic partners, deeper regulatory engagement, and the transition to a neutral foundation.

  4. Scale and sustain

    Premium data, support and advanced capabilities sustain the open core, keeping the standard improving for everyone building on it.

Availability is a plan, not a promise

Specs, APIs and data schemas may change during the preview, sometimes with little notice, and the exact order and timing of drops may shift. Lock your package versions — see Access and licence.

Who governs it

CRC is a not-for-profit, founded and funded by RiskThinking.AI, which is the first maintainer of the standard rather than its owner. The open assets — the specification, the reference pipeline, the open reference data, the open impact-function library, and governance itself — are stewarded through a neutral, non-profit foundation as adoption grows.

Governance is lightweight but real: clear spec versioning, a published contribution process, reference implementations and a public roadmap. RiskThinking.AI holds no privileged position in the specification, and competing providers are explicitly welcome to conform and contribute.

Thriving open-source ecosystems are communities, not codebases. The practical test is whether what you build on is still there in five years, which is why the foundation transition is a roadmap item rather than an aspiration.

Where the commercial boundary sits

You can build a serious, end-to-end risk system on the open release. The commercial layer is additive rather than a gate. RiskThinking.AI contributes the standard, the reference pipeline and the open reference data, and licenses these commercially:

  • Bias-corrected and downscaled climate hazard data, including derivative hazards.
  • Physical-asset discovery data.
  • Proprietary impact functions beyond the open starter set.
  • An enterprise edition of the pipeline, running a step ahead of the open release, with priority fixes and newer capabilities.
  • Onboarding, technical support with SLAs, and premium VELO capabilities.

What differs is the data flowing through the machinery, not the machinery doing the work. These offerings sit outside the foundation and compete on merit alongside anyone else who builds against the specification.

Membership is not a teaching edition

It is the same engine that powers large financial institutions, applied to open reference data or to whatever you bring yourself. What is reserved for paying customers is the proprietary high-fidelity data, and the commercial revenue it earns is what sustains the open platform.

Built to pass a security review

There is no opaque third-party binary to fear, because your information-security team can read exactly what runs before it touches your environment. That transparency is the default here, and it is precisely what CISOs and regulators have spent years asking closed vendors to provide.

About the project / Questions

Questions

The ones partners, researchers and engineering teams actually ask.

The project

What is Climate Risk Commons?

An open initiative to give the physical climate-risk field a shared technical foundation. Today the field is fragmented across data vendors, asset taxonomies, hazard models, impact functions and reporting workflows: outputs are hard to compare, assumptions are hard to audit, and providers are hard to integrate.

CRC addresses that by publishing an open standard, a reference pipeline, open data and tooling that anyone can inspect, run and build on. It is a not-for-profit, founded and funded by RiskThinking.AI, with governance moving to a neutral foundation so the standard outlives any single company's commercial decisions.

What is included?
  • The specification: input contracts for climate hazards, physical assets and impact functions, plus the output model.
  • An open reference pipeline that implements the specification end to end.
  • Open reference data, so you can run the whole pipeline and validate results with no commercial relationship.
  • A starter library of open impact functions.
  • Output APIs and SDKs, plus supporting tooling and documentation.
  • Open governance: spec versioning, a published contribution process, and a public roadmap.

Production-grade results at scale typically draw on commercial data and functions, from RiskThinking.AI or from any other conforming provider.

Why use it rather than a vendor?

Comparability, auditability, no lock-in, and a much lower cost to start. See Why CRC for the full argument. The one-line version: it turns physical climate-risk analysis from incompatible vendor black boxes into an open architecture you can inspect, compare and own.

Why is RiskThinking.AI open-sourcing this?

Because a standard is worth more than an island. A shared, inspectable architecture grows the whole market for physical climate-risk analysis, and the company has chosen to compete on data quality and analytics rather than on lock-in. Open adoption also lets prospects prove value with no friction before any commercial conversation, which suits a trust-driven market with slow procurement.

Can I trust its neutrality?

Judge it on the mechanics rather than the intent. The open assets and governance are stewarded through a neutral, non-profit foundation; spec versioning, the contribution process, reference implementations and the roadmap are public; and RiskThinking.AI holds no privileged position in the specification. Competing providers are explicitly welcome to conform and contribute. See Roadmap and governance.

Building on it

What should the ecosystem build?

The standard is designed so value grows with participation. The gaps worth filling are conforming data products published against the input contracts, including from providers who compete with RiskThinking.AI; sector, peril and region-specific impact functions; integrations into risk platforms, portfolio systems, GIS tools and disclosure workflows; alternative pipelines, validation and benchmarking tooling; research that tests and extends the encoded methods; and decision tools for underwriting, lending, asset management, supply chain, infrastructure planning and regulatory reporting.

What does building on it actually look like?
  1. Orient. Read the specification and the three input contracts, then run the reference pipeline end to end on the reference data. A day or two, and you have a working mental model of the whole architecture.
  2. Pick your surface. Data providers publish against an input contract, modellers contribute or sell impact functions, tool builders consume outputs through the APIs, implementers build specialised pipelines.
  3. Build against the contracts. The reference pipeline and open data are your test harness. If your component works with them, it works with the ecosystem.
  4. Validate conformance. Run the validation tooling. Conformance is what makes your product plug-compatible, and it is the claim you put in front of customers.
  5. Contribute back where it makes sense. Fixes, tooling, docs and open impact functions flow through the published contribution process.
  6. Ship and distribute. Offer your conforming product commercially or openly. The standard is your distribution: every adopter is a prospective user of every conforming component.

Throughout, the community channel and stewardship team are available for technical questions, and spec gaps you hit feed directly into the roadmap.

How do researchers and developers benefit?

Researchers get reproducibility by default, because methods published against the standard re-run by anyone on open data in an open pipeline. They get a path from paper to practice, shared infrastructure that removes months of setup, and — for early participants — influence on how version 1 encodes hazards, exposure and vulnerability.

Developers get a stable foundation, where open contracts and versioned specs mean a vendor API change will not strand your work. Distribution comes through the standard, evaluation costs an afternoon rather than a procurement cycle, and there is a direct line to the team.

What does RiskThinking.AI keep as proprietary?

Bias-corrected and downscaled hazard data including derivative hazards, physical-asset discovery data, proprietary impact functions beyond the open starter set, an enterprise edition of the pipeline running a step ahead of the open release, and support with SLAs. The commercial gateway is a data purchase. See where the commercial boundary sits.

Using it

Who should join, and what do partners get?

Five kinds of partner: founding institutions, who get a seat shaping governance and the version 1 standard; data providers, who get distribution through a common contract; developers and implementers, who get a stable foundation; researchers, who get reproducible methods and real-world adoption; and adopters — financial institutions, corporates and public agencies wanting comparable, auditable outputs.

Early partners get direct influence on the version 1 specification, early access to the reference pipeline and sample data, visibility as founding participants, and a direct channel to the stewardship team.

What data can members use?

The open reference datasets provided within the Commons environment, any open dataset you upload, and any data, models or damage functions of your own. Results remain conformant with the standard regardless of whose data powers them.

Are results produced on open data scientifically credible?

Yes. The engine, schema and conformance suite are identical to the commercial stack. Precision depends on the resolution of the data used, and you should report that resolution — exactly as good science already requires.

Can membership be used for commercial work?

No. Membership is free for non-commercial research only. Commercial use of the platform or of RiskThinking.AI's data requires a commercial licence. Note that this is a separate question from the licence on the code itself, which is AGPL-3.0-or-later — see Access and licence.

What if I later need high-fidelity data?

That is a commercial conversation, and the team is happy to have it. Write to academic@riskthinking.ai and it will be routed to the right people.

How do I get involved?

Join the early access preview. Send your GitHub handle and contact email and confirm the early access terms, and you will be onboarded to the repositories, the community channel and the specification. From there, run the reference pipeline on the sample data and report what you find — early feedback directly shapes the public version 1 release.

About the project / Community and resources

Community and resources

Every published artefact, and the people already using them.

Packages and repositories

ArtefactWhereWhat it holds
crc-sdkPyPI, GitHubData access, connectors, geometry, ingest and portfolio workflows
crc-frameworkPyPI, GitHubThe Rust computation core and its Python bindings
crc-docsGitHubNotebooks, headless pipeline twins and the Cologne fixtures
Project siteclimateriskcommons.orgThe initiative, membership and the public case for the standard

Getting in touch

academic@riskthinking.ai
Research access, institutional enrolment, and any conversation about high-fidelity data.
opensource@riskthinking.ai
The open-source community: contributions, conformance questions, and building on the standard.
GitHub issues
Bugs and spec gaps in the packages themselves. Gaps you hit feed into the roadmap.

The most useful early contribution is unglamorous: run the reference pipeline on the sample data and report what happened. See Access and licence for how to join, and Questions for what building on the standard looks like in practice.

Institutions already on board

InstitutionFaculty or group
The Fields InstituteMathematical sciences
University of TorontoArchitecture; Mathematical Finance
London School of EconomicsDoctoral research
Technical University of MunichCAMBIR
Durham UniversityFinance and Management Science
Queen's UniversitySmith School of Business; Institute for Sustainable Finance
University of British ColumbiaSauder School of Business
University of VictoriaPacific Institute for Climate Solutions
University of Western OntarioAll science faculties
Tel Aviv University
University of Zurich
University of Duisburg-Essen
Universidade Federal do Rio Grande do Norte
The Cyprus Academy of Sciences, Letters and Arts
Central Bank of Brazil
Stockholm Environment InstituteSEI Latin America
Jewish Climate Trust
Twintree.Org

The platform behind the standard

The open packages run on open reference data, and on whatever you bring. These figures describe the commercial Climate Digital Twin that the same machinery is built to drive, and they are the coverage a commercial agreement reaches:

DimensionCoverage
Climate hazards modelled50+
Geospatially aligned locations241B
Countries193
Emissions scenariosAll IPCC
Time horizons to 210015
Physical assets, curated7M+
Pathway scenarios, downscaled to 10km2,000+

Quote these carefully

They describe the commercial platform, not the open reference datasets shipped with the packages. A result you produce on open data is conformant and credible, but its precision is the precision of the inputs you used. Report the resolution.