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.
- acdc_search-0.1.0/.gitignore +229 -0
- acdc_search-0.1.0/LICENSE +53 -0
- acdc_search-0.1.0/NOTICE +83 -0
- acdc_search-0.1.0/PKG-INFO +332 -0
- acdc_search-0.1.0/README.md +306 -0
- acdc_search-0.1.0/acdc/__init__.py +56 -0
- acdc_search-0.1.0/acdc/core.py +199 -0
- acdc_search-0.1.0/acdc/frank_wolfe.py +144 -0
- acdc_search-0.1.0/acdc/matching.py +195 -0
- acdc_search-0.1.0/acdc/objective.py +129 -0
- acdc_search-0.1.0/acdc/swaps.py +304 -0
- acdc_search-0.1.0/pyproject.toml +55 -0
- acdc_search-0.1.0/requirements.txt +4 -0
- acdc_search-0.1.0/tests/test_acdc.py +323 -0
|
@@ -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.
|
acdc_search-0.1.0/NOTICE
ADDED
|
@@ -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.
|