Skip to content
Browse by subject:
Applied and bridge studies study record
Hollow Win theory & mechanisms

Electronic Waste: A System Asset Pricing Model

STATUS · Manuscript in progressSSRN · Not yet posted
Open teaching deckSSRN — not yet postedPublication record
MECHANISM
Identify the incentive structure and the condition that would falsify the claim.
RULE CHANGE
Read the intervention only after the paper shows how the current payoff space fails to support system welfare.
READER USE
Use the summary to see where private gain creates system exposure, then check the study record.
Reading βW
βW means annual system-welfare loss divided by annual industry revenue Π. Revenue is the denominator, never profit, earnings, or net income; ΔW and Π must use the same domain, same time period, and same activity boundary. See the βW methodology manual.
Contribution — what this adds to the conversation
Provides a replicable, code-backed SAPM calibration with 100,000 Monte Carlo draws. Introduces the Basel Convention Evasion Theorem as the 22nd impossibility result in the SAPM program. Enables cross-domain comparison with other SAPM domains.
WHAT'S NEW · Novel application of SAPM to e-waste, integrating six welfare channels under explicit uncertainty. The Basel Evasion Theorem is a new impossibility result. Prior literature treats channels separately; the paper unifies them.

The paper applies the System Asset Pricing Model (SAPM) to global electronic waste, finding that every dollar of industry revenue destroys $6.60 in system welfare (no canonical estimate is assigned), yielding a net welfare cost of $690 billion per year. The Basel Convention Evasion Theorem proves a welfare-destruction floor of βW≥4.2 under current voluntary mechanisms, requiring sovereign intervention to breach.

Monte Carlo methodology & code
100,000 draws · 6 channels · seed 42
Every βW in this study is produced by one reproducible engine: 100,000 correlated draws across 6 welfare-cost channels, coupled by a Cholesky correlation of ρ = 0.3, with each channel drawn from its own distribution over a low/mid/high range. βWis the channel welfare cost summed and divided by annual industry revenue Π. The exact inputs and the shared runner are below.
Median βW
6.59
ΔW ÷ Π
90% interval
3.49–13.95
p5 – p95
P(βW < 1)
0%
draws below 1
Mean βW
6.71
draw mean
Draws
100,000
Monte Carlo
Seed
42
reproducible
Correlation ρ
0.3
cross-channel
Annual revenue Π
$1,050B
denominator
Channel inputs · $B/yr
ChannelDistributionLowMidHighWeight
Planned obsolescencelog-normal2,000.023,000.044,500.050.3
Toxic healthlog-normal1,000.011,600.022,500.030.25
Environmentallog-normal600.011,000.011,600.020.15
Lost materialsnormal4562800.1
Carbonlog-normal400700.011,100.010.12
Governancelog-normal250450.01700.010.08
+Config JSON· pipeline/mc_configs/ewaste.json
{
  "paper_id": "ewaste",
  "seed": 42,
  "draws": 100000,
  "correlation": 0.3,
  "annual_revenue": 1050,
  "channels": {
    "C1_planned_obsolescence": {
      "dist": "lognormal",
      "low": 2000.02,
      "mid": 3000.04,
      "high": 4500.05,
      "weight": 0.3
    },
    "C2_toxic_health": {
      "dist": "lognormal",
      "low": 1000.01,
      "mid": 1600.02,
      "high": 2500.03,
      "weight": 0.25
    },
    "C3_environmental": {
      "dist": "lognormal",
      "low": 600.01,
      "mid": 1000.01,
      "high": 1600.02,
      "weight": 0.15
    },
    "C4_lost_materials": {
      "dist": "normal",
      "low": 45,
      "mid": 62,
      "high": 80,
      "weight": 0.1
    },
    "C5_carbon": {
      "dist": "lognormal",
      "low": 400,
      "mid": 700.01,
      "high": 1100.01,
      "weight": 0.12
    },
    "C6_governance": {
      "dist": "lognormal",
      "low": 250,
      "mid": 450.01,
      "high": 700.01,
      "weight": 0.08
    }
  }
}
+Shared Python runner· pipeline/monte_carlo.py · same engine for every domain
The engine below is identical across all domains — correlated draws via Cholesky decomposition of the ρ matrix, per-channel lognormal / normal / triangular sampling, then βW = Σ channel welfare cost ÷ Π. Only the JSON config above changes per domain.
#!/usr/bin/env python3
"""
Monte Carlo simulation engine for SAPM papers.

Takes a per-domain JSON config, runs N draws with correlated channel
uncertainties, and produces reproducible results for paper injection.

Usage:
    python -m pipeline.monte_carlo tobacco
    python -m pipeline.monte_carlo --config pipeline/mc_configs/tobacco.json

Outputs:
    output/<paper_id>/mc_results.json   — all computed statistics
    output/<paper_id>/mc_histogram.json — binned histogram data for figure generation
"""
import json
import os
import sys
import numpy as np
from pathlib import Path
from scipy import stats

