acdc-search 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,229 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # UV
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
201
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
203
+ # you could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
221
+
222
+ # The original paper
223
+ *.pdf
224
+
225
+ # Matlab source files
226
+ _src
227
+
228
+ # Connectome source files
229
+ _csv
@@ -0,0 +1,53 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philipp Schlegel
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.
22
+
23
+
24
+ ================================================================================
25
+ THIRD-PARTY NOTICES
26
+ ================================================================================
27
+
28
+ This package is a Python translation of the MATLAB reference implementation of
29
+ AC(+)DC search, published as supplementary material to:
30
+
31
+ D. D. Lee, A. Matsliah and L. K. Saul,
32
+ "AC(+)DC search: behind the winning solution to the FlyWire graph-matching
33
+ challenge",
34
+ Transactions on Machine Learning Research (01/2026).
35
+ https://openreview.net/forum?id=8MjCOMyaDf
36
+
37
+ That original work is distributed under the MIT licence, reproduced below in
38
+ full and verbatim from the LICENSE.txt file of the supplementary material. Its
39
+ copyright notice and permission notice are retained here as that licence
40
+ requires. See the NOTICE file for attribution details and a summary of the
41
+ changes made in translating it.
42
+
43
+ --------------------------------------------------------------------------------
44
+
45
+ MIT License
46
+
47
+ Copyright (c) 2025 Daniel Lee and Lawrence Saul
48
+
49
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
50
+
51
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
52
+
53
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ NOTICE
2
+ ======
3
+
4
+ This package (`acdc-search`, importable as `acdc`) is a Python translation of the
5
+ MATLAB reference implementation of AC(+)DC search, published as supplementary
6
+ material to:
7
+
8
+ D. D. Lee, A. Matsliah and L. K. Saul,
9
+ "AC(+)DC search: behind the winning solution to the FlyWire graph-matching
10
+ challenge",
11
+ Transactions on Machine Learning Research (01/2026).
12
+ https://openreview.net/forum?id=8MjCOMyaDf
13
+
14
+ The original MATLAB implementation is:
15
+
16
+ Copyright (c) 2025 Daniel Lee and Lawrence Saul
17
+
18
+ and is distributed under the MIT licence. That licence, including its copyright
19
+ and permission notices, is reproduced in full in the LICENSE file of this
20
+ package, as the licence requires.
21
+
22
+ Because the original is MIT-licensed, this translation is offered under the MIT
23
+ licence as well (see LICENSE). If you use this package, please cite the paper
24
+ above: the algorithm is the authors' work, and this is a translation of it.
25
+
26
+ This package does not redistribute any of the original MATLAB source files.
27
+
28
+
29
+ Summary of changes
30
+ ------------------
31
+
32
+ The MIT licence does not require modifications to be documented. The following
33
+ summary is provided for clarity about how faithful the translation is:
34
+
35
+ * The algorithm was translated from MATLAB to Python, using NumPy and SciPy
36
+ for array and sparse-matrix operations, and reorganised from a flat set of
37
+ scripts into an installable package with a documented public API:
38
+
39
+ compute_gradient.m + score expression -> acdc/objective.py
40
+ permutation_match.m -> acdc/matching.py
41
+ do_frank_wolfe.m -> acdc/frank_wolfe.py
42
+ evaluate_swaps.m, make_swaps.m,
43
+ greedy_search.m -> acdc/swaps.py
44
+ main_acdc.m, main_continuous.m,
45
+ main_discrete.m -> acdc/core.py
46
+
47
+ * Node indices are 0-based throughout, following Python convention, rather
48
+ than 1-based as in MATLAB.
49
+
50
+ * The linear-assignment subproblem, solved in the original by the undocumented
51
+ internal routine `matlab.internal.graph.perfectMatching`, is solved here by
52
+ SciPy: `scipy.optimize.linear_sum_assignment` (`solver='dense'`, the default
53
+ and the faithful analogue) or
54
+ `scipy.sparse.csgraph.min_weight_full_bipartite_matching`
55
+ (`solver='sparse'`, faster at scale but restricted to stored edges).
56
+
57
+ * The warm-start preconditioner that the original applies before its
58
+ assignment solver is omitted. Its transform adds only per-row and
59
+ per-column constants and so does not change the optimal matching; it existed
60
+ to speed up MATLAB's sparse solver.
61
+
62
+ * The FlyWire-specific I/O routines (`read_connectome.m`, `read_solution.m`,
63
+ `save_solution.m`) and the figure-generation scripts were not translated;
64
+ this package covers the core algorithm only.
65
+
66
+ * A test suite (`tests/`) and a scaling benchmark (`benchmark.py`) were added.
67
+ These are original to this package and have no counterpart in the MATLAB
68
+ implementation.
69
+
70
+ Further notes on fidelity to the original are given in the README.
71
+
72
+
73
+ Connectome data
74
+ ---------------
75
+
76
+ This package ships no data. The connectome CSV files distributed alongside the
77
+ original MATLAB code were obtained from the VNC Matching Challenge website,
78
+
79
+ https://codex.flywire.ai/app/vnc_matching_challenge
80
+
81
+ and the supplementary material asks that the contest organisers be acknowledged
82
+ for any further use of that data. This applies to anyone using those files with
83
+ this package; it is not a condition of the software licence.
@@ -0,0 +1,332 @@
1
+ Metadata-Version: 2.4
2
+ Name: acdc-search
3
+ Version: 0.1.0
4
+ Summary: AC/DC search - alternating continuous and discrete combinatorial graph matching
5
+ Project-URL: Homepage, https://github.com/flyconnectome/acdc_py
6
+ Project-URL: Source, https://github.com/flyconnectome/acdc_py
7
+ Project-URL: Issues, https://github.com/flyconnectome/acdc_py/issues
8
+ Author-email: Philipp Schlegel <observing@web.de>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Keywords: connectomics,frank-wolfe,graph-matching,quadratic-assignment
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: numpy>=1.23.2
22
+ Requires-Dist: scipy>=1.9.2
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest>=7.0; extra == 'test'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # AC/DC search (Python)
28
+
29
+ A Python translation of the MATLAB reference implementation of **AC/DC search** —
30
+ the **A**lternating **C**ontinuous and **D**iscrete **C**ombinatorial optimization
31
+ behind the winning solution to the FlyWire Ventral Nerve Cord Matching
32
+ Challenge.
33
+
34
+ > D. D. Lee, A. Matsliah & L. K. Saul, *"AC/DC search: behind the winning
35
+ > solution to the FlyWire graph-matching challenge"*, Transactions on Machine
36
+ > Learning Research (01/2026).
37
+ > [OpenReview](https://openreview.net/forum?id=8MjCOMyaDf)
38
+
39
+ ## What it does
40
+
41
+ Given two weighted directed graphs `A` and `B` (same number of nodes), AC/DC
42
+ searches for the permutation `P` (a one-to-one node correspondence) that
43
+ maximizes the **overlapping edge weight**
44
+
45
+ ```
46
+ J(P) = sum_ij min( A_ij , B_{p(i), p(j)} )
47
+ ```
48
+
49
+ where `p(i)` is the node of `B` matched to node `i` of `A`. It alternates:
50
+
51
+ - a **continuous** phase — Frank–Wolfe optimization over the doubly stochastic
52
+ relaxation (Birkhoff polytope), with an exact closed-form line search and a
53
+ linear-assignment subproblem at each step; and
54
+ - a **discrete** phase — greedy pairwise-swap search over permutations, applying
55
+ the exact-gain swaps that improve the score.
56
+
57
+ Each phase warm-starts the other.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install acdc-search
63
+ ```
64
+
65
+ The distribution is named `acdc-search` (the names `acdc` and `acdc-py` are
66
+ taken on PyPI by unrelated projects); the import name is `acdc`. It is pure
67
+ Python and needs only NumPy and SciPy.
68
+
69
+ For a development install from a checkout:
70
+
71
+ ```bash
72
+ pip install -e ".[test]" # numpy, scipy + pytest
73
+ ```
74
+
75
+ ## Usage
76
+
77
+ ```python
78
+ import numpy as np
79
+ import scipy.sparse as sp
80
+ from acdc import acdc_match, score, matrix_to_perm
81
+
82
+ # --- build two graphs A, B as scipy.sparse n x n matrices ---
83
+ rng = np.random.default_rng(0)
84
+ A = sp.random(200, 200, density=0.05, random_state=0).tocsr()
85
+
86
+ # (toy example) B is A relabeled by a hidden permutation we will recover
87
+ perm = rng.permutation(200)
88
+ P = sp.csc_matrix((np.ones(200), (np.arange(200), perm)), shape=(200, 200))
89
+ B = (P.T @ A @ P).tocsr()
90
+
91
+ # --- run the full AC/DC algorithm ---
92
+ M = acdc_match(A, B, max_iter=5, num_frank_wolfe=10)
93
+
94
+ print("score:", score(M, A, B), " (optimum:", A.sum(), ")")
95
+ matching = matrix_to_perm(M) # 0-based: node i of A -> matching[i] of B
96
+ ```
97
+
98
+ ### High-level functions
99
+
100
+ | Function | MATLAB equivalent | Description |
101
+ |---|---|---|
102
+ | `acdc_match(A, B, P0=None, max_iter=5, num_frank_wolfe=10, max_swap=inf, solver='dense')` | `main_acdc.m` | Full alternating algorithm. |
103
+ | `frank_wolfe_search(A, B, P0=None, num_updates=40, solver='dense')` | `main_continuous.m` | Continuous (Frank–Wolfe) phase only. |
104
+ | `greedy_match(A, B, P0=None, max_swap=inf)` | `main_discrete.m` | Discrete (greedy-swap) phase only. |
105
+
106
+ All three return a sparse permutation matrix.
107
+
108
+ **Progress output.** The phases emit their MATLAB-style tables through the
109
+ `acdc` logger at `INFO`. `verbose=True` (the default) attaches a stdout handler
110
+ for the duration of the call, so they just appear; `verbose=False` leaves your
111
+ logging configuration alone, which means an application that has configured
112
+ `logging` at `INFO` still receives them and one that has not stays silent:
113
+
114
+ ```python
115
+ import logging
116
+ logging.basicConfig(level=logging.INFO) # or logging.getLogger("acdc")
117
+ M = acdc_match(A, B, verbose=False) # tables go to your handlers
118
+ ```
119
+
120
+ **Inputs.** `A` and `B` may be a SciPy sparse matrix, a dense `numpy.ndarray`,
121
+ or an edge-list tuple `(rows, cols, weights)` / `(rows, cols, weights, n)` with
122
+ **0-based** node indices. `P0` may be a permutation vector, a sparse permutation
123
+ matrix, or `None` (identity start).
124
+
125
+ ### Public API
126
+
127
+ `acdc` exports exactly six names: the three entry points above, plus `score`
128
+ to evaluate a matching and `perm_to_matrix` / `matrix_to_perm` to convert
129
+ between a permutation matrix and a 0-based permutation vector.
130
+
131
+ The individual algorithm phases live in the submodules that mirror the MATLAB
132
+ files — `acdc.objective` (`compute_gradient`), `acdc.frank_wolfe`
133
+ (`do_frank_wolfe`), `acdc.swaps` (`evaluate_swaps`, `make_swaps`,
134
+ `greedy_search`), `acdc.matching` (`permutation_match`, `as_graph`, …) — and
135
+ can be imported from there. They are **internal**: their signatures follow the
136
+ call graph rather than any user-facing contract and may change without a major
137
+ version bump.
138
+
139
+ ## File map (MATLAB → Python)
140
+
141
+ | MATLAB (`../src/`) | Python (`acdc/`) |
142
+ |---|---|
143
+ | `compute_gradient.m` + score expression | `objective.py` |
144
+ | `permutation_match.m` | `matching.py` |
145
+ | `do_frank_wolfe.m` | `frank_wolfe.py` |
146
+ | `evaluate_swaps.m`, `make_swaps.m`, `greedy_search.m` | `swaps.py` |
147
+ | `main_acdc.m`, `main_continuous.m`, `main_discrete.m` | `core.py` |
148
+
149
+ ## Notes on fidelity & performance
150
+
151
+ - **`solver='dense'` (default)** uses SciPy's exact Hungarian/LAPJV solver
152
+ (`linear_sum_assignment`) — the faithful analogue of the MATLAB
153
+ `perfectMatching`. **`solver='sparse'`** uses
154
+ `min_weight_full_bipartite_matching`, which is much faster and lighter at
155
+ scale but only considers stored edges (a perfect matching must exist on the
156
+ sparsity pattern).
157
+ - The MATLAB warm-start preconditioner for the assignment solver is omitted on
158
+ purpose: its transform only adds per-row/column constants and does not change
159
+ the optimal matching — it existed solely to speed MATLAB's sparse solver.
160
+ - Like the MATLAB original, the gradient and swap-gain matrices are dense
161
+ `n × n`, so memory is `O(n²)`. At challenge scale (`n = 18524`, see below)
162
+ that needs ~16 GB+ of RAM and is best run with `solver='dense'` on a machine
163
+ with enough memory; small and medium graphs run comfortably anywhere.
164
+ - The quadratic part of the swap-gain matrix has two implementations. Six of
165
+ its eight terms are `min(x, y)` with one argument taken from `A` or `P B P'`,
166
+ so for **nonnegative** edge weights they vanish off those supports and can be
167
+ scattered onto the union of the supports instead of swept densely. On sparse
168
+ graphs that holds 2 dense `n × n` arrays instead of 5 (≈5.5 GB rather than
169
+ ≈13.7 GB at challenge scale). Scattered writes are slower per element, so dense
170
+ or negatively-weighted graphs use the literal dense expression; the two paths
171
+ agree bit for bit and the choice is automatic.
172
+ - The greedy phase recomputes the full score after each trial swap (faithful to
173
+ the MATLAB original); incremental scoring is a possible future optimization.
174
+ - Both loops stop early at a fixed point: a Frank–Wolfe update that leaves the
175
+ iterate unchanged, or an AC/DC alternation that leaves the matching
176
+ unchanged, makes every remaining iteration a deterministic replay. `max_iter`
177
+ and `num_frank_wolfe` are therefore upper bounds, not exact counts. The
178
+ result is unchanged — only the wasted work is skipped.
179
+
180
+ ## TODO
181
+
182
+ ### Pinned (fixed) matches
183
+
184
+ Neither the MATLAB original nor this translation supports **pinning** a subset of
185
+ matches so they are never swapped out. Every pair is free to move at every stage:
186
+ the discrete phase picks its swap from a `max` over the whole gain matrix
187
+ (`make_swaps.m` → `swaps.py`), and each Frank–Wolfe step solves an
188
+ *unconstrained* linear assignment problem over the full gradient
189
+ (`permutation_match.m` → `matching.py`) — the `P0` argument there is only a
190
+ warm-start accelerator and does not restrict the solution. This would be useful
191
+ whenever part of the correspondence is known a priori, e.g. neurons matched with
192
+ high confidence by cell type.
193
+
194
+ Adding it means two hooks, and would surface as a `pinned=` argument on the three
195
+ entry points:
196
+
197
+ - **Discrete phase** — mask the swap-gain matrix `D`: zero the row *and* column
198
+ of each pinned index where `D` is assembled in `evaluate_swaps`, so that `dMax`
199
+ also respects the pins and the `while dMax > 0` loop still terminates.
200
+ - **Continuous phase** — constrain the assignment problem. Prefer *reducing* it
201
+ (drop the pinned rows/columns, solve on the free submatrix, reinsert the pinned
202
+ pairs) over penalising forbidden entries with a large negative cost: the
203
+ reduced LAP is strictly smaller and avoids `-inf` handling in
204
+ `linear_sum_assignment` entirely. Only the two `permutation_match` calls need
205
+ constraining — the interpolation `P + step*(Q - P)` with `step` in `(0, 1]`
206
+ preserves any entry that is `1` in both `P` and `Q`, so pinned entries carry
207
+ through the continuous iterate on their own.
208
+
209
+ ## Benchmark
210
+
211
+ [`benchmark.py`](benchmark.py) runs AC/DC on synthetic *planted* instances of
212
+ increasing size to give a feel for scaling. Each instance relabels a random
213
+ graph by a hidden permutation, corrupts that alignment to make a warm start
214
+ (40 % of nodes scrambled by default), then measures runtime and how much of the
215
+ planted optimum is recovered.
216
+
217
+ ```bash
218
+ python benchmark.py # default sweep, ~1 min
219
+ python benchmark.py --sizes 100 200 400 800 # quick
220
+ python benchmark.py --sizes 1000 2000 --density 0.02 --solver sparse
221
+ ```
222
+
223
+ Example run (laptop, `solver='dense'`, `max_iter=3`, `num_frank_wolfe=10`):
224
+
225
+ ```
226
+ n edges mem grad eval acdc cont disc start% final% nodes%
227
+ -------------------------------------------------------------------------------------------------
228
+ 100 481 78.1KB 0.001 0.002 0.014 0.010 0.004 38.9 100.0 100.0
229
+ 200 1935 312.5KB 0.003 0.003 0.021 0.013 0.008 36.0 100.0 100.0
230
+ 400 7771 1.2MB 0.007 0.009 0.060 0.038 0.022 39.3 100.0 100.0
231
+ 800 31147 4.9MB 0.028 0.035 0.219 0.139 0.080 38.1 100.0 100.0
232
+ 2000 194913 30.5MB 0.522 0.572 3.483 2.255 1.227 38.2 100.0 100.0
233
+ 5000 1219064 190.7MB 8.149 8.837 50.284 32.975 17.308 38.2 100.0 100.0
234
+ - - - - - - - - - - - - - - - - - - - - - - - -
235
+ extrapolated from the n=800..5000 rows (acdc ~ n^2.97, R2=1.000 in log-log) -- NOT measured
236
+ ~10000 4.88e+06 762.9MB 71.6 72.2 399 266 134 - - -
237
+ ~50000 1.22e+08 18.6GB 1.04e+04 9.29e+03 4.73e+04 3.25e+04 1.52e+04 - - -
238
+ ~100000 4.88e+08 74.5GB 8.89e+04 7.52e+04 3.7e+05 2.57e+05 1.16e+05 - - -
239
+ ```
240
+
241
+ | extrapolated `n` | full `acdc_match` | one dense `n × n` array |
242
+ |---|---|---|
243
+ | 10 000 | ~6.7 min | 762.9 MB |
244
+ | 50 000 | ~13 h | 18.6 GB |
245
+ | 100 000 | ~4.3 d | 74.5 GB |
246
+
247
+ - `grad`/`eval` are per-call times (in seconds) for `compute_gradient` / `evaluate_swaps`
248
+ (best of as many repeats as fit in a short budget, so small sizes are not
249
+ dominated by noise). `acdc` is the full run, split into the continuous
250
+ (`cont`) and discrete (`disc`) phases — roughly a 2:1 split throughout.
251
+ `mem` is the size of one dense `n × n` float64 matrix; the `n = 5000` row
252
+ peaks at about 1.1 GB resident.
253
+ - **Rows prefixed `~` are extrapolations, not measurements.** Each timing column
254
+ is fitted as a power law over the asymptotic tail of the measured rows
255
+ (`n ≥ 800`) and evaluated at the larger size. Fitting the *small* rows instead
256
+ gives an exponent near 1.3 and under-predicts `n = 5000` by ~25×, because
257
+ those sizes are dominated by fixed overhead — the benchmark warns when the
258
+ fitted exponent lands below 2.5. Held out against a real measurement, a fit
259
+ over `n = 400..2000` predicts 33 s at `n = 5000` against 42 s measured.
260
+ - **Scaling is `O(n³)` at fixed density**, not `O(n²)`: the gradient loops over
261
+ the `n` matched nodes and does work quadratic in the degree, which at fixed
262
+ density is itself proportional to `n`. The measured exponent is 2.97 with
263
+ `R² = 1.000`. At fixed *average degree* — the realistic regime for a
264
+ connectome — the same argument gives `O(n²)`, dominated by the dense
265
+ matrices. Which of the two applies to your graph is the single biggest factor
266
+ in what it will cost.
267
+ - The extrapolated rows hold density fixed at 0.05, so they are an **upper
268
+ bound** for a graph of that size rather than a prediction for a real one. The
269
+ challenge connectomes are `n = 18524` at density 0.012 (male) and 0.006
270
+ (female) — 4–8× sparser than this sweep. At these sizes memory binds well
271
+ before time does: 74.5 GB for a *single* array at `n = 100 000`, and the
272
+ greedy phase holds two to three of them.
273
+ - From a corrupted-but-informative warm start the planted alignment is
274
+ recovered essentially perfectly (`final% ≈ 100`). Quality from a *cold*
275
+ (identity) start is much lower — AC/DC is a local search and, like the paper,
276
+ relies on a reasonable warm start.
277
+
278
+ ## The challenge data
279
+
280
+ The challenge was to align the connectomes of the **ventral nerve cords (VNCs)
281
+ of a male and a female fruit fly** — not the FlyWire brain connectome, despite
282
+ the challenge carrying the FlyWire name. Per the paper, each VNC connectome is
283
+ a directed weighted graph with `n = 18524` nodes (neurons) and millions of
284
+ edges (synapse counts); the organizers also supplied a cell-type-derived
285
+ baseline match, scoring 5 154 247, which teams could use as a warm start.
286
+
287
+ The two graphs are that size in the data shipped here as well: the male edge
288
+ list spans node ids `m1..m18524` and the female `f1..f18524` (the female list
289
+ has 18 523 nodes with at least one edge — `f9574` is isolated, which is why
290
+ `_coerce_pair` pads to a common `n`). Their densities are 0.012 and 0.006
291
+ respectively, so both are far sparser than the benchmark's default 0.05.
292
+
293
+ ## CSV / challenge I/O
294
+
295
+ This package translates the **core algorithm only**. The challenge-specific CSV
296
+ readers/writers (`read_connectome.m`, `read_solution.m`, `save_solution.m`) and
297
+ the figure scripts are intentionally out of scope; build `A`, `B` as sparse
298
+ matrices (or pass edge lists) as shown above.
299
+
300
+ ## Tests
301
+
302
+ ```bash
303
+ pytest -q
304
+ ```
305
+
306
+ Covers: a finite-difference gradient check, the score identity, self-match
307
+ optimality, recovery of a known permutation, exactness of the swap-gain matrix,
308
+ dense/sparse backend agreement, and end-to-end smoke tests.
309
+
310
+ ## Attribution & licence
311
+
312
+ This package is a **translation** of the MATLAB reference implementation that
313
+ Lee, Matsliah & Saul published as supplementary material to their TMLR paper:
314
+
315
+ > D. D. Lee, A. Matsliah & L. K. Saul, *"AC/DC search: behind the winning
316
+ > solution to the FlyWire graph-matching challenge"*, Transactions on Machine
317
+ > Learning Research (01/2026).
318
+ > <https://openreview.net/forum?id=8MjCOMyaDf>
319
+
320
+ That original MATLAB code is **MIT-licensed**, Copyright (c) 2025 Daniel Lee and
321
+ Lawrence Saul. This Python translation is therefore also offered under the MIT
322
+ licence; [LICENSE](LICENSE) carries both notices, with the upstream licence
323
+ reproduced in full as it requires.
324
+
325
+ [NOTICE](NOTICE) summarises how the translation departs from the original —
326
+ 0-based indexing, SciPy in place of MATLAB's internal `perfectMatching`, the
327
+ omitted assignment preconditioner, the untranslated challenge I/O, and the added
328
+ tests and benchmark. The "Notes on fidelity & performance" section above covers
329
+ the same ground in more detail.
330
+
331
+ If you use this package, please cite the paper above — the algorithm is the
332
+ authors' work; this is only a translation of it.