Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: ci

on:
push:
branches: [main, "release/**"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: python -m pip install --upgrade pip && python -m pip install -r requirements-dev.txt
- name: Unit tests
run: python -m pytest
- name: Ablation demo runs
run: python amt_ablation_demo.py > /dev/null
- name: Review verification scripts run
run: |
python docs/reviews/2026-08-22-overmier-v1.0.0/verify_01_replay_regen_iot.py > /dev/null
python docs/reviews/2026-08-22-overmier-v1.0.0/verify_02_replay_with_ballast.py > /dev/null
python docs/reviews/2026-08-22-overmier-v1.0.0/verify_03_regen_topology.py > /dev/null
python docs/reviews/2026-08-22-overmier-v1.0.0/iot_gate_check.py > /dev/null

oldest-supported-cryptography:
# The v1.0.0 review ran on cryptography 3.4.8; keep that floor honest.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install pinned floor
run: python -m pip install --upgrade pip && python -m pip install "cryptography==3.4.8" "pytest>=7.0"
- name: Unit tests
run: python -m pytest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ __pycache__/
dist/
build/
.DS_Store
.pytest_cache/
uv.lock
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ authors:
affiliation: Ravenhelm
license: MIT
repository-code: "https://github.com/nwalker85/agentropy"
version: 1.0.0
date-released: 2026-06-23
version: 1.0.1
date-released: 2026-06-23 # v1.0.0 deposit date; update at the v1.0.1 tag/deposit
keywords:
- conservation law
- artificial life
Expand Down
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,33 @@ C_{n+1} + S_{n+1} + L_n = C_n

An agent's cipher mass after an interaction (`C_{n+1}`), plus the signal extracted (`S_{n+1}`), plus the loss incurred (`L_n`), equals its mass before (`C_n`). Nothing is created; nothing disappears. The law is not a protocol — it is an algebraic identity of AES-256-GCM decryption.

Applied **unmodified** across four unrelated domains — cross-organizational accountability, token economics, physical-IoT resource management, and population ecology — it produces life-like dynamics (trustless auditability, market stratification, load-shedding, carrying capacity, speciation) that no participant programmed. Across **750 agents and 5,415 interactions**, plus a **50,000-agent / multi-million-interaction scale test**, there are **zero conservation violations**. A systematic ablation study establishes that structured, irreversible depletion is a *necessary condition* for these dynamics.
Applied **unmodified** across four unrelated domains — cross-organizational accountability, token economics, physical-IoT resource management, and population ecology — it produces dynamics that no participant programmed: independent per-organization auditability, market stratification, unified resource accounting, niche-differential survival and nutrient cycling. Across **750 agents and 5,415 interactions**, plus a **50,000-agent / ~2.3-million-visit scale test**, there are **zero conservation violations** — the equation is an accounting identity and cannot fail in a correct implementation. A systematic ablation study is consistent with structured, irreversible depletion being a *necessary condition* for these dynamics; with its controls repaired (v1.0.1) it establishes that depletion is necessary for every dynamic measured and that class-selective structure is necessary for per-class attribution.

## Paper

- **[`paper/agentropy.md`](paper/agentropy.md)** — *Agentropy: A Conservation Law as a Necessary Condition for Life-Like Dynamics.*
- Domain papers: [cross-org accountability](paper/cross_org_accountability.md) · [token economy](paper/token_economy.md) · [physical IoT](paper/physical_iot.md) · [population ecology](paper/marketplace.md).
- **[`paper/ERRATA-v1.0.1.md`](paper/ERRATA-v1.0.1.md)** — what v1.0.1 corrects, and why. The deposited v1.0.0 text is preserved at [DOI 10.5281/zenodo.20818597](https://doi.org/10.5281/zenodo.20818597).

## v1.0.1 (patch)

v1.0.1 responds to Kurt Overmier's open technical review of v1.0.0 (received 2026-08-22; verification record in [`docs/reviews/2026-08-22-overmier-v1.0.0/`](docs/reviews/2026-08-22-overmier-v1.0.0/)). It is a patch release: code fixes for confirmed bugs and wording corrections where the papers claimed more than the code does. No new architecture — uniqueness, settlement, signing and external anchoring are v2 work. The one-line version of the correction: **the equation is an accounting identity, and an accounting identity does not by itself give you scarcity, authorization, or non-repudiation.**

## Reproduce

```bash
pip install cryptography
pip install -r requirements-dev.txt # cryptography (>=3.4.8) + pytest
python -m pytest # 31 tests, incl. known-limitations tests that pass while the limitation is present

python3 amt_cross_org_demo.py # Domain 1: cross-org accountability
python3 amt_token_economy_demo.py # Domain 2: token economy
python3 amt_physical_iot_demo.py # Domain 3: physical IoT
python3 amt_marketplace_demo.py # Domain 4: population ecology
python3 amt_marketplace_demo.py # Domain 4: population ecology (AMT_SEED=n to sample other runs)
python3 amt_ablation_demo.py # Ablation study (paper §9)
python3 amt_scale.py --agents 50000 --steps 50 # population-scale verification
```

Every interaction asserts the conservation law (`mass_before == mass_after + signal + loss`). A violation crashes the program. None has ever fired.
Every interaction asserts the conservation identity (`mass_before == mass_after + signal + loss`). A violation crashes the program. None has ever fired — because `loss` is defined as the remainder (paper Appendix B), which is also why zero violations is a statement about the bookkeeping, not about nature.

## Repository map

Expand All @@ -41,9 +47,11 @@ Every interaction asserts the conservation law (`mass_before == mass_after + sig
| `amt_core.py` | The conservation law: `Layer`, `Agent`, `Environment`, `interact()`, `AgentFactory` |
| `amt_extensions.py` | Ledgers (local/public), Merkle commitments, nodes, topology, behavior |
| `amt_cross_org.py` · `amt_token_economy.py` · `amt_physical_iot.py` · `amt_marketplace.py` | The four domain models (+ `*_demo.py` runners) |
| `amt_ablation.py` | CONTROL / IMMORTAL / RANDOM ablation (paper §9) |
| `amt_ablation.py` | CONTROL / IMMORTAL / RANDOM / RANDOM-MATCHED ablation (paper §9) |
| `amt_scale.py` | Population-scale verification |
| `paper/` | The paper and the four domain papers |
| `tests/` | pytest suite: conservation identity, price enforcement, determinism, marketplace accounting, ablation controls, known limitations |
| `paper/` | The paper, the four domain papers, and `ERRATA-v1.0.1.md` |
| `docs/reviews/` | Verification record for the Overmier review of v1.0.0 |

## Citation

Expand Down
197 changes: 124 additions & 73 deletions amt_ablation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
Agent, Layer, Environment, AgentFactory,
InteractionResult, DecryptionResult,
interact as interact_control,
derive_key, decrypt_layer,
derive_key, decrypt_layer, LAYER_OVERHEAD,
)


Expand Down Expand Up @@ -76,16 +76,63 @@ def interact_immortal(agent: Agent, env: Environment) -> InteractionResult:
)


def interact_random(agent: Agent, env: Environment) -> InteractionResult:
def _strip_random_layers(agent: Agent, n_to_remove: int, mass_before: int) -> InteractionResult:
"""
Shared tail of the two RANDOM conditions: remove `n_to_remove` layers chosen
uniformly at random, ignoring key class, and account for them honestly.

Signal and loss are derived from the real layer geometry, exactly as the
control's interact() would measure them after decryption:
mass = LAYER_OVERHEAD + len(plaintext)
signal = len(plaintext) = mass - LAYER_OVERHEAD (0 for an empty layer)
loss = mass - signal
v1.0.0 invented a 50/50 signal/loss split here (Overmier review, ablation §).
Per-class attribution (delta_L) stays empty by design: that is the structure
the condition removes.
"""
Ablation B: Random layers consumed regardless of key class.
n_to_remove = max(0, min(n_to_remove, len(agent.layers)))

indices = list(range(len(agent.layers)))
random.shuffle(indices)
remove_indices = set(indices[:n_to_remove])

The environment removes a random subset of layers, ignoring
key class affinity entirely. Mass is consumed (depletion exists)
but the structure of consumption is destroyed.
removed = [agent.layers[i] for i in remove_indices]
agent.layers = [l for i, l in enumerate(agent.layers) if i not in remove_indices]

mass_after = agent.mass
consumed = mass_before - mass_after
signal = sum(max(0, layer.mass - LAYER_OVERHEAD) for layer in removed)
loss = consumed - signal

Key class selectivity is eliminated — an alpha environment
strips beta and gamma layers with equal probability.
return InteractionResult(
agent_survived=agent.alive,
agent_could_enter=True,
mass_before=mass_before,
mass_after=mass_after,
total_signal=signal,
total_loss=loss,
total_consumed=consumed,
layers_stripped=n_to_remove,
per_layer=[],
delta_L={}, # No per-class tracking — structure destroyed
)


def interact_random(agent: Agent, env: Environment) -> InteractionResult:
"""
Ablation B (v1.0.0 condition, kept for comparability): random layers
consumed regardless of key class, ~1 layer per environment key class.

The environment removes a random subset of layers, ignoring key class
affinity entirely. Mass is consumed (depletion exists) but the structure
of consumption is destroyed.

KNOWN CONFOUND (Overmier review): the CONTROL condition removes *every*
affinity layer per visit, while this condition removes only
`env.hazard_classes` layers, so depletion rate is not held constant. It
is retained so the v1.0.0 tables remain reproducible; the rate-matched
variant is `interact_random_matched`. The 50/50 signal/loss split of
v1.0.0 is replaced by the real layer geometry in both variants.
"""
mass_before = agent.mass

Expand All @@ -108,38 +155,50 @@ def interact_random(agent: Agent, env: Environment) -> InteractionResult:
layers_stripped=0, per_layer=[], delta_L={},
)

# Remove random layers (ignoring key class)
# Match control's stripping rate: ~1 layer per env key class
# Remove random layers (ignoring key class), ~1 layer per env key class
n_to_remove = max(1, min(env.hazard_classes, len(agent.layers)))
n_to_remove = min(n_to_remove, len(agent.layers))
return _strip_random_layers(agent, n_to_remove, mass_before)

indices = list(range(len(agent.layers)))
random.shuffle(indices)
remove_indices = set(indices[:n_to_remove])

removed = [agent.layers[i] for i in remove_indices]
agent.layers = [l for i, l in enumerate(agent.layers) if i not in remove_indices]
def interact_random_matched(agent: Agent, env: Environment) -> InteractionResult:
"""
Ablation B' (v1.0.1): random layers consumed, depletion rate MATCHED to control.

Removes exactly as many layers as the CONTROL condition would have decrypted
on this visit (the number of layers whose key class the environment holds),
but chooses *which* layers at random regardless of class. This holds the
amount of depletion constant and ablates only its structure — the control
the review asked for ("match depletion rates across conditions").

Consequence worth stating: for a *pure-class* agent (every layer the same
class) this condition is indistinguishable from CONTROL, because "random
layers of one class" is the same set as "all layers of that class". The
selectivity experiment therefore cannot separate structure from depletion
with pure agents; see the v1.0.1 errata.
"""
mass_before = agent.mass

mass_after = agent.mass
consumed = mass_before - mass_after
if not env.can_enter(agent):
return InteractionResult(
agent_survived=agent.alive,
agent_could_enter=False,
mass_before=mass_before,
mass_after=mass_before,
total_signal=0, total_loss=0, total_consumed=0,
layers_stripped=0, per_layer=[], delta_L={},
)

# Signal/loss accounting holds (consumed mass is real)
# but per-class attribution is meaningless
signal = consumed // 2
loss = consumed - signal
if not agent.layers:
return InteractionResult(
agent_survived=False,
agent_could_enter=True,
mass_before=0, mass_after=0,
total_signal=0, total_loss=0, total_consumed=0,
layers_stripped=0, per_layer=[], delta_L={},
)

return InteractionResult(
agent_survived=agent.alive,
agent_could_enter=True,
mass_before=mass_before,
mass_after=mass_after,
total_signal=signal,
total_loss=loss,
total_consumed=consumed,
layers_stripped=n_to_remove,
per_layer=[],
delta_L={}, # No per-class tracking — structure destroyed
)
n_to_remove = sum(1 for layer in agent.layers if layer.key_class in env.key_classes)
return _strip_random_layers(agent, n_to_remove, mass_before)


# =============================================================================
Expand Down Expand Up @@ -509,6 +568,7 @@ def run_full_ablation(seed: int = 42) -> list[AblationResult]:
("CONTROL", interact_control),
("IMMORTAL", interact_immortal),
("RANDOM", interact_random),
("RANDOM-MATCHED", interact_random_matched),
]

results = []
Expand Down Expand Up @@ -606,31 +666,21 @@ def format_comparison(results: list[AblationResult]) -> str:
lines.append("=" * 80)
lines.append("")

c, i, r = results[0], results[1], results[2]

lines.append(f" {'Property':<32} {'CONTROL':>14} {'IMMORTAL':>14} {'RANDOM':>14}")
lines.append(f" {'─' * 32} {'─' * 14} {'─' * 14} {'─' * 14}")
col = 16
header = f" {'Property':<28}" + "".join(f"{r.condition:>{col}}" for r in results)
lines.append(header)
lines.append(f" {'─' * 28}" + "".join(f" {'─' * (col - 1)}" for _ in results))

# Scarcity
def scarcity_label(res):
return "YES" if res.scarcity['death_rate'] > 0.01 else "NO"

lines.append(
f" {'Finite lifespans':<32} {scarcity_label(c):>14} "
f"{scarcity_label(i):>14} {scarcity_label(r):>14}"
)

# Stratification
def strat_label(res):
ratio = res.stratification['rich_poor_ratio']
if ratio == float('inf') or (ratio == 1.0 and res.stratification['rich_avg_signal'] == 0):
return "NO (zero output)"
return f"YES ({ratio:.1f}x)" if ratio > 1.3 else "NO"

lines.append(
f" {'Budget stratification':<32} {strat_label(c):>14} "
f"{strat_label(i):>14} {strat_label(r):>14}"
)
return "NO (zero out)"
return f"YES ({ratio:.1f}x)" if ratio > 1.3 else f"NO ({ratio:.1f}x)"

# Selectivity
def select_label(res):
Expand All @@ -642,43 +692,44 @@ def select_label(res):
else:
return "NO"

lines.append(
f" {'Niche differentiation':<32} {select_label(c):>14} "
f"{select_label(i):>14} {select_label(r):>14}"
)

# Accountability
def audit_label(res):
s = res.accountability
if s['meaningful_rate'] < 0.01:
return "NO (vacuous)"
if s['per_class_audit_rate'] > 0.5:
return "YES"
return "PARTIAL"
return "TOTAL ONLY" # total accounting valid, no per-class attribution

lines.append(
f" {'Per-class audit':<32} {audit_label(c):>14} "
f"{audit_label(i):>14} {audit_label(r):>14}"
)
for label, fn in (
("Finite lifespans", scarcity_label),
("Budget stratification", strat_label),
("Niche differentiation", select_label),
("Per-class audit", audit_label),
):
lines.append(f" {label:<28}" + "".join(f"{fn(r):>{col}}" for r in results))

lines.append("")
lines.append("─" * 80)
lines.append(" INTERPRETATION")
lines.append(" INTERPRETATION (read against the measured rows above, not as a script)")
lines.append("─" * 80)
lines.append("")
lines.append(" CONTROL (conservation ON): All four emergent properties present.")
lines.append(" IMMORTAL (no depletion): Zero emergence. No depletion = no scarcity")
lines.append(" = no differentiation = nothing to audit.")
lines.append(" RANDOM (unstructured): Scarcity exists (agents die) but class-selective")
lines.append(" properties vanish. Depletion without structure")
lines.append(" produces death without meaning.")
lines.append("")
lines.append(" Two factors are both necessary:")
lines.append(" 1. DEPLETION (mass decreases on interaction)")
lines.append(" 2. STRUCTURE (depletion is class-selective and accountable)")
lines.append(" CONTROL: conservation ON, class-selective, every affinity layer stripped.")
lines.append(" IMMORTAL: no depletion. Nothing is consumed, so nothing is scarce,")
lines.append(" differentiated, or auditable — and zero signal is returned")
lines.append(" by construction, so 'no economic output' is defined in.")
lines.append(" RANDOM: v1.0.0 condition — class-blind stripping at ~1 layer per")
lines.append(" environment key. Depletion rate NOT matched to control.")
lines.append(" RANDOM-MATCHED: v1.0.1 condition — class-blind stripping of exactly as many")
lines.append(" layers as control would strip. Isolates structure from rate.")
lines.append(" For pure-class agents (selectivity experiment) it is")
lines.append(" identical to control by construction.")
lines.append("")
lines.append(" The conservation law provides both. Remove either: emergence degrades.")
lines.append(" Remove both: emergence disappears entirely.")
lines.append(" What the study can support: the programmed depletion and selection rules")
lines.append(" affect the measured outputs, and per-class audit requires class-selective")
lines.append(" stripping. Whether structured, irreversible depletion is a *necessary*")
lines.append(" condition in general is the paper's hypothesis, not a result these four")
lines.append(" fixtures establish (see paper §9 and the v1.0.1 errata).")
lines.append("=" * 80)

return "\n".join(lines)
Loading
Loading