PROJECT_ROOT = Path(__file__).parent.parent
CONFIG_DIR = PROJECT_ROOT / "pipeline" / "mc_configs"
OUTPUT_DIR = PROJECT_ROOT / "output"
LEGACY_DIAGNOSTIC_ENV = "SAPM_ALLOW_LEGACY_DIAGNOSTIC"


def load_config(paper_id: str) -> dict:
    """Load the MC config for a domain."""
    config_path = CONFIG_DIR / f"{paper_id}.json"
    if not config_path.exists():
        raise FileNotFoundError(f"No MC config found: {config_path}")
    return json.loads(config_path.read_text(encoding="utf-8"))


def generate_correlated_draws(config: dict, rng: np.random.Generator) -> np.ndarray:
    """Generate correlated channel draws using Cholesky decomposition.

    Returns array of shape (n_draws, n_channels) with channel welfare costs.
    """
    channels = config["channels"]
    n_draws = config.get("draws", 100_000)
    rho = config.get("correlation", 0.3)
    n_ch = len(channels)

    # Build correlation matrix
    corr = np.full((n_ch, n_ch), rho)
    np.fill_diagonal(corr, 1.0)

    # Cholesky decomposition for correlation
    L = np.linalg.cholesky(corr)

    # Generate independent standard normal draws
    Z = rng.standard_normal((n_draws, n_ch))

    # Apply correlation structure
    Z_corr = Z @ L.T

    # Transform to channel-specific distributions
    results = np.zeros((n_draws, n_ch))
    channel_names = list(channels.keys())

    for i, (name, params) in enumerate(channels.items()):
        dist = params.get("dist", "lognormal")
        low = params["low"]
        mid = params["mid"]
        high = params["high"]

        # Convert uniform quantiles from correlated normals
        U = stats.norm.cdf(Z_corr[:, i])

        if dist == "lognormal":
            # Fit log-normal: median = mid, and 5th/95th pctiles ≈ low/high
            mu = np.log(mid)
            # Use (high/mid) ratio to estimate sigma
            # P95 = exp(mu + 1.645*sigma) → sigma = ln(high/mid) / 1.645
            sigma = np.log(high / mid) / 1.645
            results[:, i] = stats.lognorm.ppf(U, s=sigma, scale=mid)

        elif dist == "normal":
            # mid = mean, (high - low) ≈ 3.29 * sigma (5th to 95th)
            sigma = (high - low) / (2 * 1.645)
            results[:, i] = stats.norm.ppf(U, loc=mid, scale=sigma)

        elif dist == "triangular":
            # Triangular with mode at mid
            c = (mid - low) / (high - low)
            results[:, i] = stats.triang.ppf(U, c, loc=low, scale=high - low)

        elif dist == "uniform":
            results[:, i] = stats.uniform.ppf(U, loc=low, scale=high - low)

        else:
            raise ValueError(f"Unknown distribution '{dist}' for channel {name}")

    return results, channel_names


def compute_beta_w(channel_draws: np.ndarray, annual_revenue: float,
                   channel_weights: list = None) -> np.ndarray:
    """Compute β_W for each draw.

    β_W = total_welfare_cost / annual industry revenue

    If channel_weights are provided, apply them. Otherwise, sum all channels.
    """
    if channel_weights:
        weights = np.array(channel_weights)
        total_cost = np.sum(channel_draws * weights, axis=1)
    else:
        total_cost = np.sum(channel_draws, axis=1)

    return total_cost / annual_revenue


