sievemin 0.1.1__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,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-10-31)
4
+
5
+ - First release on PyPI: `pip install sievemin`. Packaging metadata, a `music21` extra, and a publish workflow. No change to the solver.
6
+
7
+ ## 0.1.0 (2026-10-30)
8
+
9
+ - First release: exact minimisation of Xenakis sieves (`minimize_union`, `minimize_dnf`, `minimize_general`, `minimize_union_tolerant`), CLI, benchmarks and report.
@@ -0,0 +1,21 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: "sievemin: exact minimisation of Xenakis sieves"
4
+ type: software
5
+ authors:
6
+ - family-names: Bentley
7
+ given-names: Duncan
8
+ orcid: "https://orcid.org/0009-0006-9443-5129"
9
+ version: 0.1.1
10
+ date-released: 2026-10-31
11
+ doi: 10.5281/zenodo.23816540
12
+ license: BSD-3-Clause
13
+ repository-code: "https://github.com/DuncanBentley/sievemin"
14
+ url: "https://github.com/DuncanBentley/sievemin"
15
+ keywords:
16
+ - Xenakis
17
+ - sieve theory
18
+ - music theory
19
+ - set cover
20
+ - integer linear programming
21
+ - music21
sievemin-0.1.1/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Duncan Bentley
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+ 3. Neither the name of the copyright holder nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md CITATION.cff REPORT.md
2
+ recursive-include tests *.py
3
+ exclude music21-issue-draft.md music21-feature-proposal.md *.patch
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: sievemin
3
+ Version: 0.1.1
4
+ Summary: Exact (provably minimal) Xenakis sieve formulas for integer sets
5
+ Author: Duncan Bentley
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/DuncanBentley/sievemin
8
+ Project-URL: Repository, https://github.com/DuncanBentley/sievemin
9
+ Project-URL: Issues, https://github.com/DuncanBentley/sievemin/issues
10
+ Project-URL: Changelog, https://github.com/DuncanBentley/sievemin/blob/main/CHANGELOG.md
11
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.23816540
12
+ Keywords: Xenakis,sieve theory,music theory,set cover,integer linear programming,music21,computational musicology
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Artistic Software
25
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
26
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: numpy
31
+ Requires-Dist: scipy>=1.9
32
+ Provides-Extra: music21
33
+ Requires-Dist: music21; extra == "music21"
34
+ Provides-Extra: bench
35
+ Requires-Dist: music21; extra == "bench"
36
+ Provides-Extra: test
37
+ Requires-Dist: pytest; extra == "test"
38
+ Requires-Dist: music21; extra == "test"
39
+ Dynamic: license-file
40
+
41
+ # sievemin
42
+
43
+ `sievemin` finds the shortest Xenakis sieve that produces a given set of integers, and proves nothing shorter exists in the formula shape you asked for. Against music21's greedy compressor it is shorter on 928 of 1,000 random segments, and it shortens several published Xenakis sieves.
44
+
45
+ [![CI](https://github.com/DuncanBentley/sievemin/actions/workflows/ci.yml/badge.svg)](https://github.com/DuncanBentley/sievemin/actions) [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/DuncanBentley/sievemin/blob/main/LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/DuncanBentley/sievemin/blob/main/pyproject.toml) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23816540.svg)](https://doi.org/10.5281/zenodo.23816540)
46
+
47
+ A sieve is a formula built from residue classes `m@r` (every n with n mod m = r), joined with union `|`, intersection `&` and complement `-`. The output uses music21's sieve syntax, so you can paste it into `music21.sieve.Sieve`.
48
+
49
+ I make metal and produce records. Sieves are a tidy way to write down a scale or a rhythm that doesn't repeat every octave or every bar. I wanted to know whether the formulas people publish, and the ones music21 generates, are actually the short ones. Mostly they're close. Sometimes they aren't.
50
+
51
+ ## Key findings
52
+
53
+ | finding | numbers | where |
54
+ |---|---|---|
55
+ | music21's greedy is already optimal on its own small examples | 12 of 12 music21 test segments match the exact class count | [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) section 3 |
56
+ | It loses on bigger sets (z = 0..99, music21's modulus limit) | Psappha 20 classes down to 15, Nomos Alpha 8 to 7, À R. 17 to 14, Nekuïa 11 to 8 | [bench/results.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/results.md), Table 1 |
57
+ | It loses on random segments of 5 to 100 points | 928 of 1,000 shorter, 22.4% fewer classes on average, max 40.5%, never longer | [bench/random_segments.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_segments.md) |
58
+ | Smaller period | median period 70 against 21,162,960 for music21, at the price of 34.5 classes on average | [bench/random_segments.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_segments.md) |
59
+ | Published Xenakis formulas are not all minimal, even with the composer's own moduli | Nomos Alpha size 23 to 21, Psappha (Flint) 59 to 36, À R. 99 to 88, Jonchaies already minimal | [bench/results.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/results.md), Table 2 |
60
+ | Complement makes formulas shorter | the major scale goes from 18 to 10 (any modulus); on 251 small random segments, `&` and `-` beat the best union on 180 | [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) section 3 |
61
+ | The gain is smaller on structured material | 100 random Xenakis-style sieves: 37 shorter, 11.4% on average, median 0% | [bench/random_study.csv](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_study.csv) |
62
+ | Error-tolerant mode (formula plus k wrong notes) | recovers a planted sieve with 1, 2, 3 flipped notes in 100%, 96%, 95% of 150 trials each (cost 1) | [bench/tolerant_study.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/tolerant_study.md) |
63
+
64
+ Random point sets are where the greedy does worst. Real material is more structured, and there the gain is smaller.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install sievemin
70
+ ```
71
+
72
+ or with uv:
73
+
74
+ ```bash
75
+ uv add sievemin
76
+ ```
77
+
78
+ `pip install "sievemin[music21]"` also installs music21, so you can check the output with `music21.sieve.Sieve`. sievemin itself only needs numpy and scipy.
79
+
80
+ ## Quick start
81
+
82
+ ```bash
83
+ sievemin 0 2 4 5 7 9 11 --period 12
84
+ # (-(12@1|12@3|12@6|12@8|12@10))
85
+ # size 10 (leaves 5, operators 5); optimal: True; ...
86
+ ```
87
+
88
+ That takes a few seconds, mostly Python start-up. The solve itself is tens of milliseconds.
89
+
90
+ ```python
91
+ from sievemin import Domain, parse, evaluate, minimize_dnf, to_music21
92
+
93
+ nomos = '(-(13@3|13@5|13@7|13@9)&11@2)|(-(11@4|11@8)&13@9)|(13@0|13@1|13@6)'
94
+ dom = Domain.periodic(143)
95
+ r = minimize_dnf(evaluate(parse(nomos), dom), dom, max_modulus=13, max_neg=4)
96
+ r.text, r.size, r.optimal
97
+ # ('11@2&-(13@3|13@5|13@7)|13@0|13@1|13@6|13@9&-(11@4|11@8)', 21, True)
98
+ to_music21(r.formula) # bracketed, see the music21 note below
99
+ ```
100
+
101
+ `Domain.window(z)` matches exactly on the points of `z`, which is how music21 works. Pass `max_modulus=len(z) - 1` to stay inside the moduli music21 itself would try. `Domain.periodic(P)` matches on every integer.
102
+
103
+ For unions you can choose what "shortest" means: `minimize_union(..., objective='leaves' | 'chars' | 'period')`.
104
+
105
+ `minimize_union_tolerant(points, dom, max_edits=2, edit_cost=1)` returns a formula plus the notes to add and the notes to remove.
106
+
107
+ One music21 note: music21 10.5.2 crashes on an unbracketed intersection with a complemented class, for example `Sieve('3@1&-4@2')`. `to_music21()` puts brackets around the formula to avoid it.
108
+
109
+ ## Reproduce everything
110
+
111
+ ```bash
112
+ git clone https://github.com/DuncanBentley/sievemin
113
+ cd sievemin
114
+ pip install -e ".[test]" # adds pytest and music21
115
+ python -m pytest # 127 tests
116
+ python bench/reproduce.py # benchmark and tolerant study, a few minutes
117
+ python bench/reproduce.py --all # adds the 1,000 random segments, 1 to 2 hours
118
+ ```
119
+
120
+ `bench/reproduce.py` writes `bench/results.md` and `results.csv`, plus `bench/tolerant_study.md` and `tolerant_study.csv`. With `--all` it also writes `bench/random_segments.md` and `random_segments.csv`. The 100 random Xenakis-style sieves are in `bench/random_study.csv`. Seeds are fixed (2026), so the optimal values reproduce exactly. Runtimes depend on the machine. Mine is a Ryzen 9 8940HX laptop, where the 1,000-segment run took 1 hour 54 minutes.
121
+
122
+ ## What it guarantees
123
+
124
+ | function | what it searches | what is proven |
125
+ |---|---|---|
126
+ | `minimize_union` | unions of residue classes, the form music21's `CompressionSegment` outputs | the fewest classes, the shortest string, or the smallest period, by exact ILP over every candidate class |
127
+ | `minimize_dnf` | unions of terms `P & -(N1\|...\|Nk)`, the shape of Xenakis's published sieves | the smallest formula of that shape, for a given cap on k and, optionally, on the moduli |
128
+ | `minimize_general` | formulas of any shape | the smallest formula there is, by exhaustive search, for domains of up to 14 points |
129
+ | `minimize_union_tolerant` | a union of classes plus up to k notes added or removed | the smallest formula size + λ × exceptions |
130
+
131
+ Size means leaves plus operators, so a union of n classes has size 2n - 1. Every result says whether the solver proved it optimal. I also ran every published result in this repo back through music21's own `Sieve` and checked it gives the same set.
132
+
133
+ ## Method
134
+
135
+ Unions are an exact set cover, solved as an ILP with HiGHS through scipy. The candidate list is every residue class that stays inside the target, so the minimum is global. Terms with complement are enumerated per positive class, then another set cover picks the cheapest union. The any-shape search is dynamic programming over all subsets of up to 14 points. [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) has the details, the benchmarks and the prior-work notes.
136
+
137
+ ## Limits / what this does not show
138
+
139
+ `minimize_dnf` is only optimal within its own shape. A formula nested differently can be shorter. With moduli up to 4, the major scale has a size 16 formula, and the best union of terms is 17. Only `minimize_general` covers every shape, and it runs out of room past 14 points.
140
+
141
+ A result on a window only holds on that window, as with music21. If the formula has to hold on every integer, use `Domain.periodic(P)`. A window formula can use moduli no composer would pick, like `41@24`.
142
+
143
+ Exact set cover is NP-hard. Most sets solve in milliseconds, but some 100-point windows take tens of seconds, and the slowest random segment took 248 s.
144
+
145
+ The error-tolerant mode only works with unions, and its answer depends on the exception cost λ, which you choose. That it finds structure in Xenakis's sets is not evidence that he edited notes by hand.
146
+
147
+ On music21's own 12 test segments the greedy is already optimal. The 1,000 random segments have no structure, so 22.4% is not what to expect on a real scale or rhythm.
148
+
149
+ I haven't read *Formalized Music* or Xenakis 1990 directly. The published formulas come from music21's tests and the hmt library, and the value lists I generate match hmt's for Nomos Alpha, Psappha and À R.
150
+
151
+ Whether a shorter formula is better music is not something this measures.
152
+
153
+ ## Upstream and sources
154
+
155
+ - music21: <https://github.com/cuthbertLab/music21>. I studied version 10.5.2 ([release](https://github.com/cuthbertLab/music21/releases/tag/v10.5.2)), specifically [`music21/sieve.py`](https://github.com/cuthbertLab/music21/blob/v10.5.2/music21/sieve.py). Docs: <https://www.music21.org/music21docs/moduleReference/moduleSieve.html>.
156
+ - hmt (Rohan Drape, Haskell), a second source for the published sieves: <https://github.com/rd--/hmt>.
157
+ - Ariza, C. 2005. "The Xenakis Sieve as Object: A New Model and a Complete Implementation." *Computer Music Journal* 29(2). DOI 10.1162/0148926054094396. This is the model behind music21's sieve.
158
+ - Xenakis, I. 1990. "Sieves." *Perspectives of New Music* 28(1): <https://doi.org/10.2307/833343>.
159
+ - Bliznets, Nederlof and Szilágyi, "Parameterized Algorithms for Covering by Arithmetic Progressions": <https://arxiv.org/abs/2312.06393>.
160
+
161
+ No external data is downloaded. The published sieve definitions are re-typed from music21's tests and hmt, and the random sets come from fixed seeds.
162
+
163
+ ## Status of upstream reports
164
+
165
+ | Report | Status |
166
+ |---|---|
167
+ | music21: crash on `Sieve('3@1&-4@2')` (unbracketed intersection with a complemented class), one-line fix plus test | issue filed: [cuthbertLab/music21#2131](https://github.com/cuthbertLab/music21/issues/2131), fixed and merged in [#2135](https://github.com/cuthbertLab/music21/pull/2135) |
168
+ | music21: opt-in exact compression, `CompressionSegment(..., method='exact')` | to be filed |
169
+
170
+ ## Citation
171
+
172
+ See [CITATION.cff](https://github.com/DuncanBentley/sievemin/blob/main/CITATION.cff).
173
+
174
+ ```bibtex
175
+ @software{bentley2026_sievemin,
176
+ author = {Bentley, Duncan},
177
+ title = {sievemin: exact minimisation of Xenakis sieves},
178
+ year = {2026},
179
+ url = {https://github.com/DuncanBentley/sievemin},
180
+ doi = {10.5281/zenodo.23816540}
181
+ }
182
+ ```
183
+
184
+ ## License
185
+
186
+ BSD-3-Clause, the same as music21. See [LICENSE](https://github.com/DuncanBentley/sievemin/blob/main/LICENSE). The repo contains no third-party data.
187
+
188
+ ## Acknowledgements
189
+
190
+ Thanks to Michael Scott Cuthbert and the music21 contributors, whose `sieve.py` I tested against, to Christopher Ariza for the sieve model it implements, and to Rohan Drape for hmt.
191
+
@@ -0,0 +1,151 @@
1
+ # sievemin
2
+
3
+ `sievemin` finds the shortest Xenakis sieve that produces a given set of integers, and proves nothing shorter exists in the formula shape you asked for. Against music21's greedy compressor it is shorter on 928 of 1,000 random segments, and it shortens several published Xenakis sieves.
4
+
5
+ [![CI](https://github.com/DuncanBentley/sievemin/actions/workflows/ci.yml/badge.svg)](https://github.com/DuncanBentley/sievemin/actions) [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](https://github.com/DuncanBentley/sievemin/blob/main/LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/DuncanBentley/sievemin/blob/main/pyproject.toml) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23816540.svg)](https://doi.org/10.5281/zenodo.23816540)
6
+
7
+ A sieve is a formula built from residue classes `m@r` (every n with n mod m = r), joined with union `|`, intersection `&` and complement `-`. The output uses music21's sieve syntax, so you can paste it into `music21.sieve.Sieve`.
8
+
9
+ I make metal and produce records. Sieves are a tidy way to write down a scale or a rhythm that doesn't repeat every octave or every bar. I wanted to know whether the formulas people publish, and the ones music21 generates, are actually the short ones. Mostly they're close. Sometimes they aren't.
10
+
11
+ ## Key findings
12
+
13
+ | finding | numbers | where |
14
+ |---|---|---|
15
+ | music21's greedy is already optimal on its own small examples | 12 of 12 music21 test segments match the exact class count | [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) section 3 |
16
+ | It loses on bigger sets (z = 0..99, music21's modulus limit) | Psappha 20 classes down to 15, Nomos Alpha 8 to 7, À R. 17 to 14, Nekuïa 11 to 8 | [bench/results.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/results.md), Table 1 |
17
+ | It loses on random segments of 5 to 100 points | 928 of 1,000 shorter, 22.4% fewer classes on average, max 40.5%, never longer | [bench/random_segments.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_segments.md) |
18
+ | Smaller period | median period 70 against 21,162,960 for music21, at the price of 34.5 classes on average | [bench/random_segments.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_segments.md) |
19
+ | Published Xenakis formulas are not all minimal, even with the composer's own moduli | Nomos Alpha size 23 to 21, Psappha (Flint) 59 to 36, À R. 99 to 88, Jonchaies already minimal | [bench/results.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/results.md), Table 2 |
20
+ | Complement makes formulas shorter | the major scale goes from 18 to 10 (any modulus); on 251 small random segments, `&` and `-` beat the best union on 180 | [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) section 3 |
21
+ | The gain is smaller on structured material | 100 random Xenakis-style sieves: 37 shorter, 11.4% on average, median 0% | [bench/random_study.csv](https://github.com/DuncanBentley/sievemin/blob/main/bench/random_study.csv) |
22
+ | Error-tolerant mode (formula plus k wrong notes) | recovers a planted sieve with 1, 2, 3 flipped notes in 100%, 96%, 95% of 150 trials each (cost 1) | [bench/tolerant_study.md](https://github.com/DuncanBentley/sievemin/blob/main/bench/tolerant_study.md) |
23
+
24
+ Random point sets are where the greedy does worst. Real material is more structured, and there the gain is smaller.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install sievemin
30
+ ```
31
+
32
+ or with uv:
33
+
34
+ ```bash
35
+ uv add sievemin
36
+ ```
37
+
38
+ `pip install "sievemin[music21]"` also installs music21, so you can check the output with `music21.sieve.Sieve`. sievemin itself only needs numpy and scipy.
39
+
40
+ ## Quick start
41
+
42
+ ```bash
43
+ sievemin 0 2 4 5 7 9 11 --period 12
44
+ # (-(12@1|12@3|12@6|12@8|12@10))
45
+ # size 10 (leaves 5, operators 5); optimal: True; ...
46
+ ```
47
+
48
+ That takes a few seconds, mostly Python start-up. The solve itself is tens of milliseconds.
49
+
50
+ ```python
51
+ from sievemin import Domain, parse, evaluate, minimize_dnf, to_music21
52
+
53
+ nomos = '(-(13@3|13@5|13@7|13@9)&11@2)|(-(11@4|11@8)&13@9)|(13@0|13@1|13@6)'
54
+ dom = Domain.periodic(143)
55
+ r = minimize_dnf(evaluate(parse(nomos), dom), dom, max_modulus=13, max_neg=4)
56
+ r.text, r.size, r.optimal
57
+ # ('11@2&-(13@3|13@5|13@7)|13@0|13@1|13@6|13@9&-(11@4|11@8)', 21, True)
58
+ to_music21(r.formula) # bracketed, see the music21 note below
59
+ ```
60
+
61
+ `Domain.window(z)` matches exactly on the points of `z`, which is how music21 works. Pass `max_modulus=len(z) - 1` to stay inside the moduli music21 itself would try. `Domain.periodic(P)` matches on every integer.
62
+
63
+ For unions you can choose what "shortest" means: `minimize_union(..., objective='leaves' | 'chars' | 'period')`.
64
+
65
+ `minimize_union_tolerant(points, dom, max_edits=2, edit_cost=1)` returns a formula plus the notes to add and the notes to remove.
66
+
67
+ One music21 note: music21 10.5.2 crashes on an unbracketed intersection with a complemented class, for example `Sieve('3@1&-4@2')`. `to_music21()` puts brackets around the formula to avoid it.
68
+
69
+ ## Reproduce everything
70
+
71
+ ```bash
72
+ git clone https://github.com/DuncanBentley/sievemin
73
+ cd sievemin
74
+ pip install -e ".[test]" # adds pytest and music21
75
+ python -m pytest # 127 tests
76
+ python bench/reproduce.py # benchmark and tolerant study, a few minutes
77
+ python bench/reproduce.py --all # adds the 1,000 random segments, 1 to 2 hours
78
+ ```
79
+
80
+ `bench/reproduce.py` writes `bench/results.md` and `results.csv`, plus `bench/tolerant_study.md` and `tolerant_study.csv`. With `--all` it also writes `bench/random_segments.md` and `random_segments.csv`. The 100 random Xenakis-style sieves are in `bench/random_study.csv`. Seeds are fixed (2026), so the optimal values reproduce exactly. Runtimes depend on the machine. Mine is a Ryzen 9 8940HX laptop, where the 1,000-segment run took 1 hour 54 minutes.
81
+
82
+ ## What it guarantees
83
+
84
+ | function | what it searches | what is proven |
85
+ |---|---|---|
86
+ | `minimize_union` | unions of residue classes, the form music21's `CompressionSegment` outputs | the fewest classes, the shortest string, or the smallest period, by exact ILP over every candidate class |
87
+ | `minimize_dnf` | unions of terms `P & -(N1\|...\|Nk)`, the shape of Xenakis's published sieves | the smallest formula of that shape, for a given cap on k and, optionally, on the moduli |
88
+ | `minimize_general` | formulas of any shape | the smallest formula there is, by exhaustive search, for domains of up to 14 points |
89
+ | `minimize_union_tolerant` | a union of classes plus up to k notes added or removed | the smallest formula size + λ × exceptions |
90
+
91
+ Size means leaves plus operators, so a union of n classes has size 2n - 1. Every result says whether the solver proved it optimal. I also ran every published result in this repo back through music21's own `Sieve` and checked it gives the same set.
92
+
93
+ ## Method
94
+
95
+ Unions are an exact set cover, solved as an ILP with HiGHS through scipy. The candidate list is every residue class that stays inside the target, so the minimum is global. Terms with complement are enumerated per positive class, then another set cover picks the cheapest union. The any-shape search is dynamic programming over all subsets of up to 14 points. [REPORT.md](https://github.com/DuncanBentley/sievemin/blob/main/REPORT.md) has the details, the benchmarks and the prior-work notes.
96
+
97
+ ## Limits / what this does not show
98
+
99
+ `minimize_dnf` is only optimal within its own shape. A formula nested differently can be shorter. With moduli up to 4, the major scale has a size 16 formula, and the best union of terms is 17. Only `minimize_general` covers every shape, and it runs out of room past 14 points.
100
+
101
+ A result on a window only holds on that window, as with music21. If the formula has to hold on every integer, use `Domain.periodic(P)`. A window formula can use moduli no composer would pick, like `41@24`.
102
+
103
+ Exact set cover is NP-hard. Most sets solve in milliseconds, but some 100-point windows take tens of seconds, and the slowest random segment took 248 s.
104
+
105
+ The error-tolerant mode only works with unions, and its answer depends on the exception cost λ, which you choose. That it finds structure in Xenakis's sets is not evidence that he edited notes by hand.
106
+
107
+ On music21's own 12 test segments the greedy is already optimal. The 1,000 random segments have no structure, so 22.4% is not what to expect on a real scale or rhythm.
108
+
109
+ I haven't read *Formalized Music* or Xenakis 1990 directly. The published formulas come from music21's tests and the hmt library, and the value lists I generate match hmt's for Nomos Alpha, Psappha and À R.
110
+
111
+ Whether a shorter formula is better music is not something this measures.
112
+
113
+ ## Upstream and sources
114
+
115
+ - music21: <https://github.com/cuthbertLab/music21>. I studied version 10.5.2 ([release](https://github.com/cuthbertLab/music21/releases/tag/v10.5.2)), specifically [`music21/sieve.py`](https://github.com/cuthbertLab/music21/blob/v10.5.2/music21/sieve.py). Docs: <https://www.music21.org/music21docs/moduleReference/moduleSieve.html>.
116
+ - hmt (Rohan Drape, Haskell), a second source for the published sieves: <https://github.com/rd--/hmt>.
117
+ - Ariza, C. 2005. "The Xenakis Sieve as Object: A New Model and a Complete Implementation." *Computer Music Journal* 29(2). DOI 10.1162/0148926054094396. This is the model behind music21's sieve.
118
+ - Xenakis, I. 1990. "Sieves." *Perspectives of New Music* 28(1): <https://doi.org/10.2307/833343>.
119
+ - Bliznets, Nederlof and Szilágyi, "Parameterized Algorithms for Covering by Arithmetic Progressions": <https://arxiv.org/abs/2312.06393>.
120
+
121
+ No external data is downloaded. The published sieve definitions are re-typed from music21's tests and hmt, and the random sets come from fixed seeds.
122
+
123
+ ## Status of upstream reports
124
+
125
+ | Report | Status |
126
+ |---|---|
127
+ | music21: crash on `Sieve('3@1&-4@2')` (unbracketed intersection with a complemented class), one-line fix plus test | issue filed: [cuthbertLab/music21#2131](https://github.com/cuthbertLab/music21/issues/2131), fixed and merged in [#2135](https://github.com/cuthbertLab/music21/pull/2135) |
128
+ | music21: opt-in exact compression, `CompressionSegment(..., method='exact')` | to be filed |
129
+
130
+ ## Citation
131
+
132
+ See [CITATION.cff](https://github.com/DuncanBentley/sievemin/blob/main/CITATION.cff).
133
+
134
+ ```bibtex
135
+ @software{bentley2026_sievemin,
136
+ author = {Bentley, Duncan},
137
+ title = {sievemin: exact minimisation of Xenakis sieves},
138
+ year = {2026},
139
+ url = {https://github.com/DuncanBentley/sievemin},
140
+ doi = {10.5281/zenodo.23816540}
141
+ }
142
+ ```
143
+
144
+ ## License
145
+
146
+ BSD-3-Clause, the same as music21. See [LICENSE](https://github.com/DuncanBentley/sievemin/blob/main/LICENSE). The repo contains no third-party data.
147
+
148
+ ## Acknowledgements
149
+
150
+ Thanks to Michael Scott Cuthbert and the music21 contributors, whose `sieve.py` I tested against, to Christopher Ariza for the sieve model it implements, and to Rohan Drape for hmt.
151
+
@@ -0,0 +1,223 @@
1
+ # Exact minimisation of Xenakis sieves
2
+
3
+ Duncan Bentley, October 2026. Code in `src/sievemin`, 127 tests in `tests/`, benchmarks in `bench/`.
4
+
5
+ ## Summary
6
+
7
+ music21's `CompressionSegment` turns a point set into a sieve with a greedy rule. It's fast and always correct, but nothing in it claims to find the shortest formula, and it only ever writes unions.
8
+
9
+ I wrote `sievemin` to find formulas that are provably the shortest. For unions of residue classes (music21's own output form) the result is exact, with no extra bounds. For Xenakis-style unions of `P & -(N1|..|Nk)` terms it is exact within a cap on k and the moduli. For formulas of any shape it is exact on up to 14 points. Every formula in this report went back through music21's own `Sieve` and gave the right set.
10
+
11
+ What I found:
12
+
13
+ On music21's own 12 test segments the greedy is already optimal. Adding complement makes 2 of the 12 shorter.
14
+
15
+ On bigger sets the greedy loses. With music21's own modulus limit on z = 0..99, Nomos Alpha goes from 8 classes to 7, Psappha from 20 to 15 and À R. from 17 to 14. Xenakis's own 20-class union for Nekuïa comes down to 8; music21 gets 11.
16
+
17
+ Several published formulas aren't minimal, even keeping the composer's moduli. Nomos Alpha goes from size 23 to 21, Psappha (Flint's analysis) from 59 to 36 and À R. from 99 to 88. The major scale from Formalized Music p.197 goes from 18 to 16 with moduli up to 4, and to 10 if 12 is allowed.
18
+
19
+ How much you gain depends on the material. On 100 random Xenakis-style sieves the size came down 11.4% on average against music21 (median 0%, max 63.6%). On 1,000 random segments of 5 to 100 points, which have no structure at all, the exact union used fewer classes on 928, 22.4% fewer on average. Structured material, which is what people actually compose with, gains less.
20
+
21
+ There's also an error-tolerant mode: a formula plus up to k notes added or removed, solved exactly. It recovered a planted sieve with 1, 2 or 3 flipped notes in 100%, 96% and 95% of trials at exception cost 1.
22
+
23
+ My verdict: a correct, small tool with an honest guarantee, and a reasonable thing to offer music21. It isn't a big research result. Covering a set with arithmetic progressions is a known NP-complete problem with known exact methods. What looks new is doing it for sieves with union, intersection and complement, and checking published formulas against the true minimum.
24
+
25
+ ## 1. What music21 does
26
+
27
+ Source: music21 10.5.2, `music21/sieve.py`, `CompressionSegment._process` and `_find` (Ariza, 2003 and 2010).
28
+
29
+ 1. It takes a sorted point list and a window `z`. By default `z` is `range(min, max+1)`, and every point of `z` that isn't in the list must stay out.
30
+ 2. It takes the first uncovered point `n` and tries `m = 1, 2, 3, ...` while `m < len(z)`, so the largest modulus it can use is `len(z) - 1`. It keeps the first class `m@(n mod m)` whose points on `z` all lie in the set. The code checks against both "the remaining points" and "the whole set", but the first is a subset of the second, so only the second check matters.
31
+ 3. It removes the covered points and repeats until nothing is left. The output is a sorted union of single classes.
32
+
33
+ So it's a "first point, smallest modulus" greedy, not even the usual best-coverage greedy for set cover. The docs make no claim about minimality, and the docstring says the result depends on `z`. It never writes intersections or complements. The other compression path, `_cmpIntersection`, only merges intersections it's given, using the Chinese remainder theorem. It tries every modulus from 1 up.
34
+
35
+ I found a crash on the way. `Sieve('3@1&-4@2')` raises `ResidualException: complemented Residual objects cannot be intersected`, while `Sieve('(3@1&-4@2)')` works. `Sieve._initCompression` calls `_cmpIntersection()` and only catches `TypeError`, but `Residual.__and__` raises `ResidualException`. So any unbracketed formula that intersects a complemented class crashes the constructor. `sievemin.to_music21()` adds brackets to avoid it, and I have a one-line fix and a test ready, to be filed upstream.
36
+
37
+ One edge case I saw in the code but didn't test: when you pass your own `z` and its first point is in the set but its last isn't, the loop `m < len(z)` may never find a class that isolates the first point, and music21 would raise `SieveException`.
38
+
39
+ ## 2. The problem and the cost
40
+
41
+ The input is a target set `S` and a domain. The domain is either a window `z`, where the formula has to match exactly on `z` (music21's way), or a period `P`, where it has to match on every integer.
42
+
43
+ Formulas are residue classes `m@r` joined with `|`, `&` and `-`. The cost is the size of the formula tree: leaves plus binary operators plus complements. A union of k classes costs 2k - 1, and `-(a|b)` costs 4. I report the class count (leaves) as well.
44
+
45
+ ### Unions of classes: `minimize_union`
46
+
47
+ The candidates are every distinct non-empty class on the domain that lies inside `S`. On a window that means moduli 1 to span+1, since anything bigger only gives single points, which are already there. On a periodic domain it means every divisor of `P`. The candidate list is complete, so the minimum is global. I solve it as exact set cover with an ILP (HiGHS through scipy, gap 0).
48
+
49
+ There are three objectives, each solved exactly on its own:
50
+ - `leaves`: the fewest classes.
51
+ - `chars`: the shortest music21 string.
52
+ - `period`: the smallest lcm of the moduli. That's the smallest L where every target point sits in a class inside the target whose modulus divides L. On windows I search L up to 200,000; on periodic domains, the divisors of P.
53
+
54
+ The solve runs in two stages. Stage one proves the primary optimum. Stage two breaks ties (fewer characters, then smaller moduli) with a 2 second limit, and `Result.bounds['tie_break_proven']` says whether it finished. Packing both into one weight was up to 10 times slower on some 100-point sets.
55
+
56
+ ### Unions of `P & -(N1|...|Nk)` terms: `minimize_dnf`
57
+
58
+ `P` is a residue class, or nothing. If leaf moduli are capped at M, `P` is written as the cheapest intersection of leaves, so `11@2&13@2` stands for `143@2` when M = 13. This is the shape of Ariza's Nomos Alpha formula.
59
+
60
+ For each `P`, I branch on its first point outside `S` and list every way to cover those outside points with at most k classes, keeping the cheapest cost for each resulting set. A weighted set cover ILP then picks the cheapest union. The search also tries the complement of `S` and wraps that answer in `-(...)`.
61
+
62
+ This is optimal over every formula of this shape for the given k and M. It isn't optimal over every formula.
63
+
64
+ ### Any formula: `minimize_general`
65
+
66
+ Dynamic programming over all 2^n subsets of the domain by increasing size, the standard way to find minimum Boolean formulas. It's exact over every formula tree, but it stops at 14 points, which fits one 12-note octave.
67
+
68
+ ### Modulus limits
69
+
70
+ A modulus bigger than the window gives a class with one point in it, which is "compression" in name only. So every window result here uses music21's own limit, m up to len(z) - 1, and the solver never uses a modulus music21 couldn't. Periodic results only use divisors of the period.
71
+
72
+ I also tried the stricter limit m up to len(z)/2. It's often impossible: 4 of music21's 12 test segments, including `[3,4,5,6,7,8,13,19]`, can't be written as a union at all under it. Some point lies in no class of that size that stays inside the set. Where it works, the minimum only goes up a little (Nomos Alpha 7 to 8, Nekuïa 8 to 9).
73
+
74
+ ### Checks
75
+
76
+ The tests check the union solver against brute force on 40 random small windows. They also check general ≤ dnf ≤ union on 40 random periodic sets, and that the smallest period is really minimal on 20 random windows. They check that my Nomos Alpha values match the list hmt publishes. Every benchmark formula was run back through music21.
77
+
78
+ ## 3. Results
79
+
80
+ Full tables are in `bench/results.md`. Sizes count leaves plus operators. "Same moduli" means leaf moduli no bigger than the largest one in the published formula, with at most 4 complemented classes per term. HiGHS proved every result here optimal.
81
+
82
+ ### Published sieves, periodic (must match on every integer)
83
+
84
+ | sieve (source) | written size | exact, same moduli | exact, any modulus | exact, any shape |
85
+ |---|---|---|---|---|
86
+ | Major scale, FM p.197 (music21, hmt) | 18 | 17 as terms; 16 any shape, moduli ≤ 4 | 10 | 10 |
87
+ | Nomos Alpha (music21 testSieve, hmt) | 23 | 21 | 21 | n/a |
88
+ | Psappha, after Flint (music21, hmt) | 59 | 36 | 23 | n/a |
89
+ | Jonchaies (hmt; Exarchos p.72) | 15 | 15 | 15 | n/a |
90
+ | À R., after Squibbs 1996 (hmt) | 99 | 88 | 57 | n/a |
91
+
92
+ The shortest Nomos Alpha with Xenakis's own moduli is `11@2&-(13@3|13@5|13@7)|13@0|13@1|13@6|13@9&-(11@4|11@8)`. It leaves 13@9 out of the first complement, because 13@9 is covered by another term anyway.
93
+
94
+ The shortest major scale with moduli up to 4 is `-(4@0&3@2|3@0&(4@2|4@3)|3@1&(4@1|4@2))`, size 16. The best union of terms is 17, which is how I know the term shape isn't always optimal.
95
+
96
+ Jonchaies is already minimal. Its period is 17, a prime, so there's nothing to factor.
97
+
98
+ ### Three ways to measure "short", window z = 0..99, music21's modulus limit
99
+
100
+ Each exact number is the optimum for that measure on its own.
101
+
102
+ | set | classes, music21 / exact (with limit len(z)/2) | characters, music21 / exact | period, music21 / exact |
103
+ |---|---|---|---|
104
+ | `[3,4,5,6,7,8,13,19]` (music21 docstring) | 6 / 6 (impossible) | 25 / 25 | 27,720 / 16 |
105
+ | Nomos Alpha | 8 / 7 (8) | 42 / 36 | 4,165,408 / 198 |
106
+ | Psappha | 20 / 15 (15) | 107 / 77 | 5,426,400 / 40 |
107
+ | Jonchaies | 8 / 8 (8) | 42 / 42 | 17 / 17 |
108
+ | À R. | 17 / 14 (14) | 93 / 75 | 4.45 × 10^14 / 88 |
109
+ | Nekuïa (window 0..48) | 11 / 8 (9) | 57 / 40 | 203,490 / 48 |
110
+ | Major scale | 6 / 6 (6) | 28 / 28 | 12 / 12 |
111
+
112
+ For the docstring example, music21 gives `6@1|7@6|8@5|9@4|10@3|11@8`, and 6 classes is the true minimum. The shortest string is also 25 characters (for example `6@1|7@6|8@5|9@4|11@8|16@3`). The smallest period is 16, which still takes 6 classes but 28 characters: `8@5|16@3|16@4|16@6|16@7|16@8`. The three measures disagree, so I report all three.
113
+
114
+ Period is where the greedy looks worst. It grabs whatever modulus fits first, so the lcm runs into the millions where 40 would do.
115
+
116
+ ### music21's greedy against the exact minimum, window semantics
117
+
118
+ | set | points | music21 classes | exact classes | music21 size | exact size with & and - |
119
+ |---|---|---|---|---|---|
120
+ | 12 music21 test segments | 3 to 17 | same as exact on all 12 | | | shorter on 2 of 12, e.g. `[0,2,4,5,7,9,11,12]`: 7 down to 6 with `-(7@1|7@3|7@6)` |
121
+ | testSieve 1 on z = 0..99 | 91 | 4 | 4 | 7 | 2 (`-12@2`) |
122
+ | Nomos Alpha, z = 0..99 | 32 | 8 | 7 | 15 | 13 |
123
+ | Psappha, z = 0..99 | 68 | 20 | 15 | 39 | 23 |
124
+ | À R., z = 0..99 | 49 | 17 | 14 | 33 | 27 |
125
+ | Nekuïa, Xenakis's 20-class union, on its window | 24 | 11 | 8 | 21 | 15 |
126
+
127
+ ### 1,000 random segments
128
+
129
+ Each segment has k points (k drawn from 5 to 100) taken from 0..S, with S drawn from k to 2k, always including 0 and S. So music21's default window is 0..S and between half and all of it is filled. Seed 2026, music21's own modulus limit, and every formula checked by music21 (`bench/random_segments.md`).
130
+
131
+ | measure | music21 mean | exact mean | sets where exact is smaller | mean reduction | max reduction |
132
+ |---|---|---|---|---|---|
133
+ | classes | 18.4 | 13.7 | 928 of 1,000 | 22.4% | 40.5% |
134
+ | characters | 96.7 | 70.6 | 931 of 1,000 | 23.1% | 42.1% |
135
+ | period (median) | 21,162,960 | 70 | 996 of 1,000 | | |
136
+
137
+ The measures fight each other. Optimising for the smallest period drives the median down to 70, but costs 34.5 classes on average against 13.7. Optimising for classes or characters leaves the period in the tens of millions, about where music21 is.
138
+
139
+ With the stricter limit len(z)/2, 58 of the 1,000 can't be written as a union at all. On the other 942 the minimum goes from 14.18 to 14.31 classes on average.
140
+
141
+ For the 251 segments with span up to 40, I also ran the full algebra (k ≤ 2). It beat the best union on 180 of them. Mean size: music21 13.1, best union 10.8, with intersection and complement 8.0.
142
+
143
+ These are the slowest cases. music21 takes 9 ms on average. The exact union averages 2.9 s, and the worst took 248 s, which is set cover being NP-hard. The whole run took 1 hour 54 minutes.
144
+
145
+ ### 100 random Xenakis-style sieves (z = 0..59, seed 2026)
146
+
147
+ Each target is a random union of 2 to 4 terms, each `m@r` or `m@r & -(n@s|...)`, moduli 2 to 13.
148
+
149
+ In unions only, music21 was beaten on 22 of 100, by 5.2% fewer classes on average (median 0%, max 40%). With intersection and complement (k ≤ 2) the exact formula was shorter than music21's on 37 of 100, by 11.4% on average (median 0%, max 63.6%). The exact answer was never longer than the formula that generated the set.
150
+
151
+ ### Every 5 to 7 note pitch-class set containing 0 (mod 12)
152
+
153
+ That's 1,254 sets. Intersection and complement beat the best plain union on 432 of them. Mean size is 7.32 for unions and 6.89 with terms.
154
+
155
+ ### Formula plus exceptions: `minimize_union_tolerant`
156
+
157
+ Find a union of classes F and at most k exceptions so that F, with those notes added or removed, equals the target. The cost is size(F) + λ × exceptions. It's one exact ILP (`src/sievemin/tolerant.py`): a binary variable per class and per exception, a constraint per target point (covered, or added), a constraint per non-target point a chosen class touches (so it must be removed), and the edit budget.
158
+
159
+ λ is a choice. At λ = 2 an exception costs the same as one more single-point class, so the cost can never come out higher than the exact formula's, but adding a note never strictly pays. At λ = 1 an exception is a bit cheaper than a class.
160
+
161
+ To test it I planted a random irredundant union of 2 or 3 classes (moduli 2 to 12) on z = 0..59, flipped e random notes, and ran the solver with k = e. That's 150 trials per cell, seed 2026 (`bench/tolerant_study.md`):
162
+
163
+ | λ | 1 flip | 2 flips | 3 flips |
164
+ |---|---|---|---|
165
+ | 1 | 100% | 96% | 95% |
166
+ | 2 | 100% | 77% | 49% |
167
+ | 3 | 28% | 13% | 3% |
168
+
169
+ All 1,350 runs were proven optimal. Every miss was a case where some other formula plus exceptions was strictly cheaper than the planted one. The planted sieve just wasn't the shortest explanation at that λ, so no solver could have picked it. There were no ties and no search failures. Without exceptions the noise is expensive: the exact formula for the noisy set averages size 7.0, 8.7 and 11.0 for 1, 2 and 3 flips, against 4.0 for the planted sieve.
170
+
171
+ I couldn't get real Xenakis score passages legitimately. The music21 corpus has none, and the scores are under copyright. As a stand-in, I ran the published sets through it (periodic, λ = 1). In unions only, Nomos Alpha drops from cost 35 to 21 with 2 exceptions, removing 48 and 74, which are exactly the points Xenakis's complement terms take out. The major scale becomes `3@2|4@0` with 3 exceptions. That's the mode finding structure. It isn't evidence that Xenakis edited notes by hand; that idea is plausible, but I've no source for it.
172
+
173
+ ### Speed
174
+
175
+ Periodic cases take 2 to 25 ms. Window cases on z = 0..99 take up to about 2 s (Psappha unions, around 3,000 candidate classes). Random 100-point windows can take tens of seconds, because exact set cover is NP-hard. The full benchmark runs in about 1.5 minutes on my laptop (Ryzen 9 8940HX).
176
+
177
+ ## 4. Limits
178
+
179
+ `minimize_dnf` is optimal within its shape, for the k and M you give it. Other shapes like `(A|B)&(C|D)` can be shorter with capped moduli. The major scale (16 against 17) is a real example. Only `minimize_general` covers every shape, and only up to 14 points.
180
+
181
+ Window results depend on `z`, the same as music21's. A window formula can use moduli Xenakis would never pick, like `41@24`. The periodic results are the musically meaningful ones.
182
+
183
+ Generating terms is exponential in k. There's a cap (2 million terms by default) that raises `OverflowError` rather than quietly returning something non-optimal.
184
+
185
+ The error-tolerant mode only does unions, and its answer depends on λ. On a periodic domain, an exception is a whole residue mod P, so a pitch class in every octave.
186
+
187
+ Sources: the published formulas come from music21's tests and the hmt Haskell library (hmt-0.16). For the major scale, Nomos Alpha and Psappha, both give the same formula. I haven't read Formalized Music or Xenakis 1990 directly. The sets my code produces match the value lists hmt publishes for Nomos Alpha (32 values), Psappha (27) and À R. (42).
188
+
189
+ Not benchmarked: Akea, Mists, Keqrops and the period-88 Nekuïa from Exarchos's thesis. Their formulas are only in images or badly scanned text.
190
+
191
+ ## 5. Prior work
192
+
193
+ A web search found the sources below. I trust what I found on music21 and hmt most, then Exarchos (I read part 1 of the thesis as extracted text). I trust it least on Ariza 2005, Xenakis 1990, Gibson and Andreatta, which are paywalled; I only saw snippets.
194
+
195
+ Ariza 2005, Computer Music Journal 29(2), "The Xenakis Sieve as Object", is the model behind music21. Its compression is the greedy described above, and I saw no claim that it's minimal.
196
+
197
+ Xenakis's own analysis method, from "Sieves" (Perspectives of New Music 28/1, 1990), is described in Exarchos's 2007 Goldsmiths PhD (sections 5.3 and 5.7). It's also greedy. For each point it takes the smallest modulus whose later points stay inside the sieve and that covers a new point. The 1990 final stage adds a rule against "parasitical" points. It works on bounded modules (a start point and a repeat count), not plain residue classes. Exarchos calls Akea's 16-module form "minimal" in the sense that no module is redundant.
198
+
199
+ hmt (Rohan Drape, Haskell) evaluates, reduces and normalises sieves. It doesn't search for a sieve from a set.
200
+
201
+ Noll, Andreatta and Agon's tone-sieve analysis, Squibbs 1996 and Gibson are analysis and theory. I found no minimiser in any of them.
202
+
203
+ The maths is known. Covering a set with arithmetic progressions, and the exact version, are NP-complete (Heath 1990). Bliznets, Nederlof and Szilágyi (arXiv 2312.06393) give parameterised exact algorithms. So an exact union-only minimum is a familiar problem, and ILP is the standard tool for it.
204
+
205
+ What seems to be new (I found nothing that already does it, but my search wasn't exhaustive):
206
+
207
+ 1. A provably optimal minimiser for sieves using union, intersection and complement, with a stated cost.
208
+ 2. An exhaustive any-shape minimum for small periods.
209
+ 3. Exact formula-plus-exceptions fitting with a stated cost, plus a recovery study.
210
+ 4. Measured evidence that music21's greedy and several published formulas aren't minimal.
211
+ 5. The music21 crash on complemented intersections.
212
+
213
+ ## 6. Offering it to music21
214
+
215
+ The crash fix is ready. It catches `ResidualException` in `Sieve._initCompression` and adds a test. It is reported upstream as [cuthbertLab/music21#2131](https://github.com/cuthbertLab/music21/issues/2131).
216
+
217
+ For the feature, a proposal will be opened upstream. It would add an opt-in `CompressionSegment(..., method='exact')` that imports scipy only when asked. scipy is already in music21's optional `extras`, and `audioSearch` already imports scipy the same way with a fallback, so it adds no new dependency. A pure-Python branch-and-bound with a size guard is the alternative. The default stays greedy.
218
+
219
+ The rest (the term shape, the general search and the tolerant mode) stays in this package. It's more than music21's core needs.
220
+
221
+ ## 7. Reproducing
222
+
223
+ `python bench/reproduce.py` regenerates `bench/results.md` and the tolerant study in a few minutes. `--all` adds the 1,000-segment run, about 2 hours. CI runs the tests on Ubuntu and Windows with Python 3.10, 3.12 and 3.14 (`.github/workflows/ci.yml`).