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-sdkpython -m pip install crc-sdkgit clone https://github.com/RiskThinking/crc-docs
cd crc-docs && uv syncInstalling 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.
| Piece | What it is | Version |
|---|---|---|
| crc-framework | Rust computation core with typed Python bindings. Distributions, explicit curve fitting, impact transforms, microscores, VaR and CVaR, H3 geography lookups. | See PyPI |
| crc-sdk | The higher-level Python interface: DuckDB and Arrow query engine, storage providers, hazard ingest, geometry and H3 tooling, portfolio workflows. | See PyPI |
| crc-docs | Worked 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.
| Input | What it looks like in code | Where it comes from |
|---|---|---|
| Climate hazard data | A canonical Parquet file of fitted curves, one row per hazard, horizon, pathway and H3 cell | Ingested from open rasters, or supplied by any conforming provider |
| Physical assets | An Arrow table or Parquet file with an id and either coordinates or an H3 cell_index | Your portfolio, loan book or asset register |
| Impact functions | A callable or a registry-backed transform mapping exposure to damage | The 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 oncrc-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,.fittingand.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.pyuv 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-frameworkWhat 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.
| Extra | Adds | You need it for |
|---|---|---|
| zarr | zarr | OSClimateProvider and ZarrRaster — OS-Climate Zarr raster ingest |
| raster | rasterio | GeoTiffRaster — GeoTIFF and COG ingest, streamed through GDAL's VSI layer |
| netcdf | h5netcdf, h5py | NetCDFRaster — NetCDF and CF ingest |
| geometry | h3, h3ronpy, shapely | H3Indexer, intersecting_cells, cell_polygon, vectorised batch H3 over Arrow, raster-to-H3 sampling |
| agriculture | icechunk, Zarr v3, pyproj | USDA CDL Icechunk scans. Check the Python and platform guidance for extra requirements |
| test | mypy, pytest, ruff | Development 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 valueEnvironment 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
1for 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.
-
Get the repository and the environment
git clone https://github.com/RiskThinking/crc-docs cd crc-docs uv syncuv syncreads the lockfile, so you get the exact dependency set the notebooks were executed against. -
Run a whole analysis before reading any of it
uv run python pipelines/asset_portfolio_pipeline.pyOutputs 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. -
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'sdata/cache lands beside it rather than in your working directory. -
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_rp25throughvalue_rp1000. The column namesasset_id,longitude,latitudeandcell_indexare inferred, so a conventional schema needs no configuration at all. -
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_rp100is 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.
| # | Notebook | Pipeline twin | What it shows |
|---|---|---|---|
| 1 | hurdle_fit_primer | — | Reconstruct a canonical hurdle curve and read its sample diagnostics |
| 2 | asset_portfolio_evaluation | asset_portfolio_pipeline.py | Fluent HazardDataset portfolio evaluation at return periods |
| 3 | portfolio_impact | portfolio_impact_pipeline.py | Event-aligned PiecewiseLinearImpact through to currency damage |
| 4 | portfolio_risk_metrics | portfolio_risk_pipeline.py | Microscores through to portfolio VaR and CVaR, with attribution |
| 5 | multi_scenario_portfolio | multi_scenario_pipeline.py | Historical 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.
| Notebook | Pipeline twin | What it shows |
|---|---|---|
| flood_risk_by_province | flood_admin_pipeline.py | OS-Climate to H3, joined to administrative boundaries, enriched with Overture places, written out as PMTiles |
| jrc_global_flood_hazard | jrc_flood_pipeline.py | Streamed 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 period | q (upper tail) | Output column |
|---|---|---|
| 25 | 0.96 | value_rp25 |
| 50 | 0.98 | value_rp50 |
| 100 | 0.99 | value_rp100 |
| 250 | 0.996 | value_rp250 |
| 500 | 0.998 | value_rp500 |
| 1000 | 0.999 | value_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.
| Class | Use it when |
|---|---|
| EmpiricalDistribution | You hold raw observations |
| TabulatedDistribution | You hold explicit probability and value pairs, such as return-period knots from a published hazard map |
| FittedDistribution | You know the parametric family and its parameters |
| HurdleDistribution | A 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 |
| PointMassDistribution | The 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
| Function | Input | Family | Diagnostics |
|---|---|---|---|
| fit_distribution | Raw observations or an EmpiricalDistribution | Selected by KS p-value, or named | KS, RMSE, R-squared |
| fit_quantiles | Probability and value knots | Explicit, required | Residuals in value space |
| fit_hurdle_quantiles | Knots plus a known point mass | Explicit, required | Residuals 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
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
| Call | What it does | Use it when |
|---|---|---|
| impact.evaluate(values) | Evaluates event-aligned exposure values. Input shape and order are preserved, including for decreasing or non-monotonic callables | Return 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 distribution | You 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_indexis 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.
| Factors | Branches | Comfort |
|---|---|---|
| 4 | 16 | Trivial |
| 10 | 1,024 | Fine |
| 16 | 65,536 | Set a limit |
| 24 | 16,777,216 | Rethink 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
-> ParquetThe 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_kind | curve_type | What is populated | Since |
|---|---|---|---|
| fitted | The parametric family | Scalar curve parameters. curve_shape is null for Gumbel families, which have no shape parameter | 1.0 |
| hurdle | The parametric family | Scalar parameters plus atom probability and location | 1.0 |
| point_mass | point_mass | Zero scale, probability one at curve_location. Constant across the whole source support | 1.1 |
| tabulated | linear_probability | The list columns curve_probabilities and curve_values. Scalar parameters are null | 1.2 |
| no_data | An explicit scientific reason code | Nothing. All parameter fields are null, and batch quantile evaluation returns nulls for these rows | 1.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.
| Recorded | Why you need it |
|---|---|
| Schema version | Tells you which curve kinds can appear |
| H3 resolution | One uncompacted resolution per dataset. Asset points are converted to it |
| Non-exceedance probability convention | Which tail is the damaging one |
| Source probability support | Where interpolation ends and extrapolation begins |
| Value unit and semantics | Metres of depth, an index value, a ratio |
| WKB CRS | The coordinate reference system of any stored source geometry |
| Producer and source provenance | Who made it and from what |
| Curve-fit policy and creation version | How 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_idandspatial_match, which is eitherexact_geometryorh3_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=True | ordered=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.
| Sample | Mode | Peak RSS | File size | Time |
|---|---|---|---|---|
| 2,148,497 rows | streaming | 656 MB | 45 MB | 16.03 s |
| 2,148,497 rows | ordered | 1.69 GB | 35 MB | 16.38 s |
| 300,000 rows | streaming | 282 MB | 6.3 MB | 3.01 s |
| 300,000 rows | ordered | 525 MB | 4.9 MB | 2.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.
| Dataset | Version | Layout |
|---|---|---|
| GLOFAS | 2.1.2 | JRC's tiled global layout |
| EFAS | 3.1.1 | Nine 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_climatePlain 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.
| Reader | Extra | Notes |
|---|---|---|
| GeoTiffRaster | raster | GeoTIFF 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 |
| ZarrRaster | zarr | Zarr stores, including OS-Climate |
| NetCDFRaster | netcdf | NetCDF and CF |
| DuckDBConnection | baseline | Connection 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_threadsandduckdb_threadsdefault 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
| Source | Used for |
|---|---|
| OS-Climate | Return-period hazard rasters, in Zarr |
| JRC LISFLOOD, GLOFAS, EFAS | Global and European flood hazard |
| JRC EDO | Soil Moisture Index, for drought |
| geoBoundaries | Administrative boundaries for spatial joins |
| Overture | Places, to enrich spatial results |
| USDA CDL | United States crop land cover, through Icechunk |
| Fields of The World | Global 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 pass | What happens |
|---|---|
| An Arrow table | Registered directly with the engine |
A Path | Read as an asset Parquet file |
| A string | Treated as caller-supplied DuckDB SQL |
| Assets with H3 cells | Pass 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
- Point assets are converted to the H3 resolution recorded by the canonical dataset.
- The integer H3 join produces a conservative superset of candidates.
- Where source WKB is present, the join is refined with
ST_Covers(source_geometry, asset_point). - 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
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.
| Helper | Does |
|---|---|
| H3Indexer | DuckDB-native H3 polyfill |
| polyfill_wkb | Vectorised batch polyfill over Arrow |
| expand_polygon_candidates | Candidate expansion for boundary coverage |
| intersecting_cells | Cells intersecting a geometry |
| cell_polygon | The polygon for a cell |
| pixel_grid_resolution | Choose an H3 resolution for a pixel grid |
| sample_grid_to_h3 | Sample a raster grid onto cells |
| write_exploded_coverage | Write exploded coverage output |
| LookupCatalog | Optional 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 defaultSpatial 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.
| Variable | Overrides |
|---|---|
| CRC_DUCKDB_THREADS | The derived thread count |
| CRC_DUCKDB_MEMORY | The memory limit |
| CRC_DUCKDB_BYTES_PER_THREAD_GIB | The per-thread memory assumption |
| CRC_DUCKDB_WORK_DIR | The spill and temp directory |
| CRC_DUCKDB_PROFILE | Set 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 tomode="offline"so a network wobble cannot kill hour three.- Point
work_dirat 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.
ImportErrornaming an extra- Not a broken install. Extras are imported lazily and the error names exactly what to add.
tippecanoeis not pip-installable- PMTiles needs
tippecanoeandtile-joinonPATH. Check withrequire_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=Trueonly 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_distributionis an error. Usefit_quantiles. - No point mass is inferred for you
- Zero-heavy hazards need an explicit
HurdleFitPolicyoratom_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_shapeis 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
ImpactContextColumnsare read from every asset whether or not they are kept as output passthrough columns. - Units are yours to declare
value_unitandvalue_semanticsare not decoration. They land in metadata and they are how the next person reads your column.
At scale
- Branches are exponential
compute_spanning_setgrows as2**n. Setmax_branches.- Ordered writes cost memory
- A global sort roughly tripled peak RSS on a two-million-row sample. Use
ordered=Falsewhen containers are tight. - Engine limits are not process limits
memory_limitcaps 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_THREADSif 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 --lockedContributing
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 a | The surface to build on |
|---|---|
| Data provider | Publish hazard, asset or exposure data against the CRC input contracts |
| Modeller | Contribute sector, peril or region-specific damage functions to the open library, or offer them commercially against the spec |
| Tool builder | Consume outputs through the APIs; build integrations into risk platforms, GIS, portfolio and disclosure workflows |
| Implementer | Alternative pipelines, validation and conformance tooling, benchmarking suites |
| Researcher | Test, 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
| Failure | What it costs you |
|---|---|
| Numbers you cannot defend | Methodology sits inside a vendor black box, closed to the regulators, auditors and boards now asking how the figures were produced |
| Results that do not compare | Every vendor encodes assets, hazards and scenarios differently, so similar portfolios diverge and supervisors cannot aggregate across the system |
| A costly door, both ways | Evaluation takes months and six figures, and proprietary formats make exit expensive, pricing smaller players out before they begin |
| Science that keeps restarting | Without a shared foundation, researchers rebuild the same infrastructure instead of advancing stochastic and multi-hazard methods |
What an open standard changes
| The closed model is | CRC is |
|---|---|
| Black-box methodology you are not permitted to inspect | Inspectable methodology, so you own your models |
| Assets, hazards and scenarios encoded differently by every vendor | One shared schema, so results compare across teams and firms |
| Single-vendor point estimates that manufacture false precision | A stochastic, full-distribution engine that makes uncertainty legible |
| Cost and expertise walls that shut out smaller institutions and emerging markets | Open, 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
-
Groundwork
Resolve the patent-rights path, select the licence, prepare the repositories, finalise the version 1 specification, and onboard pilot users.
-
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.
-
Standard expansion
Community contributions, academic partners, deeper regulatory engagement, and the transition to a neutral foundation.
-
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?
- 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.
- 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.
- 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.
- 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.
- Contribute back where it makes sense. Fixes, tooling, docs and open impact functions flow through the published contribution process.
- 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
| Artefact | Where | What it holds |
|---|---|---|
| crc-sdk | PyPI, GitHub | Data access, connectors, geometry, ingest and portfolio workflows |
| crc-framework | PyPI, GitHub | The Rust computation core and its Python bindings |
| crc-docs | GitHub | Notebooks, headless pipeline twins and the Cologne fixtures |
| Project site | climateriskcommons.org | The 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
| Institution | Faculty or group |
|---|---|
| The Fields Institute | Mathematical sciences |
| University of Toronto | Architecture; Mathematical Finance |
| London School of Economics | Doctoral research |
| Technical University of Munich | CAMBIR |
| Durham University | Finance and Management Science |
| Queen's University | Smith School of Business; Institute for Sustainable Finance |
| University of British Columbia | Sauder School of Business |
| University of Victoria | Pacific Institute for Climate Solutions |
| University of Western Ontario | All 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 Institute | SEI 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:
| Dimension | Coverage |
|---|---|
| Climate hazards modelled | 50+ |
| Geospatially aligned locations | 241B |
| Countries | 193 |
| Emissions scenarios | All IPCC |
| Time horizons to 2100 | 15 |
| Physical assets, curated | 7M+ |
| Pathway scenarios, downscaled to 10km | 2,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.