def build_histogram(beta_draws: np.ndarray, n_bins: int = 80) -> list:
    """Build histogram data for figure generation."""
    counts, bin_edges = np.histogram(beta_draws, bins=n_bins)
    bins = []
    for i in range(len(counts)):
        bins.append({
            "bin_low": round(float(bin_edges[i]), 3),
            "bin_high": round(float(bin_edges[i + 1]), 3),
            "bin_mid": round(float((bin_edges[i] + bin_edges[i + 1]) / 2), 3),
            "count": int(counts[i]),
            "density": round(float(counts[i] / len(beta_draws)), 6),
        })
    return bins


def build_sensitivity_matrix(config: dict, rng: np.random.Generator) -> list:
    """Build VSL × double-counting sensitivity matrix.

    Varies the largest channel (typically mortality/VSL) and applies
    double-counting adjustments to produce a grid of β_W values.
    """
    # Find the largest channel (assume it's the VSL/mortality channel)
    channels = config["channels"]
    ch_names = list(channels.keys())
    ch_mids = [channels[n]["mid"] for n in ch_names]
    largest_idx = ch_mids.index(max(ch_mids))
    largest_name = ch_names[largest_idx]
    largest_mid = ch_mids[largest_idx]

    annual_revenue = config.get("annual_revenue", config.get("revenue", config["private_payoff"]))

    # VSL multipliers (fraction of central estimate)
    vsl_multipliers = [0.5, 0.75, 1.0, 1.25, 1.5]
    # Double-counting adjustments
    dc_adjustments = [0.0, 0.10, 0.20, 0.30, 0.40]

    matrix = []
    for vsl_mult in vsl_multipliers:
        row = {"vsl_multiplier": vsl_mult, "values": {}}
        for dc_adj in dc_adjustments:
            # Compute deterministic β_W under these parameters
            total = 0
            for i, name in enumerate(ch_names):
                val = ch_mids[i]
                if i == largest_idx:
                    val *= vsl_mult
                total += val
            # Apply double-counting reduction
            total *= (1 - dc_adj)
            beta = round(total / annual_revenue, 2)
            row["values"][f"dc_{int(dc_adj * 100)}pct"] = beta
        matrix.append(row)

    return matrix


