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.
- cadence_net-0.1.0/.github/workflows/ci.yml +22 -0
- cadence_net-0.1.0/.gitignore +11 -0
- cadence_net-0.1.0/LICENSE +21 -0
- cadence_net-0.1.0/PKG-INFO +152 -0
- cadence_net-0.1.0/README.md +123 -0
- cadence_net-0.1.0/docs/backends.md +44 -0
- cadence_net-0.1.0/docs/concepts.md +70 -0
- cadence_net-0.1.0/docs/quickstart.md +139 -0
- cadence_net-0.1.0/docs/receipts.md +47 -0
- cadence_net-0.1.0/examples/half_center.py +29 -0
- cadence_net-0.1.0/examples/ring_protocol.py +79 -0
- cadence_net-0.1.0/pyproject.toml +55 -0
- cadence_net-0.1.0/src/cadence/__init__.py +54 -0
- cadence_net-0.1.0/src/cadence/custody.py +72 -0
- cadence_net-0.1.0/src/cadence/protocol.py +275 -0
- cadence_net-0.1.0/src/cadence/receipts.py +107 -0
- cadence_net-0.1.0/src/cadence/reference.py +103 -0
- cadence_net-0.1.0/src/cadence/rules.py +72 -0
- cadence_net-0.1.0/src/cadence/settle.py +297 -0
- cadence_net-0.1.0/src/cadence/wiring.py +138 -0
- cadence_net-0.1.0/tests/test_cadence.py +156 -0
|
@@ -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,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)}")
|