Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Numerical checks
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
env:
MPLBACKEND: Agg
OMP_NUM_THREADS: '1'
MKL_NUM_THREADS: '1'
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-python@v7.0.0
with:
python-version: '3.12'
cache: pip
- run: python -m pip install --index-url https://download.pytorch.org/whl/cpu torch==2.14.1
- run: python -m pip install -r requirements-tested.txt
- run: python -m pip install --no-deps -e .
- run: python -m pytest
- run: python e1_kuramoto.py --smoke --no-plots --no-mirror --out-root smoke-results
- run: python e2_bc.py --smoke --no-plots --no-mirror --out-root smoke-results
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

A PyTorch research implementation that learns how agents influence one another over time. It combines neural interaction kernels, differentiable simulation, and stochastic optimization.

Companion code for *Graphon Controlled Opinion Dynamics*, by **Joseph K. Miller and Brandon Su**.
Companion code for *Optimal Graphon Control of Collective Dynamics*, by **Joseph K. Miller and Brandon Su**.

## Experiments

Expand All @@ -26,22 +26,23 @@ From the repository root:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install torch numpy matplotlib tqdm pillow
python -m pip install -e .
python e1_kuramoto.py --smoke --device cpu --no-mirror
```

This runs a 20-iteration workflow check. Results appear under `out_1/`, including `untwist.gif`, `kernel_time.gif`, and `metrics.json`.
This runs a 20-iteration workflow check on a small grid and network. Results appear under `out_1/`, including `untwist.gif`, `kernel_time.gif`, and `metrics.json`.

## Code

| File | Contents |
| --- | --- |
| `model_1.py` | Neural controller, simulation, and training |
| `graphon_control/` | Reusable controller, dynamics, objectives, and training |
| `model_1.py` | General experiment entry point |
| `e1_kuramoto.py` | Oscillator experiment |
| `e2_bc.py` | Opinion dynamics experiment |
| `model_1_viz.py` | Visualization utilities |

Use `python <script> --help` for experiment options.
Use `python <script> --help` for experiment options. See [development](docs/DEVELOPMENT.md) for testing and reproduction, and [numerical conventions](docs/NUMERICS.md) for the connection to the paper.

## Research scope

Expand Down
36 changes: 9 additions & 27 deletions collect_paper_figures.sh
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
#!/bin/sh
# Collect the figures used by main5.tex into the flat folder figures/paper/
# (the folder to drag into Overleaf). Re-run after regenerating any experiment
# figures; the experiment mirror dirs (figures/e1_*, figures/e2_*) stay the
# archival source of truth.
set -e
cd "$(dirname "$0")"
mkdir -p figures/paper

# schematic / example figures (names unchanged)
for f in x0-histogram w1 w2 graphon-N graphon-2N; do
cp "figures/$f.pdf" "figures/paper/$f.pdf"
done

# bounded-confidence experiment (subsec:exp-bc)
cp figures/e2_eps1_T10/opinion_traj.pdf figures/paper/bc_opinion_traj.pdf
cp figures/e2_eps1_T10/state_evolution.pdf figures/paper/bc_state_evolution.pdf
cp figures/e2_eps1_T10/kernel_strip.pdf figures/paper/bc_kernel_strip.pdf

# Kuramoto experiment (subsec:exp-kuramoto)
cp figures/e1_k3_T20/r_panels.pdf figures/paper/kur_r_panels.pdf
cp figures/e1_k3_T20/state_evolution.pdf figures/paper/kur_state_evolution.pdf
cp figures/e1_k3_T20/kernel_strip.pdf figures/paper/kur_kernel_strip.pdf
cp figures/e1_k3_T20/winding.pdf figures/paper/kur_winding.pdf
cp figures/e1_k2_T12_sigma0p3/r_noise_panel.pdf figures/paper/kur_noise_panel.pdf

echo "figures/paper/ refreshed:"
ls figures/paper/
# Select source runs and schematics explicitly in a JSON manifest. All inputs
# are checked before any destination is written; existing exports need --overwrite.
set -eu
if [ "$#" -eq 0 ]; then
echo "Usage: $0 MANIFEST.json [--output-dir PATH] [--overwrite]" >&2
exit 2
fi
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
exec "${PYTHON:-python3}" "$script_dir/graphon_control/artifacts.py" collect "$@"
97 changes: 97 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Developing and reproducing experiments

Install the package and development tools from the repository root:

```bash
python -m pip install -e '.[dev]'
python -m pytest
```

`requirements-tested.txt` records the direct dependency versions of the
verified Python 3.12 environment. It is a tested snapshot, not a full transitive
or cross-platform lockfile. CPU tests run in GitHub Actions; accelerator tests
run when compatible hardware is available.

## Structure

- `graphon_control/controls.py`: neural time/label kernel.
- `dynamics.py`, `objectives.py`, `diagnostics.py`: shared scientific operations.
- `training.py`: Adam loop with finite-loss and finite-gradient guards.
- `artifacts.py`: new run directories, provenance, checkpoints and figure export.
- `interactions.py`, `initial_conditions.py`: reusable example ingredients.
- `experiments/`: model-specific configurations, baselines and orchestration.
- `plotting.py`: visualization, separate from the importable numerical core.
- Root Python scripts preserve the previous command and import entry points.

To add a model, supply a differentiable interaction function, an initial-state
sampler and an objective to `train_controller`; reuse the shared rollout and
cost. Keep model-specific diagnostics beside the experiment. Numerical
conventions and the relationship to the paper are in [NUMERICS.md](NUMERICS.md).

## Run and inspect

```bash
python e1_kuramoto.py --smoke --no-mirror
python e2_bc.py --smoke --no-mirror
python model_1.py --smoke --no-plots
```

Smoke runs perform 20 updates on a smaller grid/network and are workflow checks,
not reproductions of manuscript results. `--no-plots` skips rendering only;
configuration, weights, trajectories and diagnostics are still saved.

For the full paper configurations:

```bash
python e2_bc.py --tag paper_bc
python e1_kuramoto.py --k 3 --T 20 --steps 3000 --tag paper_kuramoto
```

Run tags reserve new directories and existing tags are refused. By default,
figures stay in their run directory. `--mirror-figures` also writes into a
new figure directory named for the run, never into a directory selected only
by model parameters.

Every run records a complete `config.json`, `provenance.json` (code revision,
working-tree state, dependency versions and training/evaluation seeds), raw
controller `model.pt`, and numerical artifacts. `complete.json` is written only
after the requested outputs succeed. Training and evaluation have independent
seeds; reproducibility across different hardware/library versions is not assumed.

## Load settings and regenerate plots

```bash
python e1_kuramoto.py --config out_1/e1_paper_kuramoto/config.json --tag rerun
python e1_kuramoto.py --replot out_1/e1_paper_kuramoto
python e2_bc.py --replot out_1/e2_paper_bc
```

Explicit scientific CLI flags override saved settings. Replotting uses saved
arrays and never retrains. It replaces plots within the selected run; original
weights, arrays and metrics are preserved. Generic runs also archive sampled
interaction values so replotting does not need to reconstruct arbitrary Python
callables. Existing raw state_dict checkpoints remain readable through
`artifacts.load_controller`.

## Collect manuscript figures

Choose exact artifacts in a JSON manifest, with paths relative to the manifest:

```json
{
"output_dir": "figures/paper",
"figures": {
"kur_winding.pdf": "out_1/e1_paper_kuramoto/winding.pdf",
"bc_opinion_traj.pdf": "out_1/e2_paper_bc/opinion_traj.pdf"
}
}
```

```bash
./collect_paper_figures.sh paper-figures.json
```

Collection checks every input and destination before copying. Existing exports
require `--overwrite`. Add schematic PDFs explicitly once available; the public
checkout does not contain or generate those manuscript assets. The noise panel
helper checks compatible noisy/noiseless run settings before comparing them.
75 changes: 75 additions & 0 deletions docs/NUMERICS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Numerical conventions

The reference is *Optimal Graphon Control of Collective Dynamics*, Joseph K.
Miller and Brandon Su, October 20, 2026, especially Appendix 5. The manuscript
and its schematic PDFs are maintained separately from this code repository.

## Kernel representation

Rows are receivers, columns are senders. On the uniform grid of N agents,
`W[i,j] = wbar[i,j]/N` (equation 64). The directed controller has nonnegative
weights and unit row sums (equation 67); self-interactions are permitted.
The uniform graphon `w = 1` is `W[i,j] = 1/N`, including the diagonal.

Consequently equation 59 is implemented as

```text
x_next[i] = x[i] + dt * sum_j W[i,j] * phi(x[j] - x[i]) + sigma * delta_B[i]
```

There is no additional 1/N in the drift. Brownian increments have variance dt.
`rollout_with_Ws(..., brownian_increments=...)` accepts physical increments,
not standard normal draws. A supplied generator controls automatic draws;
deterministic rollouts consume no random numbers.

Shapes: initial states `(batch,N)`, sampled kernels `(NT,N,N)`, trajectories
`(NT+1,batch,N)`. Scalar time returns `(N,N)`; a time vector returns `(times,N,N)`,
including a vector with one entry. The horizon must satisfy `T/dt` integral;
invalid grids are rejected instead of silently rounding the endpoint.

## Objective

Equation 60 uses the population variance (`unbiased=False`), the left endpoints
of the time grid, and the kernel density penalty

```text
L2(wbar)^2 = (1/N^2) * sum_ij wbar[i,j]^2 = sum_ij W[i,j]^2
J = sum_n dt * (Var(x[n]) + beta * sum_ij W[n,i,j]^2)
+ gamma_target * (mean(x[-1]) - x_target)^2
```

`trajectory_cost` is the single implementation; `paper_cost` adapts it to the
experiment configuration. The generic trainer uses the same beta coefficient.
The old generic prototype used beta/2: to retain that historical regularization
strength, pass half the former beta. The paper experiment coefficients and
defaults are unchanged.

The generic trainer also exposes named extensions: terminal variance,
uniform-kernel penalty (eta_uniform/2), scheduled mean targets and ramped
running variance. These are disabled or configured explicitly. Checkpoint
targets are snapped to the nearest grid point, and the resolved times should
be used when interpreting them.

## Constraints and diagnostics

Directed row softmax enforces nonnegativity and unit row sums. With
`score_bound=S`, bounded scores give `N*W <= exp(2*S)`; the default `None`
preserves the original unconstrained-score experiment architecture. A fixed
attention cap is therefore not claimed for default runs across refinements.

Receiver variation is measured by equation 14 on the interval:

```text
sum_i sqrt(mean_j((N*W[i+1,j] - N*W[i,j])^2))
```

It is not a training constraint. A finite trained result does not certify a
continuum optimum or the theorem's fixed BV class. The optional symmetric
controller normalizes the average row sum, not each row, and is an exploratory
extension outside the paper's directed row-stochastic class.

The compact-support opinion law is equation 68. The Gaussian fallback has
nonzero tails, so its gap-threshold cluster count is descriptive and does not
certify exact isolation or freezing. The Kuramoto experiments use identical
natural frequencies in a rotating frame; order-parameter alignment alone does
not distinguish phase differences that are integer multiples of 2*pi.
Loading