def run_monte_carlo(paper_id: str):
    """Run the full Monte Carlo simulation for a domain paper."""
    if os.environ.get(LEGACY_DIAGNOSTIC_ENV) != "1":
        raise RuntimeError(
            "Legacy synthetic MC runner is disabled. Use ppt-companion/scripts/run_admitted_mc_20260715.py "
            "with an admitted-mc-packet/1 packet. Set SAPM_ALLOW_LEGACY_DIAGNOSTIC=1 only for "
            "explicit historical diagnostics."
        )
    config = load_config(paper_id)
    rng = np.random.default_rng(seed=config.get("seed", 42))

    n_draws = config.get("draws", 100_000)
    annual_revenue = config.get("annual_revenue", config.get("revenue", config["private_payoff"]))
    channels = config["channels"]

    print(f"Monte Carlo simulation: {paper_id}")
    print(f"  Channels: {len(channels)}")
    print(f"  Draws: {n_draws:,}")
    print(f"  Correlation ρ: {config.get('correlation', 0.3)}")
    print(f"  Annual revenue denominator: ${annual_revenue:,}B")

    # Generate draws
    channel_draws, channel_names = generate_correlated_draws(config, rng)

    # Compute β_W
    weights = config.get("channel_weights")
    beta_draws = compute_beta_w(channel_draws, annual_revenue, weights)

    # Core statistics
    median_beta = float(np.median(beta_draws))
    mean_beta = float(np.mean(beta_draws))
    p5 = float(np.percentile(beta_draws, 5))
    p95 = float(np.percentile(beta_draws, 95))
    p1 = float(np.percentile(beta_draws, 1))
    p99 = float(np.percentile(beta_draws, 99))
    p_below_1 = float(np.mean(beta_draws < 1.0))
    std_beta = float(np.std(beta_draws))

    # Impossibility floor verification (if specified)
    floor = config.get("impossibility_floor")
    p_below_floor = float(np.mean(beta_draws < floor)) if floor else None

    # Total welfare cost statistics
    total_cost = np.sum(channel_draws, axis=1)
    pi_sa = total_cost - annual_revenue

    # Per-channel statistics
    channel_stats = {}
    for i, name in enumerate(channel_names):
        ch_draws = channel_draws[:, i]
        channel_stats[name] = {
            "median": round(float(np.median(ch_draws)), 1),
            "mean": round(float(np.mean(ch_draws)), 1),
            "p5": round(float(np.percentile(ch_draws, 5)), 1),
            "p95": round(float(np.percentile(ch_draws, 95)), 1),
            "std": round(float(np.std(ch_draws)), 1),
        }

    # Histogram data
    histogram = build_histogram(beta_draws)

    # Sensitivity matrix
    sensitivity = build_sensitivity_matrix(config, rng)

    # Assemble results
    results = {
        "paper_id": paper_id,
        "config_file": f"pipeline/mc_configs/{paper_id}.json",
        "seed": config.get("seed", 42),
        "n_draws": n_draws,
        "n_channels": len(channels),
        "correlation_rho": config.get("correlation", 0.3),
        "annual_revenue_B": annual_revenue,
        "private_payoff_B": annual_revenue,
        "beta_w": {
            "median": round(median_beta, 2),
            "mean": round(mean_beta, 2),
            "std": round(std_beta, 2),
            "p1": round(p1, 2),
            "p5": round(p5, 2),
            "p95": round(p95, 2),
            "p99": round(p99, 2),
            "ci_90": [round(p5, 1), round(p95, 1)],
            "ci_99": [round(p1, 1), round(p99, 1)],
            "p_below_1": round(p_below_1, 6),
            "p_below_1_pct": f"{p_below_1 * 100:.4f}%",
        },
        "impossibility_floor": {
            "floor_value": floor,
            "p_below_floor": round(p_below_floor, 6) if p_below_floor is not None else None,
            "p_below_floor_pct": f"{p_below_floor * 100:.4f}%" if p_below_floor is not None else None,
        } if floor else None,
        "welfare_cost": {
            "total_median_B": round(float(np.median(total_cost)), 1),
            "total_mean_B": round(float(np.mean(total_cost)), 1),
            "total_p5_B": round(float(np.percentile(total_cost, 5)), 1),
            "total_p95_B": round(float(np.percentile(total_cost, 95)), 1),
        },
        "pi_sa": {
            "median_B": round(float(np.median(pi_sa)), 1),
            "mean_B": round(float(np.mean(pi_sa)), 1),
            "p5_B": round(float(np.percentile(pi_sa, 5)), 1),
            "p95_B": round(float(np.percentile(pi_sa, 95)), 1),
        },
        "channel_statistics": channel_stats,
        "sensitivity_matrix": sensitivity,
    }

    # Save results
    out_dir = OUTPUT_DIR / paper_id
    out_dir.mkdir(parents=True, exist_ok=True)

    results_path = out_dir / "mc_results.json"
    results_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
    print(f"\n  Results: {results_path}")

    hist_path = out_dir / "mc_histogram.json"
    hist_path.write_text(json.dumps(histogram, indent=2), encoding="utf-8")
    print(f"  Histogram: {hist_path}")

    # Print summary
    print(f"\n  === RESULTS ===")
    print(f"  β_W median:  {results['beta_w']['median']}")
    print(f"  β_W mean:    {results['beta_w']['mean']}")
    print(f"  90% CI:      [{results['beta_w']['ci_90'][0]}, {results['beta_w']['ci_90'][1]}]")
    print(f"  P(β_W < 1):  {results['beta_w']['p_below_1_pct']}")
    if floor:
        print(f"  P(β_W < {floor}): {results['impossibility_floor']['p_below_floor_pct']}")
    print(f"  Π^SA median: −${abs(results['pi_sa']['median_B']):,.0f}B/yr")

    return results


def main():
    if len(sys.argv) < 2:
        print("Usage: python -m pipeline.monte_carlo <paper_id>")
        sys.exit(1)

    paper_id = sys.argv[1]
    run_monte_carlo(paper_id)


if __name__ == "__main__":
    main()
Reproduce: python -m pipeline.monte_carlo ewaste · outputs output/ewaste/mc_results.json