Skip to main content

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 PDF
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+Enter on the whole file, or split into # %% cells). The figure is bound to fig at 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=True to 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:

  1. Real R parity (test_r_parity in tests/test_parity_fig4c.py) — frozen numerical constants generated by actually running portfolio_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 running scripts/gen_r_fixture.sh if the R formula ever changes.
  2. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fig4python-0.1.0.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fig4python-0.1.0-py3-none-any.whl (13.2 kB view details)

Uploaded Python 3

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

Hashes for fig4python-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3f9114afe6f66b3c4c98c3e2ff1a45bd1e4347eef2eb1ca4b9164f0385fd5175
MD5 0d96ece1cd6d301761e03e2caa28d5ab
BLAKE2b-256 f9b3f823a963ef2881b637c389ef122d7dcf1d8fce644935581020c64f05fc51

See more details on using hashes here.

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

Hashes for fig4python-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f1a08a71275f0cd265200bc02c970c41d2ad0eceb315efe2e16f4ae845a4f972
MD5 470c97ce26ca2b8341f868e931371d6a
BLAKE2b-256 9ce61f423e1e1791193a2e4b8d1c775eb9ba0efb6e6a9c7f987eb8919510fc90

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page