Generalised Carbon-at-Risk portfolio engine (mean–SD scatter, JSON export).
Project description
fig4python
A small, fast, generalised portfolio engine for the Carbon-at-Risk mean–SD analysis from Lee et al. (NCC submission), Figure 4c.
Given an arbitrary list of carbon-removal technologies — each with a unit cost,
per-project survival probability, and tonnes-per-project — together with a
within-tech and between-tech correlation structure, fig4python enumerates
integer portfolios over a grid and returns the mean delivery (μ), standard
deviation (σ), cost, and 5th-percentile delivery (p5) of every portfolio. It
plots the (σ, μ) cloud (Figure 4c-style) and emits a JSON payload suitable for
a web frontend.
The two-technology DACCS + Forest case from
ncc/code/main/figure4/figure4.R is a
parity-tested special case.
Status
v0.1 — engine, plot, CLI, YAML config, JSON export, and parity tests all
working. See docs/slides.md for the design walkthrough.
Verified outputs:
| Config | Portfolios | Feasible | JSON | |
|---|---|---|---|---|
fig4c.yaml (N=2) |
251,001 | 224,758 | 701 K | 90 K |
four_tech.yaml (N=4) |
7,780,611 | 2,633,437 | 723 K | 62 K |
Layout
fig4python/
├── README.md
├── docs/slides.md # design slideshow / spec
├── pyproject.toml # packaging — pip-installable
├── src/fig4python/
│ ├── __init__.py
│ ├── portfolio.py # core engine (numpy, vectorised)
│ ├── plot.py # mean–SD scatter
│ ├── io.py # YAML config + JSON output
│ └── cli.py # `python -m fig4python configs/fig4c.yaml`
├── configs/
│ ├── fig4c.yaml # reproduces Figure 4c (N=2)
│ └── four_tech.yaml # illustrative N=4 example
├── tests/
│ ├── test_parity_fig4c.py # vs frozen R outputs (real cross-language parity)
│ └── test_scenarios.py # editable scenario sweep + faceted figure
├── scripts/
│ └── gen_r_fixture.sh # regenerate R parity constants
└── outputs/
The src/ layout means this folder can be lifted out and published to PyPI
later with no restructuring.
Install
Requires Python ≥ 3.10.
cd fig4python
python3.10 -m venv .venv
source .venv/bin/activate
pip install -e . # editable; add `pytest ipykernel` for dev
Quick start — Python API
import numpy as np
from fig4python import (
Technology, PortfolioProblem,
enumerate_portfolios, min_cost_feasible, plot_mean_sd,
)
problem = PortfolioProblem(
techs=(
Technology("DACCS", cost=500, survival_prob=0.99, q=500),
Technology("Forest", cost= 40, survival_prob=0.40, q=500),
),
rho_within=np.array([0.5, 0.2]),
rho_between=np.array([[1.0, 0.0],
[0.0, 1.0]]),
target=30_000,
max_n=(500, 500),
)
df = enumerate_portfolios(problem) # pandas DataFrame, one row per portfolio
opt = min_cost_feasible(df) # cheapest portfolio with p5 ≥ target
fig = plot_mean_sd(df, problem) # matplotlib Figure
Quick start — CLI
python -m fig4python configs/fig4c.yaml --pdf outputs/fig4c.pdf --json outputs/fig4c.json
python -m fig4python configs/four_tech.yaml --pdf outputs/four_tech.pdf --json outputs/four_tech.json
Interactive scenario sweep
tests/test_scenarios.py is a self-contained,
editable file that defines a list of Scenario objects, solves them all, and
renders a faceted mean–SD figure. Edit the SCENARIOS list to add or change
cases — no other file needs touching.
Three ways to use it:
- Interactively (VS Code Interactive Window / Jupyter): open the file
and run it as one cell (
Shift+Enteron the whole file, or split into# %%cells). The figure is bound tofigat the bottom and renders inline. - As a script:
python tests/test_scenarios.py— prints a summary table of every scenario's optimum. - Under pytest:
pytest tests/test_scenarios.py— runs comparative-statics sanity checks on the optima (independent < correlated, cross-corr hurts diversification, etc).
Worked example: four technologies
configs/four_tech.yaml defines a 4-tech portfolio
spanning the cost / permanence spectrum:
| Tech | $/project | survival p | role |
|---|---|---|---|
| DACCS | 500 | 0.99 | engineered, near-certain |
| BiocharS | 150 | 0.85 | mid-cost, high permanence |
| Forest | 40 | 0.40 | cheap, fire-exposed |
| Soil | 20 | 0.25 | cheap, low permanence |
Within-tech correlations (common shocks across same-type projects):
[0.50, 0.30, 0.20, 0.15].
Between-tech correlation matrix (illustrative, not calibrated — captures shared climate exposure; DACCS is largely independent, Forest and Soil share a strong drought link):
DACCS BiocharS Forest Soil
DACCS [ 1.00 0.05 0.00 0.00 ]
BiocharS [ 0.05 1.00 0.10 0.10 ]
Forest [ 0.00 0.10 1.00 0.30 ]
Soil [ 0.00 0.10 0.30 1.00 ]
Target: 20,000 tCO₂ at 95% confidence. Grid: max_n = [40, 50, 60, 60] →
7.78M portfolios, of which 2.63M are feasible.
Result:
Min-cost feasible: n_DACCS=7, n_BiocharS=50, n_Forest=50, n_Soil=0
cost=$13,000 mu=34,715 p5=20,034
The optimum mixes engineered (DACCS), high-permanence biological (BiocharS), and cheap fire-exposed (Forest), but skips Soil entirely — its drought correlation with Forest makes it a poor diversifier once Forest is in the basket. This is exactly the kind of insight a generalised mean–SD engine makes visible that the two-tech version cannot.
Output sizes
For website use, both outputs are tuned to stay small:
- JSON is columnar (dict-of-lists), rounded to integer tonnes / dollars,
feasible-only by default, and downsampled to a hard cap (default 20k
feasible / 2k infeasible). Pass
include_infeasible=Trueto keep a sparse background cloud. The 7.8M-portfolio 4-tech run fits in ~720 KB. - PDF rasterizes the scatter at 150 dpi, so file size is independent of the number of points. Both example runs land under 100 KB.
Design goals
- General N: arbitrary number of technologies.
- Full correlation structure: per-tech within correlation + N×N between-tech matrix.
- Fast: single vectorised pass (numpy einsum), no Python loop over portfolios.
- Clear: ~200 LOC core, no hidden state, no inheritance.
- Portable: pip-installable, standalone — no dependency on the parent repo.
- Web-ready: emits a compact JSON payload.
Relationship to the R code
Formulas mirror
ncc/code/0_funcs/portfolio_funcs.R
exactly. Two layers of test guard against drift:
- Real R parity (
test_r_parityintests/test_parity_fig4c.py) — frozen numerical constants generated by actually runningportfolio_stats()in R, including a non-zero between-tech case. The Python engine must reproduce μ, σ, p5, and cost to ~1e-9. Regenerate the constants by runningscripts/gen_r_fixture.shif the R formula ever changes. - Formula pinning (
test_formula_pinning) — re-implements the R algebra in plain numpy and asserts the engine matches it. Catches refactor regressions even when R isn't installed.
Comparative-statics sanity checks
The scenario sweep verifies the engine moves the right way under standard shocks:
| Scenario | Optimum | Cost |
|---|---|---|
| fig4c, independent (ρ=0) | 0 DACCS + 177 Forest | $7,080 |
| fig4c, correlated (ref) | 61 DACCS + 45 Forest | $32,300 |
| fig4c, correlated + 0.3 cross-tech | 69 DACCS + 0 Forest | $34,500 |
| fig4c, target = 50k | 102 DACCS + 64 Forest | $53,560 |
| 3-tech (DACCS+Biochar+Forest) | 0 D + 60 BiocharS + 61 Forest | $11,440 |
| 4-tech, zero cross-corr | 0 D + 42 BiocharS + 42 Forest + 57 Soil | $9,120 |
Reading the table: dropping within-tech correlation makes diversification
unnecessary (forest-only); adding cross-tech correlation flips the optimum to
all-DACCS because forest no longer hedges; the 4-tech case brings Soil back
when its drought link with Forest is severed. All comparative statics check
out — see tests/test_scenarios.py.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fig4python-0.1.0.tar.gz.
File metadata
- Download URL: fig4python-0.1.0.tar.gz
- Upload date:
- Size: 20.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f9114afe6f66b3c4c98c3e2ff1a45bd1e4347eef2eb1ca4b9164f0385fd5175
|
|
| MD5 |
0d96ece1cd6d301761e03e2caa28d5ab
|
|
| BLAKE2b-256 |
f9b3f823a963ef2881b637c389ef122d7dcf1d8fce644935581020c64f05fc51
|
File details
Details for the file fig4python-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fig4python-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1a08a71275f0cd265200bc02c970c41d2ad0eceb315efe2e16f4ae845a4f972
|
|
| MD5 |
470c97ce26ca2b8341f868e931371d6a
|
|
| BLAKE2b-256 |
9ce61f423e1e1791193a2e4b8d1c775eb9ba0efb6e6a9c7f987eb8919510fc90
|