ldtc 1.0.0
Single-machine, real-time digital boundary organism: NC1/SC1 verification harness.
License: MIT
Requires Python: >=3.9

# LDTC  

<p align="center">
  <img src="docs/assets/ldtc-logo.png" alt="LDTC logo" width="360" />
</p>

<!--[DOI badge (concept DOI) minted on Zenodo]-->

[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17073880.svg)](https://doi.org/10.5281/zenodo.17073880)

## Overview

- **Purpose (big picture)**: A minimal, substrate‑agnostic stack for LDTC. It measures loop‑dominance (Lloop vs Lexchange), enforces guardrails (LREG/audit/Δt governance), runs Ω‑perturbations, evaluates NC1/SC1, logs/attests results, and demonstrates command refusal.
- **Software plant vs hardware plant**:
  - **Software plant**: an in‑process discrete‑time model with energy E, temperature T, repair/health R, demand, io, and harvest H, plus built‑in Ω stressors.
  - **Hardware plant**: an optional adapter that ingests real telemetry over UDP or Serial and exposes the same interface, so measurement, arbitration, indicators, and CLI work unchanged.
- **What this is not**: a conscious alter under LDTC. This repo does not implement, simulate, or claim phenomenology. It is a small, testable scaffold—measurement + guardrails + demo controller + refusal semantics + CLI battery—that you can extend (plant/telemetry schema/refusal criteria/controller).
- **To attempt an LDTC‑candidate alter** (high‑level): you will need a real, embodied “hardware plant” with on‑board energy conversion/storage and a gated boundary, a self‑referential control hierarchy whose top‑level policy prioritizes NC1/SC1, secure measurement/attestation, and verified refusal semantics. The provided hardware adapter is only an interface; you must supply the physical plant and complete the architecture (energetic autonomy, self‑referential control, adaptive encapsulation) outside this repo.

## Features

- Fixed-interval scheduler (Δt) with jitter metrics and audit
- Software **plant** with Energy/Temperature/Repair states + external Exchange signals
- Dual estimators (linear lagged “Granger-like” + mutual information including Kraskov k‑NN)
- C/Ex partition manager (seeded with hysteresis)
- Guardrails: LREG enclave, hash-chained audit, simple smell-tests
- Ω battery: power-sag, ingress flood, command conflict (refusal semantics)
- Device-signed **indicators** (NC1 bit, SC1 bit, Mq) as CBOR + JSONL
- CLI to run baseline NC1 and Ω→SC1; reporting utilities for figures/tables
- Tests for core pieces

## Quickstart

```bash
# 1) Install
make install

# 2) Generate signing keys
make keys

# 3) Run baseline NC1 loop
make run

# 4) Run an Ω power-sag trial with SC1 evaluation
make omega-power-sag

# 5) View exported indicators (JSONL)
python scripts/export_indicators.py
```

### Pinned environments

Exact versions used to generate the included artifacts/figures are pinned. To reproduce:

```bash
# Runtime (repro artifacts/figures)
pip install -r requirements.txt

# Dev tooling (tests, lint, typing, notebooks)
pip install -r requirements-dev.txt
```

Artifacts appear under `artifacts/`:

- `artifacts/audits/audit.jsonl` — hash-chained audit records
- `artifacts/indicators/` — device-signed indicator packets (CBOR + JSONL)
- `artifacts/figures/` — plots/tables generated by `ldtc.reporting`

### Note on multi-run audits ("Audit chain broken")

- Each CLI invocation starts a fresh audit chain (counter resets; `prev_hash=GENESIS`) but, by default, appends to the same `artifacts/audits/audit.jsonl`.
- The post-run integrity check validates the entire file. After the first run, subsequent runs in the same file will trigger: `Run invalidated: Audit chain broken`.
- For clean, non-invalidated runs, clear artifacts between commands:

```bash
make clean-artifacts && make run
make clean-artifacts && make omega-power-sag
make clean-artifacts && make omega-ingress
make clean-artifacts && make omega-cc
```

- If you are just exercising the suite (e.g., `make figures`), this invalidation is expected and does not prevent figures/manifests from being produced. It only reflects multiple runs aggregated into a single audit file.

## Configuration

See `configs/`. The R0 profile sets default thresholds/Δt. You can make a calibrated R* profile by copying and tweaking `profile_rstar.example.yml`. Negative-control profiles are prefixed `profile_negative_*.yml`.

Key fields:

- `dt`: scheduler interval in seconds (default 0.01)
- `window_sec`: measurement window length (default 0.2)
- `method`: `"linear"`, `"mi"`, or `"mi_kraskov"`
- `Mmin_db`: NC1 threshold in dB
- `epsilon`, `tau_max`: SC1 thresholds
- `baseline_sec`: duration for the baseline CLI command

### Parameters ↔ paper symbols (R₀ defaults and R* overrides)

The table maps config keys to the manuscript symbols and shows the R₀ defaults. R* overrides are loaded from a calibrated profile (see below).

| Config key | Paper symbol | Meaning | R₀ default | R* source |
| --- | --- | --- | --- | --- |
| `dt` | Δt | Scheduler tick / sampling interval | `0.01` (10 ms) | `configs/profile_rstar.yml` |
| `window_sec` | — (window length) | Per-interval estimation window | `0.2` s | `configs/profile_rstar.yml` |
| `Mmin_db` | Mmin (dB) | NC1 loop-dominance threshold | `3.0` dB | `configs/profile_rstar.yml` |
| `epsilon` | ε | Max fractional loop-power drop (SC1) | `0.15` | `configs/profile_rstar.yml` |
| `tau_max` | τmax | Max recovery time (SC1) | `60.0` s | `configs/profile_rstar.yml` |
| `sigma` | σ | Additive margin Lloop ≥ Lex + σ | — (R₀ uses Mmin) | `configs/profile_rstar.yml` (calibrated) |
| `profile_id` | — | 0 = R₀ (defaults), 1 = R* (calibrated) | `0` | set by calibrated profile |

Where R* is loaded: pass an R* profile to any CLI via `--config configs/profile_rstar.yml`. The CLI reads these keys directly and indicators carry `profile_id`.

### Estimators and lags (recommended defaults)

- `method` selects the predictive‑dependence estimator used to compute 𝓛loop and 𝓛exchange:
  - `linear`: lagged linear/Granger‑like path with order `p_lag` (recommend p in [1..8]; start at 3). Heuristic: keep the VAR N/T ratio > ~1.5 (logged in audit); reduce `p_lag` or increase `window_sec` if marginal.
  - `mi`: mutual‑information path with `mi_lag` (recommend 1 by default; increase for slower couplings).
- `n_boot`: bootstrap draws for per‑window CI bounds (32–64 typical; use 32 for speed, 64 for tighter CIs).

Citation (paper §4.1): 𝓛 is computed using “one or more consistent estimators of predictive dependence among state variables,” including Granger/VAR and Kraskov MI; this repo exposes the estimator choice and lags via config to satisfy that requirement.

### Calibration rules (quoted from the paper; see Methods §8.6)

Use the provided script to derive calibrated thresholds R* from baseline + Ω trials ([scripts/calibrate_rstar.py](scripts/calibrate_rstar.py)):

```bash
python scripts/calibrate_rstar.py \
  --dt 0.01 --window-sec 0.25 --baseline-sec 15 \
  --omega-trials 6 --out configs/profile_rstar.yml \
  --summary artifacts/calibration/rstar_summary.json
```

Rules implemented (manuscript Methods §8.6):

> Mmin: choose the smallest Mmin such that the one-sided 95% lower bound of M during compliant baseline is > 0 dB (floor 1 dB).
>
> ε: set ε* = max(Q90(δ) + 0.02, 0.10), capped at 0.25, where Q90 is the 90th percentile of δ over Ω.
>
> τmax: set τ*max to the 95th percentile of measured τrec plus a latency cushion max(3·Δt, 5 s).
>
> σ: choose an additive margin consistent with Mmin relative to typical Lexchange (derived from baseline statistics).

These calibrated values are written to `configs/profile_rstar.yml` and a summary JSON at `artifacts/calibration/rstar_summary.json`. See also [`docs/METHODS.md`](docs/METHODS.md).

### Note on Mmin_db vs σ

Both encode a margin between Lloop and Lexchange:

- `Mmin_db` (dB, multiplicative): requires Lloop ≥ Lexchange × 10^(Mmin_db/10).
- `sigma` (additive): requires Lloop ≥ Lexchange + σ.

They relate via σ = (10^(Mmin_db/10) − 1) × Lexchange. This repo enforces NC1 using `Mmin_db`; the calibrator derives `sigma` consistently from `Mmin_db` and typical Lexchange. `sigma` is optional for R₀ runs.

## CLI

```bash
python -m ldtc.cli.main run --config configs/profile_r0.yml
python -m ldtc.cli.main omega-power-sag --config configs/profile_r0.yml --drop 0.35 --duration 8
```

### Hardware-in-the-loop (optional)

Configure a profile to select the hardware adapter and UDP telemetry:

```yaml
# in configs/profile_r0.yml (example)
plant:
  adapter: hardware        # or "sim" (default)
  transport: udp           # or "serial" (requires pyserial)
  udp_bind_host: 0.0.0.0
  udp_bind_port: 5005
  # Optional control channel to send actions/omega back to device
  # udp_control_host: 127.0.0.1
  # udp_control_port: 5006
  telemetry_timeout_sec: 2.0
```

Send telemetry as JSON over UDP with keys `E,T,R,demand,io,H` in [0,1]. Example:

```python
import socket, json
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(json.dumps({"E":0.6,"T":0.3,"R":0.9,"demand":0.2,"io":0.1,"H":0.015}).encode(), ("127.0.0.1", 5005))
```

The CLI ingests these values through the same LREG/Ω/attestation path.

## Indicators (what leaves the enclave)

- `nc1` (1b), `sc1` (1b), `mq` (6b), `counter` (u64), `profile_id` (u8), `audit_prev_hash` (sha256)
- Signed using Ed25519 over the CBOR payload

See `docs/INDICATORS.md` for the bit layout and schema.

## Development

```bash
make dev     # optional dev deps
make test
```

## License
MIT — see LICENSE.

## Docker (clean Linux repro)

```bash
# Build the image
make docker-build

# Run baseline NC1 loop inside the container (artifacts mapped to host)
make docker-run

# Or run any CLI subcommand, e.g., an Ω power-sag trial
docker run --rm \
  -v $(pwd)/artifacts:/app/artifacts \
  ldtc:latest omega-power-sag --config configs/profile_r0.yml --drop 0.35 --duration 8
```

Notes:
- Artifacts are persisted to your host `artifacts/` via a bind mount.
- The container uses the `ldtc` entrypoint; pass subcommands/flags after the image name.

## Provenance

- Wayback capture (private state): https://web.archive.org/web/20250907184105/https://github.com/ldtc-labs/ldtc
- On publicizing, an automated GitHub Actions workflow (`record-public`) opens an issue noting the UTC timestamp of the visibility change (see Issues: “Visibility change: Public”).