cadence-net 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ test:
8
+ runs-on: ${{ matrix.os }}
9
+ strategy:
10
+ matrix:
11
+ os: [ubuntu-latest, macos-latest]
12
+ python: ["3.11", "3.12", "3.13"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python }}
18
+ - run: python -m pip install -e ".[dev]"
19
+ - run: ruff check src tests examples
20
+ - run: mypy
21
+ - run: pytest -q
22
+ - run: python examples/half_center.py && python examples/ring_protocol.py
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ data/
11
+ *_receipt.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bernhard Mueller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.5
2
+ Name: cadence-net
3
+ Version: 0.1.0
4
+ Summary: Machine learning by patch-net settlement: owner-local repair, held-out tests, receipts.
5
+ Project-URL: Homepage, https://github.com/muellerberndt/cadence
6
+ Project-URL: Issues, https://github.com/muellerberndt/cadence/issues
7
+ Author: Bernhard Mueller
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: connectome,local learning,patch net,receipts,settlement
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: numpy>=1.26
21
+ Provides-Extra: accel
22
+ Requires-Dist: torch>=2.2; extra == 'accel'
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.10; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.5; extra == 'dev'
27
+ Requires-Dist: torch>=2.2; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Cadence
31
+
32
+ **Machine learning by patch-net settlement: owner-local repair, held-out tests, receipts.**
33
+
34
+ A patch net is a set of *owners*, each holding one patch of state, joined by declared
35
+ *overlaps*. Nothing is computed globally. Every owner repairs its own patch from what
36
+ arrives over its overlaps, and the state the net comes to rest in is the answer. Cadence
37
+ is the library for building, settling, testing, and certifying such nets, from a
38
+ six-owner ring to a 161,827-owner nervous system read from a connectome.
39
+
40
+ ```python
41
+ import cadence as cd
42
+
43
+ wiring = cd.Wiring.from_edges(4, pre=[0, 1, 2, 3], post=[1, 2, 3, 0], count=[120] * 4)
44
+ engine = cd.Settlement(wiring, cd.GradedRule(gain=0.03))
45
+ engine.settle(clamp={0: 1.0}, steps=60).activation.round(2)
46
+ # array([1., 1., 1., 1.])
47
+ ```
48
+
49
+ ## What is in the box
50
+
51
+ | layer | what it gives you |
52
+ |---|---|
53
+ | `Wiring` | owners and overlaps as sorted arrays, named sets, digests; built from edge lists |
54
+ | `GradedRule`, `Adaptation` | the owner rule: a graded potential with a rectified sigmoid that emits nothing at rest, and an optional adaptation variable that turns fixed points into rhythm |
55
+ | `Settlement` | the engine, on NumPy float64 (`"cpu"`) or torch (`"torch"`: CUDA, Apple silicon, or CPU) |
56
+ | `conformance`, `settle_owner_by_owner`, `Ledger` | an owner-by-owner reference engine with a message ledger, to certify that a fast backend computes nothing the owners could not |
57
+ | `Protocol`, `Row`, `shuffled`, `select_gain` | declared stimuli, readouts, and held-out facts with preconditions; the shuffled-wiring control; gain selection under a sparsity cap |
58
+ | `Receipt`, `source_manifest` | canonical JSON bound to code and data by digest, verified by recomputing every pass flag |
59
+ | `Source`, `fetch` | pinned public data, downloaded once, verified always |
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install cadence-net # NumPy only; the import is `cadence`
65
+ pip install "cadence-net[accel]" # adds torch for CUDA and Apple silicon
66
+ ```
67
+
68
+ Python 3.11 or newer. On an M-series Mac the torch backend runs on MPS in float32; on CUDA
69
+ it runs in float64. The CPU backend is always float64 and is the one receipts are made on.
70
+
71
+ ## Sixty seconds
72
+
73
+ **A wiring** is `n` owners plus directed overlaps with a contact count and a sign. Build it
74
+ from edge lists; parallel overlaps merge, autapses drop, and you can name sets of owners.
75
+
76
+ ```python
77
+ w = cd.Wiring.from_edges(
78
+ 3, pre=[0, 0, 1], post=[1, 2, 2], count=[80, 20, 80], sign=[1, 1, -1],
79
+ sets={"input": [0], "output": [2]},
80
+ )
81
+ ```
82
+
83
+ **A rule** says what an owner does with its inbox. `GradedRule` is the one every
84
+ connectome lane uses. Add `Adaptation` when you want rhythm.
85
+
86
+ ```python
87
+ rule = cd.GradedRule(gain=0.02, adaptation=cd.Adaptation(tau_steps=40, strength=1.0))
88
+ ```
89
+
90
+ **Settle** from rest under a clamp. A clamp is a list of owners at full amplitude, a
91
+ `{owner: level}` map, or a dense drive vector. Ask for the trajectory when you want to watch.
92
+
93
+ ```python
94
+ engine = cd.Settlement(w, rule, backend="torch") # or "cpu"
95
+ state = engine.settle(w.members("input"), steps=100, trajectory=True)
96
+ state.activation, state.trajectory.shape
97
+ ```
98
+
99
+ **Declare a protocol** and score it. Rows are held-out facts with predicates that carry
100
+ their preconditions. The shuffled control keeps every count, sign, and set.
101
+
102
+ ```python
103
+ protocol = cd.Protocol(
104
+ stimuli={"rest": (), "drive": ("input",)},
105
+ training=[("drive", "output", "active")],
106
+ rows=[cd.Row("R1", "rest", "output", "inactive", "nothing in, nothing out")],
107
+ )
108
+ protocol.score(engine)["passed"], protocol.score(cd.Settlement(cd.shuffled(w, 0), rule))["passed"]
109
+ ```
110
+
111
+ **Certify** the backend and **write a receipt**.
112
+
113
+ ```python
114
+ cd.conformance(engine, w.members("input"))["max_abs_deviation"]
115
+ receipt = cd.Receipt.build("my-lane/v1", {"score": protocol.score(engine)}, sources=[("lane.py", Path("lane.py"))])
116
+ receipt.write(Path("receipt.json"))
117
+ cd.Receipt.verify(Path("receipt.json"), sources=[("lane.py", Path("lane.py"))])
118
+ ```
119
+
120
+ The [quickstart](docs/quickstart.md) walks through a connectome; [concepts](docs/concepts.md)
121
+ explains why the library is shaped this way; [backends](docs/backends.md) covers devices
122
+ and precision; [receipts](docs/receipts.md) covers what a verified result means.
123
+
124
+ ## Discipline
125
+
126
+ Three rules the library enforces rather than recommends:
127
+
128
+ 1. **Owner-local or nothing.** The reference engine reads one owner and its inbox at a
129
+ time and ledgers every delivery. `conformance` compares any backend against it.
130
+ 2. **Held out means held out.** A protocol names the few facts a model may be shown. Gains
131
+ are selected on those alone, and only while the net stays sparse, because runaway
132
+ activity lights every readout and proves nothing about the wiring.
133
+ 3. **A result is a receipt.** Canonical JSON, a digest, the digests of the code and data,
134
+ and every pass flag recomputable from the stored readings. A receipt that fails to
135
+ verify is not a result.
136
+
137
+ ## Where it comes from
138
+
139
+ Cadence consolidates the lanes of the observer patch net programme: a *C. elegans*
140
+ connectome scored against classical ablation phenotypes, the FlyWire *Drosophila* brain
141
+ and the MANC nerve cord joined by their descending neurons and scored against held-out
142
+ taste, grooming, escape, olfaction, and motor facts, and that nervous system driving a
143
+ biomechanical fly in MuJoCo. Every one of those lanes is a wiring, a rule, a protocol, a
144
+ control, and a receipt; the library is what they had in common.
145
+
146
+ ## Status
147
+
148
+ Version 0.1.0 is the core: wiring, rule, engine, reference, protocol, receipts, custody.
149
+ On the roadmap: the owner-local free/nudged learning rule, closure sub-nets for
150
+ in-browser settlement, environment adapters for embodiment, and connectome loaders.
151
+
152
+ MIT licensed.
@@ -0,0 +1,123 @@
1
+ # Cadence
2
+
3
+ **Machine learning by patch-net settlement: owner-local repair, held-out tests, receipts.**
4
+
5
+ A patch net is a set of *owners*, each holding one patch of state, joined by declared
6
+ *overlaps*. Nothing is computed globally. Every owner repairs its own patch from what
7
+ arrives over its overlaps, and the state the net comes to rest in is the answer. Cadence
8
+ is the library for building, settling, testing, and certifying such nets, from a
9
+ six-owner ring to a 161,827-owner nervous system read from a connectome.
10
+
11
+ ```python
12
+ import cadence as cd
13
+
14
+ wiring = cd.Wiring.from_edges(4, pre=[0, 1, 2, 3], post=[1, 2, 3, 0], count=[120] * 4)
15
+ engine = cd.Settlement(wiring, cd.GradedRule(gain=0.03))
16
+ engine.settle(clamp={0: 1.0}, steps=60).activation.round(2)
17
+ # array([1., 1., 1., 1.])
18
+ ```
19
+
20
+ ## What is in the box
21
+
22
+ | layer | what it gives you |
23
+ |---|---|
24
+ | `Wiring` | owners and overlaps as sorted arrays, named sets, digests; built from edge lists |
25
+ | `GradedRule`, `Adaptation` | the owner rule: a graded potential with a rectified sigmoid that emits nothing at rest, and an optional adaptation variable that turns fixed points into rhythm |
26
+ | `Settlement` | the engine, on NumPy float64 (`"cpu"`) or torch (`"torch"`: CUDA, Apple silicon, or CPU) |
27
+ | `conformance`, `settle_owner_by_owner`, `Ledger` | an owner-by-owner reference engine with a message ledger, to certify that a fast backend computes nothing the owners could not |
28
+ | `Protocol`, `Row`, `shuffled`, `select_gain` | declared stimuli, readouts, and held-out facts with preconditions; the shuffled-wiring control; gain selection under a sparsity cap |
29
+ | `Receipt`, `source_manifest` | canonical JSON bound to code and data by digest, verified by recomputing every pass flag |
30
+ | `Source`, `fetch` | pinned public data, downloaded once, verified always |
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install cadence-net # NumPy only; the import is `cadence`
36
+ pip install "cadence-net[accel]" # adds torch for CUDA and Apple silicon
37
+ ```
38
+
39
+ Python 3.11 or newer. On an M-series Mac the torch backend runs on MPS in float32; on CUDA
40
+ it runs in float64. The CPU backend is always float64 and is the one receipts are made on.
41
+
42
+ ## Sixty seconds
43
+
44
+ **A wiring** is `n` owners plus directed overlaps with a contact count and a sign. Build it
45
+ from edge lists; parallel overlaps merge, autapses drop, and you can name sets of owners.
46
+
47
+ ```python
48
+ w = cd.Wiring.from_edges(
49
+ 3, pre=[0, 0, 1], post=[1, 2, 2], count=[80, 20, 80], sign=[1, 1, -1],
50
+ sets={"input": [0], "output": [2]},
51
+ )
52
+ ```
53
+
54
+ **A rule** says what an owner does with its inbox. `GradedRule` is the one every
55
+ connectome lane uses. Add `Adaptation` when you want rhythm.
56
+
57
+ ```python
58
+ rule = cd.GradedRule(gain=0.02, adaptation=cd.Adaptation(tau_steps=40, strength=1.0))
59
+ ```
60
+
61
+ **Settle** from rest under a clamp. A clamp is a list of owners at full amplitude, a
62
+ `{owner: level}` map, or a dense drive vector. Ask for the trajectory when you want to watch.
63
+
64
+ ```python
65
+ engine = cd.Settlement(w, rule, backend="torch") # or "cpu"
66
+ state = engine.settle(w.members("input"), steps=100, trajectory=True)
67
+ state.activation, state.trajectory.shape
68
+ ```
69
+
70
+ **Declare a protocol** and score it. Rows are held-out facts with predicates that carry
71
+ their preconditions. The shuffled control keeps every count, sign, and set.
72
+
73
+ ```python
74
+ protocol = cd.Protocol(
75
+ stimuli={"rest": (), "drive": ("input",)},
76
+ training=[("drive", "output", "active")],
77
+ rows=[cd.Row("R1", "rest", "output", "inactive", "nothing in, nothing out")],
78
+ )
79
+ protocol.score(engine)["passed"], protocol.score(cd.Settlement(cd.shuffled(w, 0), rule))["passed"]
80
+ ```
81
+
82
+ **Certify** the backend and **write a receipt**.
83
+
84
+ ```python
85
+ cd.conformance(engine, w.members("input"))["max_abs_deviation"]
86
+ receipt = cd.Receipt.build("my-lane/v1", {"score": protocol.score(engine)}, sources=[("lane.py", Path("lane.py"))])
87
+ receipt.write(Path("receipt.json"))
88
+ cd.Receipt.verify(Path("receipt.json"), sources=[("lane.py", Path("lane.py"))])
89
+ ```
90
+
91
+ The [quickstart](docs/quickstart.md) walks through a connectome; [concepts](docs/concepts.md)
92
+ explains why the library is shaped this way; [backends](docs/backends.md) covers devices
93
+ and precision; [receipts](docs/receipts.md) covers what a verified result means.
94
+
95
+ ## Discipline
96
+
97
+ Three rules the library enforces rather than recommends:
98
+
99
+ 1. **Owner-local or nothing.** The reference engine reads one owner and its inbox at a
100
+ time and ledgers every delivery. `conformance` compares any backend against it.
101
+ 2. **Held out means held out.** A protocol names the few facts a model may be shown. Gains
102
+ are selected on those alone, and only while the net stays sparse, because runaway
103
+ activity lights every readout and proves nothing about the wiring.
104
+ 3. **A result is a receipt.** Canonical JSON, a digest, the digests of the code and data,
105
+ and every pass flag recomputable from the stored readings. A receipt that fails to
106
+ verify is not a result.
107
+
108
+ ## Where it comes from
109
+
110
+ Cadence consolidates the lanes of the observer patch net programme: a *C. elegans*
111
+ connectome scored against classical ablation phenotypes, the FlyWire *Drosophila* brain
112
+ and the MANC nerve cord joined by their descending neurons and scored against held-out
113
+ taste, grooming, escape, olfaction, and motor facts, and that nervous system driving a
114
+ biomechanical fly in MuJoCo. Every one of those lanes is a wiring, a rule, a protocol, a
115
+ control, and a receipt; the library is what they had in common.
116
+
117
+ ## Status
118
+
119
+ Version 0.1.0 is the core: wiring, rule, engine, reference, protocol, receipts, custody.
120
+ On the roadmap: the owner-local free/nudged learning rule, closure sub-nets for
121
+ in-browser settlement, environment adapters for embodiment, and connectome loaders.
122
+
123
+ MIT licensed.
@@ -0,0 +1,44 @@
1
+ # Backends, devices, precision
2
+
3
+ ```python
4
+ import cadence as cd
5
+ cd.available_backends()
6
+ # {'cpu': 'numpy float64', 'torch': 'mps float32'} # on an M-series Mac with torch installed
7
+ ```
8
+
9
+ | backend | where it runs | precision | use it for |
10
+ |---|---|---|---|
11
+ | `"cpu"` | NumPy | float64 | receipts, conformance, anything you will cite |
12
+ | `"torch"` on CUDA | GPU | float64 | large wirings at receipt precision |
13
+ | `"torch"` on MPS | Apple silicon GPU | float32 | interactive work; MPS has no float64 |
14
+ | `"torch"` on CPU | torch CPU | float64 | when torch is installed and you want one code path |
15
+
16
+ Both backends do the same arithmetic: one scatter of every overlap's message into its
17
+ owner's inbox per step, then one owner-local update. NumPy uses `bincount`; torch uses
18
+ `index_add_`. Neither builds a dense matrix, so a wiring of a few million overlaps settles
19
+ in tens of milliseconds per step on a GPU and under a second on a CPU.
20
+
21
+ ## Choosing a device
22
+
23
+ ```python
24
+ cd.Settlement(w, rule, backend="torch") # cuda, else mps, else cpu
25
+ cd.Settlement(w, rule, backend="torch", device="cpu") # force
26
+ ```
27
+
28
+ ## Precision matters
29
+
30
+ Owners can sit on knife edges, where a difference of 1e-7 in a drive flips a bistable
31
+ readout. Float32 summation order alone did that to a motor neuron in the fly brain. Two
32
+ habits keep this honest:
33
+
34
+ 1. Make receipts on `"cpu"` or on CUDA float64.
35
+ 2. When you use MPS float32 for a page or a demo, run `cd.conformance` on the same wiring
36
+ and clamp, and show the deviation. It is usually around 1e-5; when it is not, a readout
37
+ near threshold is telling you something.
38
+
39
+ ## Extending to another device
40
+
41
+ The torch kernel is one small class, `settle._TorchKernel`, with three operations: gather
42
+ `s[pre]`, scatter-add into `inbox`, and the elementwise update. Any array library that
43
+ offers those three can host a backend; the reference engine and `conformance` are what you
44
+ check it against.
@@ -0,0 +1,70 @@
1
+ # Concepts
2
+
3
+ ## Owners, overlaps, settlement
4
+
5
+ A patch net is not a function that maps inputs to outputs. It is a set of owners, each
6
+ holding a patch of state, joined by overlaps that carry messages. There is no global step:
7
+ every owner repairs its own patch from its own state, its inbox, its clamp, and its bias.
8
+ Iterating that repair from rest is *settlement*, and the state the net rests in is what a
9
+ readout sees.
10
+
11
+ The graded rule is
12
+
13
+ v <- v + dt * ( -v + inbox + clamp + bias - strength * a )
14
+ inbox = sum over inbound overlaps of gain * count * sign * exp(log_gain[pre]) * s[pre]
15
+ s(v) = rectified sigmoid, exactly zero at rest
16
+
17
+ The drive is absolute per contact: a hub integrates every contact it receives, and the
18
+ one global parameter is the drive of one contact. That follows the leaky integrate-and-fire
19
+ convention used for the fly brain, and it is why gains are small numbers.
20
+
21
+ ## Why rest emits nothing
22
+
23
+ A plain sigmoid emits a few percent at rest. Multiplied by thousands of contacts on a hub
24
+ that leak ignites the net with no input at all. The activation is therefore re-based so
25
+ that an owner at exactly rest publishes exactly zero, in the engine's own precision. Rest
26
+ is then a fixed point of the whole net, and "nothing in, nothing out" is a testable fact.
27
+
28
+ ## Why adaptation
29
+
30
+ A graded rule with one time constant converges to a fixed point under a constant clamp:
31
+ a posture, never a gait. Adaptation adds one slow variable per owner that follows its own
32
+ activation and subtracts from its own drive. With mutual inhibition, which every real
33
+ wiring has in abundance, that is the half-center oscillator, and the net can carry rhythm.
34
+ It is still owner-local: an owner reads only its own adaptation. It is off by default, and
35
+ a lane that turns it on says how it chose the two numbers.
36
+
37
+ ## Why sparsity gates the gain
38
+
39
+ Raise the gain enough and any wiring runs away: a large fraction of owners saturate and
40
+ every readout lights. In that regime a protocol passes for reasons that have nothing to do
41
+ with the wiring, and a shuffled control passes just as well. So a gain is admissible only
42
+ while the net stays sparse under the training stimuli. The cap is declared, the table of
43
+ gains tried is recorded, and the same rule is applied to the control.
44
+
45
+ ## Why float64
46
+
47
+ Owners sit on knife edges. In the fly brain, one motor neuron under one taste settled to
48
+ 1.0 in one run and to 0.01 in another with the same wiring and clamp, because float32
49
+ summation order differed. Receipts are made on the float64 CPU backend for that reason.
50
+ Accelerated backends are for looking, and they come with a conformance number.
51
+
52
+ ## Why a shuffled control
53
+
54
+ A protocol scored on a wiring alone measures the protocol as much as the wiring. The
55
+ control keeps everything about the wiring that is not the wiring: every count, every sign,
56
+ every owner's out-degree, every named set; only who talks to whom is destroyed. What the
57
+ wiring passes and the control fails is what the wiring predicted.
58
+
59
+ ## Why receipts
60
+
61
+ Numbers in a notebook rot. A receipt is canonical JSON with its own digest, the digests of
62
+ the code and data that produced it, and enough stored readings that every pass flag can be
63
+ recomputed by the verifier. Editing any source file the receipt names invalidates it, on
64
+ purpose: a result belongs to the code that made it.
65
+
66
+ ## What Cadence does not claim
67
+
68
+ Settling a measured wiring and passing held-out facts is evidence that the wiring carries
69
+ those facts under this rule. It is not a claim about biology beyond the scored predicates,
70
+ and nothing in the library ascribes experience to anything.
@@ -0,0 +1,139 @@
1
+ # Quickstart: a measured wiring, a held-out test, a receipt
2
+
3
+ This walks the whole loop on a wiring you build yourself, then shows the same calls on a
4
+ connectome edge list. Everything runs on the NumPy backend; add `backend="torch"` for a GPU.
5
+
6
+ ## 1. Build a wiring
7
+
8
+ ```python
9
+ import numpy as np
10
+ import cadence as cd
11
+
12
+ rng = np.random.default_rng(0)
13
+ n, e = 200, 1500
14
+ w = cd.Wiring.from_edges(
15
+ n,
16
+ pre=rng.integers(0, n, e),
17
+ post=rng.integers(0, n, e),
18
+ count=rng.integers(5, 40, e), # contacts per overlap
19
+ sign=np.where(rng.random(e) < 0.3, -1.0, 1.0), # 30% inhibitory
20
+ sets={"sensors": range(0, 10), "motors": range(190, 200)},
21
+ min_count=5, # drop weak overlaps, as connectomes do
22
+ )
23
+ w.summary()
24
+ ```
25
+
26
+ `Wiring` sorts overlaps by (post, pre), merges parallel ones, drops autapses, and digests
27
+ itself. Sets are tuples of owner rows and travel with the wiring, so protocols speak in names.
28
+
29
+ ## 2. Settle it
30
+
31
+ ```python
32
+ rule = cd.GradedRule(gain=0.02) # drive per contact per unit activation
33
+ engine = cd.Settlement(w, rule)
34
+ state = engine.settle(w.members("sensors"), steps=60, trajectory=True)
35
+ state.mean(w.sets["motors"]), state.active(), state.trajectory.shape
36
+ ```
37
+
38
+ Rest is an exact fixed point: with no clamp nothing fires. The activation is a sigmoid
39
+ re-based to emit zero at rest, so a quiet net stays quiet.
40
+
41
+ ## 3. Declare what you will test
42
+
43
+ ```python
44
+ protocol = cd.Protocol(
45
+ stimuli={"rest": (), "touch": ("sensors",)},
46
+ training=[("touch", "motors", "active")], # the one fact the model may see
47
+ rows=[
48
+ cd.Row("R1", "rest", "motors", "inactive", "no input, no output"),
49
+ cd.Row("R2", "touch", "motors", "reduced", "cutting the sensors", ablate=("sensors",)),
50
+ ],
51
+ )
52
+ ```
53
+
54
+ Predicates carry preconditions: `reduced` requires the intact net to have been active, so a
55
+ dead net cannot pass it.
56
+
57
+ ## 4. Select the gain on the training fact, under a sparsity cap
58
+
59
+ ```python
60
+ gain, table = cd.select_gain(
61
+ lambda g: cd.Settlement(w, rule.replace(gain=g)), protocol, grid=(0.01, 0.02, 0.03, 0.05),
62
+ )
63
+ engine = cd.Settlement(w, rule.replace(gain=gain))
64
+ ```
65
+
66
+ A gain is admissible only while at most 5% of owners are active under the training
67
+ stimuli; pass `sparsity_cap=None` for toy nets that are meant to light entirely. Above the
68
+ cap the net runs away and every readout lights, which says nothing about the wiring. The
69
+ table records every gain tried.
70
+
71
+ ## 5. Score, and score the control
72
+
73
+ ```python
74
+ report = protocol.score(engine)
75
+ control = protocol.score(cd.Settlement(cd.shuffled(w, seed=0), rule.replace(gain=gain)))
76
+ report["passed"], control["passed"]
77
+ ```
78
+
79
+ `shuffled` permutes postsynaptic endpoints and keeps every count, sign, out-degree, and
80
+ named set. If the wiring passes what the control does not, the prediction came from the
81
+ wiring.
82
+
83
+ ## 6. Certify the engine
84
+
85
+ ```python
86
+ cd.conformance(engine, w.members("sensors"), steps=60)
87
+ # {'backend': 'cpu', 'max_abs_deviation': 1e-15, 'ledger': {'clean': True, ...}, ...}
88
+ ```
89
+
90
+ The reference engine reads one owner and its inbox slice at a time and counts one delivery
91
+ per declared overlap per step. Run this on the torch backend too, and record the number.
92
+
93
+ ## 7. Write the receipt
94
+
95
+ ```python
96
+ from pathlib import Path
97
+
98
+ receipt = cd.Receipt.build(
99
+ "quickstart/v1",
100
+ {
101
+ "wiring": w.summary(),
102
+ "rule": engine.rule.to_dict(),
103
+ "gain_selection": {"selected": gain, "table": table},
104
+ "protocol": protocol.to_dict(),
105
+ "score": report,
106
+ "control": control,
107
+ "conformance": cd.conformance(engine, w.members("sensors")),
108
+ },
109
+ sources=[("quickstart.py", Path(__file__))] if "__file__" in globals() else [],
110
+ )
111
+ receipt.write(Path("receipt.json"))
112
+
113
+ def check(body):
114
+ for row in body["score"]["rows"]:
115
+ if row["passed"] != cd.evaluate_predicate(row["predicate"], row["reading"], row["reference"]):
116
+ return f"row {row['id']} pass flag does not follow from its readings"
117
+ return None
118
+
119
+ cd.Receipt.verify(Path("receipt.json"), check=check)
120
+ ```
121
+
122
+ ## The same calls on a connectome
123
+
124
+ A connectome is an edge list with a synapse count and a presynaptic sign. Pin the file,
125
+ fetch it once, verify it always, and build the wiring the same way.
126
+
127
+ ```python
128
+ src = cd.Source(
129
+ key="edges", file="edges.csv", url="https://example.org/edges.csv",
130
+ sha256="<64 hex chars>", citation="Who measured it, where it was published",
131
+ )
132
+ paths = cd.fetch([src], root=Path("data"), allow_download=True)
133
+ pre, post, count, sign = load_your_csv(paths["edges"]) # your parser
134
+ w = cd.Wiring.from_edges(n_neurons, pre=pre, post=post, count=count, sign=sign, min_count=5,
135
+ sets={"sugar_grn": [...], "mn9": [...]})
136
+ ```
137
+
138
+ From there the protocol, the control, the conformance check, and the receipt are the
139
+ same six calls, with `cd.manifest([src])` embedded in the receipt body as custody.
@@ -0,0 +1,47 @@
1
+ # Receipts
2
+
3
+ A receipt is a result that can be checked by someone who was not there.
4
+
5
+ ## Shape
6
+
7
+ ```json
8
+ {
9
+ "kind": "my-lane/v1",
10
+ "body": { "...": "whatever the lane recorded: readings, rows, gain tables, custody" },
11
+ "source": { "files": [{"path": "lane.py", "sha256": "..."}], "manifest_sha256": "..." },
12
+ "digest": "sha256 of the canonical JSON of kind, body, and source"
13
+ }
14
+ ```
15
+
16
+ Written as newline-terminated canonical JSON: sorted keys, no whitespace, no NaN.
17
+
18
+ ## Verification
19
+
20
+ `Receipt.verify(path, sources=..., check=...)` recomputes four things and fails on the first
21
+ that disagrees:
22
+
23
+ 1. the file is byte-for-byte the canonical form of its own content;
24
+ 2. the embedded digest matches;
25
+ 3. the source files named in the manifest still hash to what the receipt says;
26
+ 4. the caller's `check(body)` finds no arithmetic problem, which for a protocol receipt
27
+ means every pass flag follows from the stored readings and every tally follows from
28
+ the rows.
29
+
30
+ ## What belongs in the body
31
+
32
+ - the wiring summary and digest, and the custody block for measured data;
33
+ - the rule and the engine description;
34
+ - the gain selection table, every gain tried, with its admissibility;
35
+ - the protocol as data, including the reference for every row;
36
+ - the score, with readings and reference readings on every row;
37
+ - the control's score;
38
+ - the conformance report for the backend used;
39
+ - an explicit boundary block: what the lane declares rather than derives, and what it does
40
+ not claim.
41
+
42
+ ## Editing invalidates, on purpose
43
+
44
+ Every file named in the source manifest is bound to the receipt. Change one line in the
45
+ lane and the receipt no longer verifies until the lane is re-run. Batch edits, then re-run
46
+ once. When several lanes share a module, an edit to that module re-runs all of them; plan
47
+ it as a versioned break and record the library version in the body.
@@ -0,0 +1,29 @@
1
+ """Two owners that inhibit each other: a fixed point without adaptation, a rhythm with it.
2
+
3
+ Run: python examples/half_center.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import cadence as cd
9
+
10
+ wiring = cd.Wiring.from_edges(2, pre=[0, 1], post=[1, 0], count=[60, 60], sign=[-1, -1])
11
+ clamp = {0: 1.0, 1: 0.95} # both driven, one a little harder
12
+
13
+ still = cd.Settlement(wiring, cd.GradedRule(gain=0.03)).settle(clamp, steps=400, trajectory=True)
14
+ rhythm = cd.Settlement(
15
+ wiring, cd.GradedRule(gain=0.03, adaptation=cd.Adaptation(tau_steps=40, strength=2.0))
16
+ ).settle(clamp, steps=400, trajectory=True)
17
+
18
+ assert still.trajectory is not None and rhythm.trajectory is not None
19
+ print(
20
+ "without adaptation, last 100 steps, std per owner:",
21
+ still.trajectory[-100:].std(axis=0).round(4),
22
+ )
23
+ print(
24
+ "with adaptation, last 200 steps, std per owner:",
25
+ rhythm.trajectory[-200:].std(axis=0).round(3),
26
+ )
27
+ for t in range(300, 400, 10):
28
+ bar = "".join("#" if x > 0.5 else "." for x in rhythm.trajectory[t])
29
+ print(f"step {t}: {bar} {rhythm.trajectory[t].round(2)}")