Browse by subject:
Applied and bridge studies study record
Security, crime & defenseApplying the System Asset Pricing Model to US Civilian Firearms: Measuring the System Welfare Cost of Lethality-by-Design
STATUS · Manuscript in progressSSRN · Not yet posted
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.
From diagnosis to action
Contribution — what this adds to the conversation
Provides a replicable beta estimate, six-channel decomposition, and cross-domain comparison. Contributes to the SAPM empirical canon.
WHAT'S NEW · First SAPM calibration for a constitutionally protected consumer product; formal impossibility theorem candidate. Method is applied, not invented.
This study applies the System Asset Pricing Model (SAPM) to estimate the system welfare cost of the U.S. civilian firearms industry. Using CDC mortality data and six welfare channels, the analysis finds a system beta of 21.98—meaning each dollar of industry revenue destroys about $22 of system welfare. The total annual welfare cost is $509.9 billion against $10 billion in industry revenue, yielding a system-adjusted payoff of -$499.9 billion. The paper also formalizes a candidate impossibility theorem: three axioms (lethality-by-design, constitutional access mandate, and regulatory capture) may jointly preclude welfare optimization.
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
21.98
ΔW ÷ Π
90% interval
14.17–34.94
p5 – p95
P(βW < 1)
0%
draws below 1
Mean βW
22.20
draw mean
Draws
100,000
Monte Carlo
Seed
42
reproducible
Correlation ρ
0.3
cross-channel
Annual revenue Π
$23.2B
denominator
Channel inputs · $B/yr
| Channel | Distribution | Low | Mid | High | Weight |
|---|---|---|---|---|---|
| Vsl mortality | log-normal | 329.97 | 433.96 | 559.95 | 0.35 |
| Healthcare morbidity | normal | 7 | 8.7 | 11 | 0.15 |
| Criminal justice | normal | 7 | 8.8 | 11 | 0.15 |
| Lost productivity | log-normal | 35 | 45.7 | 59.99 | 0.2 |
| Community damage | log-normal | 8 | 11.3 | 16 | 0.1 |
| Governance | log-normal | 0.5 | 0.75 | 1.2 | 0.05 |
+Config JSON· pipeline/mc_configs/firearms.json
{
"paper_id": "firearms",
"seed": 42,
"draws": 100000,
"correlation": 0.3,
"annual_revenue": 23.2,
"channels": {
"C1_vsl_mortality": {
"dist": "lognormal",
"low": 329.97,
"mid": 433.96,
"high": 559.95,
"weight": 0.35
},
"C2_healthcare_morbidity": {
"dist": "normal",
"low": 7,
"mid": 8.7,
"high": 11,
"weight": 0.15
},
"C3_criminal_justice": {
"dist": "normal",
"low": 7,
"mid": 8.8,
"high": 11,
"weight": 0.15
},
"C4_lost_productivity": {
"dist": "lognormal",
"low": 35,
"mid": 45.7,
"high": 59.99,
"weight": 0.2
},
"C5_community_damage": {
"dist": "lognormal",
"low": 8,
"mid": 11.3,
"high": 16,
"weight": 0.1
},
"C6_governance": {
"dist": "lognormal",
"low": 0.5,
"mid": 0.75,
"high": 1.2,
"weight": 0.05
}
}
}+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 firearms · outputs output/firearms/mc_results.json