gsm-governance User Guide
A comprehensive guide to simulating justice-augmented multi-level governance. Everything you need to build, run, and interpret GSM-J models — from a single jurisdiction to a full hierarchy.
Getting started
What the library does, how to install it, and a first working simulation.
01 What this library does
gsm-governance simulates multi-level governance systems in
which justice is a capability — a state variable that
co-evolves with accountability, competence, cohesion, continuity, and
learning — rather than an external evaluative constraint.
You can use it to:
- Model governance hierarchies with arbitrary numbers of levels, jurisdictions, and links.
- Simulate justice dynamics across five dimensions: distributive, procedural, recognition, corrective, intergenerational.
- Explore normative trade-offs by varying weights, floors, and prioritarian penalties.
- Test intervention schedules and observe capability costs, delays, and frequency ceilings.
- Compare scenarios like universal floors vs. no floors, or single-channel vs. multi-channel intervention cost.
- Investigate structural questions about multi-level governance under bounded non-exploitation.
The library is conditional: it does not decide what is just. You supply the normative parameters; the model predicts systemic consequences.
02 Installation and verification
Requirements
- Python 3.9 or later
- NumPy 1.22 or later
- Optional:
matplotlib(visualization),pandas(data loaders)
Install from source
git clone https://github.com/hongxueharriswang/gsm-governance.git
cd gsm-governance
pip install -e ".[dev]"
Optional extras
pip install -e ".[viz]" # matplotlib for visualization
pip install -e ".[data]" # pandas for data loaders
pip install -e ".[dev]" # full development stack
Verify installation
import gsm_governance as gsm
print(gsm.__version__)
# "0.3.0"
Run the built-in demonstration
python -m gsm_governance
This runs a default simulation, prints a per-jurisdiction profile, and compares single-channel vs. multi-channel intervention costs.
03 Your first simulation
A minimal single-jurisdiction model
from gsm_governance import (
GSMConfig, MultiLevelGovernance, JusticeState, Simulation,
)
# 1. Configure
config = GSMConfig()
# 2. Build a governance system
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction(
"Denmark",
level=4,
capabilities={"A": 0.82, "C": 0.85, "S": 0.78, "T": 0.75, "L": 0.80},
justice=JusticeState({
"distributive": 0.78,
"procedural": 0.75,
"recognition": 0.60,
"corrective": 0.35,
"intergenerational": 0.55,
}),
)
# 3. Run
sim = Simulation(gov, config=config)
history = sim.run(steps=200)
# 4. Inspect
print(f"Final welfare W*: {history[-1]['W_star']:.4f}")
print(f"Mean justice: {history[-1]['mean_justice']:.4f}")
What just happened. At each time step:
- Base capabilities drifted toward targets determined by the justice profile.
- Justice dimensions evolved via internal coupling, exploitation/compensation, diffusion, and decay.
- Cross-level accountability was updated (dimension-specific).
- Justice suppressed exploitation.
- The policy intervention enforcer scheduled and applied corrective interventions where floors or vertical alignment were violated.
A multi-level hierarchy
from gsm_governance import (
GSMConfig, MultiLevelGovernance, JusticeState, Simulation,
JusticeInteractionMatrix, MJRegime,
)
config = GSMConfig()
gov = MultiLevelGovernance(config=config)
# National level
gov.add_jurisdiction("national", level=4,
capabilities={"A": 0.70, "C": 0.75, "S": 0.60,
"T": 0.65, "L": 0.70})
# Two regions
for rid, recog in [("region_east", 0.55), ("region_west", 0.35)]:
gov.add_jurisdiction(
rid, level=3,
justice=JusticeState({
"distributive": 0.60, "procedural": 0.55,
"recognition": recog, "corrective": 0.25,
"intergenerational": 0.45,
}),
)
gov.link_vertical("national", rid)
gov.link_horizontal("region_east", "region_west")
# Run with a symmetric interaction matrix
sim = Simulation(gov, config=config,
M_J=JusticeInteractionMatrix(regime=MJRegime.SYMMETRIC))
history = sim.run(steps=300)
04 GSM-J 0.3.0 as a superset of GSM 0.2.0
GSM-J 0.3.0 is a strict superset of GSM 0.2.0. Every v0.2.0 import path, class, and function is preserved. The 0.3.0 additions are entirely additive; no existing API was renamed, removed, or changed in behavior in a breaking way.
What "superset" means concretely
| Aspect | GSM 0.2.0 | GSM-J 0.3.0 |
|---|---|---|
| State vector | [A, C, S, T, L] |
[A, C, S, T, L, J] — justice added as a sixth capability |
| Constraints | C1 (bounded non-exploitation) | C1 + C2 + C3 + C4 |
| Welfare | Scalar welfare(gov) |
Scalar welfare(gov) plus welfare_dimension_specific(...) and welfare_components(...) |
| Enforcement | Ad-hoc projection | Formal PolicyInterventionEnforcer with cost, horizon, multi-channel effects, and recovery |
| Metrics | gsi, hfi, misalignment, resilience |
All retained, plus justice_index, justice_floor_violations, exploitation_index, justice_misalignment |
| Config | GSMConfig with structural + basic normative fields |
All v0.2.0 fields unchanged; new justice fields added with defaults |
Backward-compatibility evidence
Every v0.2.0 import continues to work:
# v0.2.0 imports — all still valid
from gsm_governance import (
MultiLevelGovernance,
GSMConfig,
BoundedNonExploitationConstraint,
CompensationVector,
Simulation,
gsi, hfi, misalignment, resilience, welfare,
)
# v0.2.0-style construction — still works
gov = MultiLevelGovernance.from_single_jurisdiction("X", level=4)
c = BoundedNonExploitationConstraint(tau=5.0)
sim = Simulation(gov, seed=42)
history = sim.run(steps=100)
What changed under the hood (without breaking anything)
-
MultiLevelGovernancenow internally carries aJusticeStateper jurisdiction. If you never initialize it, it defaults to0.5on every dimension. This is why v0.2.0 code runs unchanged. -
gsiandhfiwere methods in v0.2.0 and are now module-level functions. This was made backward-compatible via a shim:MultiLevelGovernance.gsi()and.hfi()still work. -
GSMConfiggained new fields (justice_weights,justice_floors,intervention_cost, etc.). All have defaults.
Why the superset design matters
- Reproducibility: any result produced with 0.2.0 can be reproduced with 0.3.0.
- Migration is optional: new users can start with justice; existing users can adopt the extensions incrementally.
- Scientific comparability: results across versions are directly comparable when justice parameters are held at defaults.
Core concepts
Jurisdictions, capabilities, justice dimensions, constraints, welfare, and interventions.
05 Jurisdictions and levels
A jurisdiction is any governance unit: a household, a neighborhood, a municipality, a national government, a regional bloc, or a global body. Each jurisdiction belongs to a level, an integer index.
The convention in GSM-J is:
| Level | Representative unit |
|---|---|
| 1 | Household / individual |
| 2 | Community |
| 3 | Subnational |
| 4 | National |
| 5 | Regional |
| 6 | Global |
Creating jurisdictions
from gsm_governance import MultiLevelGovernance, GSMConfig, JusticeState
gov = MultiLevelGovernance(config=GSMConfig())
# Minimal jurisdiction (defaults to all capabilities = 0.5)
gov.add_jurisdiction("municipality_A", level=3)
# With custom capabilities and justice profile
gov.add_jurisdiction(
"municipality_B",
level=3,
capabilities={"A": 0.65, "C": 0.70, "S": 0.55, "T": 0.50, "L": 0.60},
justice=JusticeState({
"distributive": 0.65,
"procedural": 0.55,
"recognition": 0.30,
"corrective": 0.20,
"intergenerational": 0.45,
}),
)
Linking jurisdictions
Vertical links establish parent-child relationships:
gov.link_vertical("national_government", "municipality_A")
Horizontal links establish neighbour relationships within a level:
gov.link_horizontal("municipality_A", "municipality_B")
Horizontal links matter because justice diffuses across neighbours
(parameter eta). Vertical links matter because:
- Higher levels are accountable for justice delivered at lower levels.
- Higher levels may not erode justice achieved at lower levels (constraint C4).
- Interventions that correct lower-level justice shortfalls may be charged to the parent as the designated authority.
Hierarchy accessors
gov["national"] # lookup by uid
"national" in gov # membership test
len(gov) # number of jurisdictions
gov.levels() # sorted list of levels
gov.by_level(3) # list of jurisdictions at level 3
gov.roots() # top-level jurisdictions (no parent)
gov.aggregate_justice() # mean aggregate justice across all jurisdictions
06 Capabilities and justice
Each jurisdiction has two state components:
- Base capabilities —
A,C,S,T,L - Justice — a
JusticeStatewith five dimensions
Reading and writing capabilities
j = gov.jurisdictions["municipality_A"]
# Read
print(j.state.A) # accountability
print(j.state.C) # competence
print(j.state.S) # cohesion
print(j.state.T) # continuity
print(j.state.L) # learning
# Write
j.state.A = 0.75
Reading and writing justice
# Read individual dimensions
print(j.state.justice.d["distributive"]) # 0.65
print(j.state.justice.d["procedural"]) # 0.55
print(j.state.justice.d["recognition"]) # 0.30
print(j.state.justice.d["corrective"]) # 0.20
print(j.state.justice.d["intergenerational"]) # 0.45
# Or use dict-style access
print(j.state.justice["distributive"]) # 0.65
j.state.justice["distributive"] = 0.80 # clips to [0, 1]
# Weighted aggregate
print(j.state.justice.aggregate(config.justice_weights))
# Floor checks
print(j.state.justice.satisfies_floors(config.justice_floors)) # bool
print(j.state.justice.floor_deficits(config.justice_floors)) # dict
Why justice is a state variable
In conventional governance models, justice is an external criterion: you compute some outcome and then ask whether it is just. In GSM-J, justice is a state variable: it evolves over time, is affected by other capabilities, and affects them in turn.
This matters for at least three reasons:
- Feedback loops. Justice deficits reduce other capabilities, which in turn slow justice improvement.
- Temporal dynamics. A jurisdiction's justice trajectory depends on its history, not just its current policies.
- Intervention realism. Correcting a justice deficit consumes capabilities, so justice improvement is not free.
JusticeState API
from gsm_governance import JusticeState
j = JusticeState({
"distributive": 0.65,
"procedural": 0.55,
"recognition": 0.30,
"corrective": 0.20,
"intergenerational": 0.45,
})
j.d # dict of dimension -> value
j["distributive"] # 0.65
j["distributive"] = 0.9 # set (clipped to [0, 1])
j.aggregate(weights) # float
j.satisfies_floors(floors) # bool
j.floor_deficits(floors) # dict of deficits
j.copy() # deep copy
j.as_dict() # dict copy
07 The justice interaction matrix
Justice dimensions interact. Some reinforce each other (procedural
justice enables recognition justice); others trade off (intergenerational
investment displaces present distribution). The interaction matrix
M_J captures both.
Default matrix
from gsm_governance import JusticeInteractionMatrix, MJRegime
M = JusticeInteractionMatrix(regime=MJRegime.SYMMETRIC)
for (src, tgt), coef in sorted(M.m.items()):
print(f"{src:20s} -> {tgt:20s} : {coef:+.3f}")
Default synergies
| Source | Target | Coefficient |
|---|---|---|
| procedural | recognition | +0.15 |
| recognition | procedural | +0.15 |
| distributive | procedural | +0.10 |
| procedural | distributive | +0.10 |
| distributive | corrective | +0.15 |
| corrective | recognition | +0.20 |
| recognition | corrective | +0.15 |
| intergenerational | procedural | +0.05 |
Default tensions
| Source | Target | Coefficient |
|---|---|---|
| intergenerational | distributive | −0.05 |
| distributive | intergenerational | −0.05 |
| corrective | distributive | −0.10 |
| procedural | intergenerational | −0.05 |
Why both signs matter
Under a purely positive matrix, justice dimensions reinforce each other monotonically: investing in one dimension lifts others. This produces unrealistically optimistic projections.
Under a signed matrix, trade-offs appear:
- Recognition improvement may temporarily reduce distributive flexibility.
- Intergenerational investment may slow present distribution.
- Large corrective transfers may draw from current distributive resources.
Setting a custom matrix
M = JusticeInteractionMatrix()
M.m = {
("procedural", "recognition"): +0.20,
("recognition", "procedural"): +0.20,
("intergenerational", "distributive"): -0.15,
("distributive", "intergenerational"): -0.15,
# ... etc.
}
M.symmetrize() # enforce symmetry prior
Symmetric vs. asymmetric regimes
- Symmetric (
m_de = m_ed): 10 free parameters, appropriate for small samples. - Asymmetric (
m_de ≠ m_ed): 20 free parameters, appropriate when directional hypotheses are of interest.
M_sym = JusticeInteractionMatrix(regime=MJRegime.SYMMETRIC)
M_asym = JusticeInteractionMatrix(regime=MJRegime.ASYMMETRIC)
08 Constraints
Four constraints govern the justice subsystem. Each is checked at every step and enforced via policy intervention when applicable.
C1 — Bounded non-exploitation
from gsm_governance import (
ConstraintChecker, ExploitationMatrix, CompensationMechanism,
BoundedNonExploitationConstraint,
)
checker = ConstraintChecker(config)
em = ExploitationMatrix()
em.set("A", "B", {"distributive": 10.0})
cm = CompensationMechanism()
cm.set("A", "B", {"distributive": 0.0})
violations = checker.C1(em, cm)
print(f"C1 violations: {violations}") # [('A', 'B')]
C2 — Justice floor
No jurisdiction may fall below the dimension-specific floor.
violations = checker.C2(gov.jurisdictions)
for uid, dim in violations:
print(f"{uid} is below the {dim} floor")
C3 — Vulnerability-sensitive non-exploitation
violations = checker.C3(em, gov.jurisdictions)
C4 — Vertical justice alignment (subsidiarity)
violations = checker.C4(gov.jurisdictions)
To use the aggregate formulation instead:
from gsm_governance import C4Mode, GSMConfig
config = GSMConfig(c4_mode=C4Mode.AGGREGATE)
checker = ConstraintChecker(config)
Checking all constraints at once
violations = checker.all(em, cm, gov.jurisdictions)
# {'C1': [...], 'C2': [...], 'C3': [...], 'C4': [...]}
09 Welfare
The welfare function is dimension-specific and prioritarian:
Reading welfare
from gsm_governance import (
welfare, welfare_dimension_specific, welfare_components,
)
# Scalar welfare (backward-compatible)
w = welfare(gov, base_welfare=1.0)
# Dimension-specific form
w_star = welfare_dimension_specific(1.0, gov, config)
# Component breakdown
comps = welfare_components(1.0, gov, config)
# {'base': 1.0,
# 'justice_contribution': 2.8,
# 'prioritarian_penalty': -0.5,
# 'W_star': 3.3}
Understanding the prioritarian commitment
The default parameters set \(\delta_d = 2.0 \, w_d\) and \(\gamma_d = 0.75 \, w_d\). The ratio \(\delta_d / \gamma_d = 2.67\) encodes the view that fixing deficits matters more than further improving the just. This is a normative choice, not a derivation.
Customizing the weights
config.gamma_dim = {d: 0.5 for d in gsm.JUSTICE_DIMENSIONS}
config.delta_dim = {d: 1.5 for d in gsm.JUSTICE_DIMENSIONS}
config.validate()
10 Interventions
When a constraint is violated, the enforcer schedules a policy intervention: a designated authority raises the affected state over a finite horizon, at a cost distributed across capabilities.
Aggregate intervention statistics
print(sim.enforcer.stats)
# {'scheduled': 42, 'completed': 40, 'total_cost': 3.2814}
print(f"Active interventions: {sim.enforcer.active_count}")
Multi-channel costs
| Channel | Default weight | Rationale |
|---|---|---|
C | 0.50 | Administrative capacity is the primary constraint |
S | 0.25 | Interventions frequently produce short-term polarization |
T | 0.15 | Overturning prior commitments weakens continuity |
A | 0.10 | Override of local decision-making has modest accountability cost |
To reproduce v0.2.0-style single-channel cost
from gsm_governance import GSMConfig
config = GSMConfig(
intervention_weights={"C": 1.0, "S": 0.0, "T": 0.0, "A": 0.0}
)
The frequency ceiling
Because interventions consume capability, aggressive justice improvement is self-limiting.
Building models
From empty hierarchy to configured simulation.
11 Building a hierarchy from scratch
Step 1 — Decide on levels and units
Suppose you are modelling a federal system:
- Level 4: national government
- Level 3: state governments
- Level 2: municipal governments
- Level 1: households (not modelled individually)
Step 2 — Instantiate configuration
from gsm_governance import (
GSMConfig, MultiLevelGovernance, JusticeInteractionMatrix, MJRegime,
)
config = GSMConfig()
gov = MultiLevelGovernance(config=config)
M = JusticeInteractionMatrix(regime=MJRegime.SYMMETRIC)
Step 3 — Add jurisdictions
from gsm_governance import JusticeState
# National level
gov.add_jurisdiction(
"national", level=4,
capabilities={"A": 0.70, "C": 0.75, "S": 0.60, "T": 0.65, "L": 0.70},
justice=JusticeState({
"distributive": 0.55, "procedural": 0.60,
"recognition": 0.35, "corrective": 0.25,
"intergenerational": 0.45,
}),
)
# States
states = {
"state_east": {"A": 0.75, "C": 0.80, "S": 0.70, "T": 0.65, "L": 0.70},
"state_west": {"A": 0.60, "C": 0.65, "S": 0.55, "T": 0.60, "L": 0.55},
"state_north": {"A": 0.70, "C": 0.60, "S": 0.65, "T": 0.55, "L": 0.60},
}
for uid, caps in states.items():
gov.add_jurisdiction(uid, level=3, capabilities=caps)
# Municipalities
municipalities = {
"city_alpha": {"distributive": 0.70, "procedural": 0.65,
"recognition": 0.50, "corrective": 0.30,
"intergenerational": 0.55},
"city_beta": {"distributive": 0.45, "procedural": 0.40,
"recognition": 0.25, "corrective": 0.15,
"intergenerational": 0.30},
"city_gamma": {"distributive": 0.60, "procedural": 0.55,
"recognition": 0.35, "corrective": 0.20,
"intergenerational": 0.45},
"city_delta": {"distributive": 0.55, "procedural": 0.50,
"recognition": 0.20, "corrective": 0.10,
"intergenerational": 0.35},
}
for uid, j_vals in municipalities.items():
gov.add_jurisdiction(uid, level=2, justice=JusticeState(j_vals))
Step 4 — Link the hierarchy
# Vertical
gov.link_vertical("national", "state_east")
gov.link_vertical("national", "state_west")
gov.link_vertical("national", "state_north")
gov.link_vertical("state_east", "city_alpha")
gov.link_vertical("state_east", "city_beta")
gov.link_vertical("state_west", "city_gamma")
gov.link_vertical("state_north", "city_delta")
# Horizontal
gov.link_horizontal("state_east", "state_west")
gov.link_horizontal("state_west", "state_north")
gov.link_horizontal("city_alpha", "city_beta")
Step 5 — Run and inspect
from gsm_governance import Simulation
sim = Simulation(gov, config=config, M_J=M)
history = sim.run(steps=500)
print(f"Final W*: {history[-1]['W_star']:.4f}")
print(f"Mean justice: {history[-1]['mean_justice']:.4f}")
12 Injecting exploitation and compensation
Setting up exploitation
sim.set_exploitation("state_east", "city_beta", {
"distributive": 0.25,
"procedural": 0.15,
"recognition": 0.20,
"corrective": 0.00,
"intergenerational": 0.10,
})
Setting up compensation
sim.set_compensation("state_east", "city_beta", {
"distributive": 0.15,
"procedural": 0.05,
"recognition": 0.00,
"corrective": 0.00,
"intergenerational": 0.05,
})
Using CompensationVector (six-component form)
from gsm_governance import CompensationVector
cv = CompensationVector(
fiscal=0.10,
infrastructural=0.05,
representational=0.02,
regulatory=0.03,
future_oriented=0.02,
restitutional=0.00,
)
print(cv.total()) # 0.22
print(cv.to_justice_dimensions())
# {'distributive': 0.08,
# 'procedural': 0.025,
# 'recognition': 0.025,
# 'corrective': 0.02,
# 'intergenerational': 0.035}
Why dimension-level decomposition matters
Non-exploitation in one dimension does not imply non-exploitation in another. A jurisdiction may pay fair prices (distributive = 0) while dominating decision-making (procedural > 0) and depleting commons (intergenerational > 0).
13 Configuring normative parameters
Justice weights
config = GSMConfig()
config.justice_weights = {
"distributive": 0.30,
"procedural": 0.20,
"recognition": 0.25,
"corrective": 0.10,
"intergenerational": 0.15,
}
config.validate()
Justice floors
config.justice_floors = {
"distributive": 0.35, # raised from default 0.30
"procedural": 0.30,
"recognition": 0.30, # raised from default 0.25
"corrective": 0.15,
"intergenerational": 0.25,
}
Prioritarian coefficients
# Default: gamma_d = 0.75 * w_d, delta_d = 2.0 * w_d
# Override for a stronger prioritarian stance
config.delta_dim = {d: 3.0 * config.justice_weights[d]
for d in config.justice_weights}
Accountability penalties
config.lambda_p_dim = {
"distributive": 0.25,
"procedural": 0.25,
"recognition": 0.50,
"corrective": 0.15,
"intergenerational": 0.40,
}
Subsidiarity
config.phi = 0.90 # tighter vertical alignment (default 0.85)
config.phi = 1.00 # perfect non-regression
config.phi = 0.00 # C4 disabled
C4 mode
from gsm_governance import C4Mode
config.c4_mode = C4Mode.PER_DIMENSION # default
config.c4_mode = C4Mode.AGGREGATE
config.c4_mode = C4Mode.HYBRID
14 Configuring structural parameters
Justice dynamics
# Diffusion rate across neighbours (default 0.20)
config.eta = 0.30
# Decay per dimension (default 0.05)
config.kappa_d = {d: 0.03 for d in config.justice_weights}
# Internal coupling (support from base capabilities)
config.alpha_d = {
"distributive": 0.35,
"procedural": 0.35,
"recognition": 0.30,
"corrective": 0.25,
"intergenerational": 0.40,
}
# Corrective restorative coupling
config.alpha_corrective_restorative = 0.40
Exploitation dynamics
config.beta_erosion = {d: 0.50 for d in config.justice_weights}
config.beta_compensation = {d: 0.60 for d in config.justice_weights}
config.mu = 0.30 # justice suppression of exploitation (default 0.25)
Accountability coupling
config.lam_c = 0.20 # parent-child accountability convergence (default 0.15)
Intervention costs
config.intervention_cost = 0.15
config.intervention_horizon = 10
config.intervention_weights = {"C": 0.40, "S": 0.30, "T": 0.20, "A": 0.10}
config.recovery = {"C": 0.08, "S": 0.06, "T": 0.06, "A": 0.05}
Validation
config.validate() # ensures intervention weights sum to 1
Running experiments
Inspect, compare, sweep, and visualize.
15 Running and inspecting simulations
Basic run
from gsm_governance import (
GSMConfig, MultiLevelGovernance, JusticeState, Simulation,
)
config = GSMConfig()
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("X", level=4)
sim = Simulation(gov, config=config)
history = sim.run(steps=500)
history is a list of dicts, one per step:
last = history[-1]
print(last)
# {
# 't': 25.0,
# 'W_star': 3.4821,
# 'mean_justice': 0.5104,
# 'dim_means': {'distributive': 0.62, ...},
# 'C1_violations': 0,
# 'C2_violations': 0,
# 'C3_violations': 0,
# 'C4_violations': 0,
# 'active_interventions': 0,
# 'total_cost': 2.7413,
# }
Plotting
import matplotlib.pyplot as plt
import pandas as pd
df = pd.DataFrame(history)
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
axes[0].plot(df["t"], df["W_star"])
axes[0].set_ylabel("W*")
axes[0].set_title("Welfare trajectory")
axes[1].plot(df["t"], df["mean_justice"])
axes[1].set_ylabel("Mean justice")
axes[1].set_xlabel("Time")
plt.tight_layout()
plt.savefig("welfare_trajectory.png", dpi=150)
16 Scenario comparisons
from gsm_governance import (
GSMConfig, MultiLevelGovernance, JusticeState, Simulation,
)
def build_gov(config):
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("G", level=4)
gov.add_jurisdiction("R", level=3)
gov.add_jurisdiction(
"C", level=2,
justice=JusticeState({d: 0.20 for d in [
"distributive", "procedural", "recognition",
"corrective", "intergenerational",
]}),
)
gov.link_vertical("G", "R")
gov.link_vertical("R", "C")
return gov
scenarios = {}
# Default
cfg = GSMConfig()
scenarios["default"] = Simulation(build_gov(cfg), config=cfg)
# No floor
cfg = GSMConfig()
cfg.justice_floors = {d: 0.0 for d in cfg.justice_weights}
scenarios["no_floor"] = Simulation(build_gov(cfg), config=cfg)
# No subsidiarity
cfg = GSMConfig(phi=0.0)
scenarios["no_subsidiarity"] = Simulation(build_gov(cfg), config=cfg)
# Single-channel intervention cost
cfg = GSMConfig(intervention_weights={"C": 1.0, "S": 0.0, "T": 0.0, "A": 0.0})
scenarios["single_channel"] = Simulation(build_gov(cfg), config=cfg)
# Run all
results = {}
for name, sim in scenarios.items():
results[name] = sim.run(steps=500)
no_floorvs.default: isolates the effect of constraint C2no_subsidiarityvs.default: isolates vertical alignmentsingle_channelvs.default: tests the frequency ceiling
17 Sensitivity analysis
Sweeping a single parameter
import numpy as np
import matplotlib.pyplot as plt
phis = np.linspace(0.0, 1.0, 11)
final_welfare = []
for phi in phis:
config = GSMConfig(phi=phi)
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("G", level=4)
gov.add_jurisdiction("C", level=2)
gov.link_vertical("G", "C")
sim = Simulation(gov, config=config)
hist = sim.run(steps=300)
final_welfare.append(hist[-1]["W_star"])
plt.plot(phis, final_welfare, marker="o")
plt.xlabel("phi (subsidiarity tolerance)")
plt.ylabel("Final W*")
plt.savefig("sensitivity_phi.png", dpi=150)
Weight sweep over the justice simplex
import numpy as np
def simplex_sample(n_samples=20):
samples = []
for _ in range(n_samples):
cuts = sorted(np.random.uniform(0, 1, 4))
w = np.diff([0] + cuts + [1])
samples.append(w)
return samples
for w in simplex_sample(20):
config = GSMConfig()
config.justice_weights = dict(zip(config.justice_weights.keys(), w))
try:
config.validate()
except ValueError:
continue
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("X", level=4)
sim = Simulation(gov, config=config)
hist = sim.run(steps=200)
print(f"Weights {w.round(2)} -> W* = {hist[-1]['W_star']:.4f}")
18 Visualization
The library provides optional visualization helpers.
from gsm_governance.utils.visualization import (
plot_welfare,
plot_dimension_trajectories,
plot_interventions,
plot_justice_radar,
)
plot_welfare(history, save_path="welfare.png")
plot_dimension_trajectories(history, save_path="dimensions.png")
plot_interventions(history, save_path="interventions.png")
plot_justice_radar(gov, uids=["national", "region", "community"],
save_path="radar.png")
If matplotlib is not installed, these functions raise an
informative ImportError suggesting:
pip install gsm-governance[viz]
Advanced usage
Custom dynamics, custom enforcement, calibration, and dimension extension.
19 Custom dynamics
You can subclass Simulation to substitute alternative dynamics.
Add an exogenous shock at time t=10
from gsm_governance import Simulation
class ShockSimulation(Simulation):
def __init__(self, *args, shock_time=10.0, shock_magnitude=0.3, **kwargs):
super().__init__(*args, **kwargs)
self.shock_time = shock_time
self.shock_magnitude = shock_magnitude
self._applied = False
def step(self):
super().step()
if not self._applied and self.t >= self.shock_time:
for j in self.governance.jurisdictions.values():
cur = j.state.justice.d["distributive"]
j.state.justice.d["distributive"] = max(
0.0, cur - self.shock_magnitude)
self._applied = True
Capture per-step diagnostics
import numpy as np
class DiagnosticSimulation(Simulation):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.diagnostics = []
def step(self):
super().step()
self.diagnostics.append({
"t": self.t,
"active": self.enforcer.active_count,
"mean_A": float(np.mean([
j.state.A for j in self.governance.jurisdictions.values()
])),
})
20 Custom enforcement
Quadratic cost
from gsm_governance import (
PolicyInterventionEnforcer, INTERVENTION_CHANNELS,
)
class QuadraticCostEnforcer(PolicyInterventionEnforcer):
def _charge_capability_cost(self, auth, cost_per_step):
scaled = cost_per_step ** 1.5
for k in INTERVENTION_CHANNELS:
w = self.config.intervention_weights.get(k, 0.0)
cur = getattr(auth.state, k)
new = max(0.0, cur - w * scaled)
setattr(auth.state, k, new)
Bounded intervention rate
class RateLimitedEnforcer(PolicyInterventionEnforcer):
def __init__(self, *args, max_per_step=3, **kwargs):
super().__init__(*args, **kwargs)
self.max_per_step = max_per_step
def check_and_schedule(self, governance, exploitation):
super().check_and_schedule(governance, exploitation)
if len(self._active) > self.max_per_step:
dropped = self._active[self.max_per_step:]
self._active = self._active[:self.max_per_step]
for iv in dropped:
self._stats["scheduled"] -= 1
21 Empirical calibration of M_J
The library ships with theoretical priors. Fitting M_J to
data follows the protocol in Appendix D of the manuscript.
Minimal interface
from gsm_governance import JusticeInteractionMatrix, MJRegime
# Example: fitted coefficients from external estimation
fitted = {
("procedural", "recognition"): +0.18,
("recognition", "procedural"): +0.16,
("distributive", "corrective"): +0.13,
("corrective", "recognition"): +0.22,
("intergenerational", "distributive"): -0.07,
# ...
}
M = JusticeInteractionMatrix(regime=MJRegime.ASYMMETRIC)
M.m = fitted
M.m_max = 0.30
Data loader
from gsm_governance.data import load_justice_indicators
# Returns {} unless a path to a local CSV is supplied
indicators = load_justice_indicators("path/to/indicators.csv")
Reporting
When using fitted matrices, report:
- The number of observations and jurisdictions
- The sign priors used
- The estimation regime (symmetric or asymmetric)
- Posterior means and 90% credible intervals
- Sensitivity to
m_max
22 Extending the justice dimensions
The model is designed around five dimensions, but you may wish to add others (e.g., ecological, territorial, epistemic).
Step 1 — Extend the dimension constants
import gsm_governance as gsm
# Extend the module-level tuple
gsm.JUSTICE_DIMENSIONS = (
"distributive", "procedural", "recognition",
"corrective", "intergenerational", "ecological",
)
Step 2 — Update configuration
config = gsm.GSMConfig()
config.justice_weights["ecological"] = 0.10
# Renormalize
total = sum(config.justice_weights.values())
config.justice_weights = {k: v / total
for k, v in config.justice_weights.items()}
config.justice_floors["ecological"] = 0.20
config.gamma_dim["ecological"] = 0.75 * config.justice_weights["ecological"]
config.delta_dim["ecological"] = 2.0 * config.justice_weights["ecological"]
config.lambda_p_dim["ecological"] = 0.35
config.alpha_d["ecological"] = 0.30
config.kappa_d["ecological"] = 0.05
config.beta_erosion["ecological"] = 0.40
config.beta_compensation["ecological"] = 0.50
Step 3 — Update the interaction matrix
M = gsm.JusticeInteractionMatrix()
M.m[("ecological", "intergenerational")] = +0.20
M.m[("intergenerational", "ecological")] = +0.20
M.m[("ecological", "distributive")] = -0.10
Reference
Recipes, troubleshooting, and glossary.
23 Common patterns and recipes
Recipe 1 — Measure the effect of a single constraint
def run_with(config):
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("G", level=4)
gov.add_jurisdiction("C", level=2)
gov.link_vertical("G", "C")
sim = Simulation(gov, config=config)
return sim.run(steps=500)
# With floor
config_with = GSMConfig()
hist_with = run_with(config_with)
# Without floor
config_without = GSMConfig()
config_without.justice_floors = {d: 0.0 for d in config_without.justice_weights}
hist_without = run_with(config_without)
print(f"With floor: W* = {hist_with[-1]['W_star']:.4f}")
print(f"Without floor: W* = {hist_without[-1]['W_star']:.4f}")
Recipe 2 — Identify jurisdictions below floor
for uid, j in gov.jurisdictions.items():
deficits = j.state.justice.floor_deficits(config.justice_floors)
for d, def_val in deficits.items():
if def_val > 0:
print(f"{uid} below {d} floor by {def_val:.3f}")
Recipe 3 — Export a jurisdiction's state to JSON
import json
def jurisdiction_to_dict(j):
return {
"uid": j.uid,
"level": j.level,
"capabilities": j.state.capabilities(),
"justice": j.state.justice.as_dict(),
"parent": j.parent,
"children": j.children,
"neighbours": j.neighbours,
}
with open("jurisdictions.json", "w") as f:
json.dump(
{uid: jurisdiction_to_dict(j)
for uid, j in gov.jurisdictions.items()},
f, indent=2,
)
Recipe 4 — Batch run over many parameter configurations
from itertools import product
import pandas as pd
configs = list(product(
[0.60, 0.85, 1.00], # phi
[0.20, 0.30, 0.40], # lambda_p base
["symmetric", "asymmetric"],
))
rows = []
for phi, lpb, regime in configs:
config = GSMConfig(phi=phi)
config.lambda_p_dim = {d: lpb for d in config.justice_weights}
M = JusticeInteractionMatrix(regime=MJRegime(regime))
gov = MultiLevelGovernance(config=config)
gov.add_jurisdiction("G", level=4)
gov.add_jurisdiction("C", level=2)
gov.link_vertical("G", "C")
sim = Simulation(gov, config=config, M_J=M)
hist = sim.run(steps=300)
rows.append({
"phi": phi,
"lambda_p": lpb,
"regime": regime,
"W_star": hist[-1]["W_star"],
"C2_violations": hist[-1]["C2_violations"],
})
df = pd.DataFrame(rows)
df.to_csv("batch_results.csv", index=False)
print(df)
24 Troubleshooting
"Invalid: justice weights must sum to 1"
total = sum(config.justice_weights.values())
config.justice_weights = {k: v / total
for k, v in config.justice_weights.items()}
config.validate()
"Invalid: prioritarian penalty must exceed welfare weight"
Set delta_dim[d] > gamma_dim[d], e.g.,
delta_dim[d] = 2 * gamma_dim[d].
Simulation produces NaN or Inf
Usually a sign of unstable parameters. Check:
config.dtis small enough (try 0.01)- Coupling coefficients are within
[-m_max, m_max] - Diffusion rate
etais below 1
Violations persist despite active interventions
Check steps_remaining on active interventions. If the
horizon exceeds the simulation length, violations persist by design.
"KeyError: unknown justice dimension"
Verify the spelling — dimensions are lowercase and use full words:
"distributive" # correct
"Distributive" # wrong
"dist" # wrong
25 Glossary
| Term | Definition |
|---|---|
| Bounded non-exploitation | Normative constraint: no jurisdiction may impose uncompensated non-trivial sacrifices on another |
| Capability | One of six governance subsystems: accountability, competence, cohesion, continuity, learning, justice |
| C1, C2, C3, C4 | The four justice constraints (bounded non-exploitation, floor, vulnerability-sensitive, vertical alignment) |
| Dimension | One of five justice components: distributive, procedural, recognition, corrective, intergenerational |
| Enforcement mode | How constraints are enforced: projection, penalty, or hybrid |
| Exploitation index | E_ij, the amount jurisdiction i extracts from j |
| Floor | Minimum acceptable value of a justice dimension |
| Frequency ceiling | Endogenous limit on intervention rate from multi-channel cost |
| Interaction matrix | M_J, signed matrix of synergies and tensions between justice dimensions |
| Intervention | A policy action that raises a state over a finite horizon at a capability cost |
| Level | An integer index in the governance hierarchy |
| Prioritarian | Weighting improvements to the worst off more heavily |
| Subsidiarity | Higher levels may not erode justice achieved at lower levels |
| Superset | GSM-J 0.3.0 preserves all v0.2.0 APIs while adding new functionality |
| Vertical alignment | Constraint C4, enforcing subsidiarity |
For the mathematical specification, see the manuscript. For the API
reference, see the docstrings in each module. For worked examples, see
the examples/ directory in the repository.