mache 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.
mache-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 Andrew Wright
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.
mache-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: mache
3
+ Version: 0.1.0
4
+ Summary: Read a sharded engine match: the pooled estimate and the ccrl fit
5
+ Author: Andrew Wright
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/aywrite/mache
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Provides-Extra: test
12
+ Requires-Dist: pytest~=9.1; extra == "test"
13
+ Dynamic: license-file
14
+
15
+ # mache
16
+
17
+ Measure A CHess Engine.
18
+
19
+ A collection of tools for measuring and benchmarking chess engines. From mache
20
+ (μάχη), Greek for battle.
21
+
22
+ ## Why this exists
23
+
24
+ [OpenBench](https://github.com/AndyGrant/OpenBench) is how engine testing is
25
+ normally done: an instance hands out tests and client machines attach to it and
26
+ play the games. In almost every case using OpenBench is a far better choice.
27
+
28
+ This tool exists for two reasons.
29
+
30
+ The first is that it was not planned. It grew as I wrote
31
+ [arche](https://github.com/aywrite/arche), my first chess engine, starting as a
32
+ basic CI job that got out of hand.
33
+
34
+ The second is that OpenBench needs machines. There is a shared instance a good
35
+ many engines develop against, and a dozen or more projects run their own. mache
36
+ is for the case where you have neither. It has no instance and no clients, and
37
+ it runs on the hosted CI runners a repository already gets, as part of the pull
38
+ requests and releases it already runs, so testing an engine costs no machine you
39
+ have to own, administer or ask anyone to lend you. Both play their games with
40
+ fastchess underneath.
41
+
42
+ Hosted runners are the point of mache and also what make it awkward. They are
43
+ slow, they are noisy, they are shared, and a job is killed at a time limit often
44
+ long before a match worth reading has finished. So a match is split across jobs
45
+ that run at once and pooled afterwards, which is most of what the tools below
46
+ are for, and why they take the care described further down.
47
+
48
+ ## What is here
49
+
50
+ `mache`, a Python package with four tools:
51
+
52
+ | Tool | What it answers |
53
+ | --- | --- |
54
+ | `match-estimate` | How much stronger, over the pooled games of every shard. Or, with bounds, whether |
55
+ | `rating-estimate` | Where an engine sits on a published rating scale, from a gauntlet |
56
+ | `match-terminations` | How the games actually ended |
57
+ | `book-slice` | Which openings a shard plays, so that no two shards share one |
58
+
59
+ `actions/setup`, a composite action that builds
60
+ [fastchess](https://github.com/Disservin/fastchess) at a pinned commit,
61
+ fetches an opening book and checks it against a recorded hash, and puts the
62
+ package on `PYTHONPATH`. Nothing is installed at match time.
63
+
64
+ ## The part that is not obvious
65
+
66
+ A sharded match is not a long match cut up. Three things have to hold or the
67
+ number it produces is wrong.
68
+
69
+ **The estimate is over the pool.** fastchess prints one, but only for the
70
+ games its own process played. With five shards that is a fifth of the
71
+ evidence, and averaging five such figures is a different calculation.
72
+ `match-estimate` reads the games themselves.
73
+
74
+ **The error bar is over pairs, not games.** Under `-repeat` the two games of a
75
+ round are one opening with the colours reversed, so they are one observation.
76
+ Counting them as two understates the spread.
77
+
78
+ **A sequential test looks only at batch boundaries.** A per-shard SPRT that
79
+ stopped when its own games settled the question would be one look per shard at
80
+ a bound priced for one, on a sample chosen by what it said. Here the shards
81
+ play their slices out with nothing watching and the test is judged once over
82
+ all of them. A run is one batch, and `--prior-pairs` carries its pairs into
83
+ the next, so repeated runs accumulate into one test rather than several.
84
+
85
+ Openings follow from a seed rather than a shuffle, so a schedule can be played
86
+ again from what the run recorded.
87
+
88
+ ## Reading a rating estimate
89
+
90
+ `rating-estimate` holds every opponent at its published figure and fits the one
91
+ free parameter, so the figure is the rating at which the expected score equals
92
+ the score actually made.
93
+
94
+ The `±` is a 95% interval and it describes the games and nothing else. Whether
95
+ one rating can describe the results at all is asked separately: when the
96
+ opponents disagree with each other by more than chance allows, a note says so,
97
+ and the interval is an understatement rather than an estimate.
98
+
99
+ **A placement against a published list carries a systematic error no number of
100
+ games reduces.** The opponents earned their ratings on other hardware at
101
+ slower time controls. Treat the figure as a placement worth about a hundred
102
+ points either way, not as a rating.
103
+
104
+ ## Install
105
+
106
+ ```
107
+ pip install mache
108
+ ```
109
+
110
+ The engine side needs no install. The action puts the package on the path.
111
+
112
+ ## Status
113
+
114
+ Alpha. mache is used by [arche](https://github.com/aywrite/arche), which is
115
+ where it was written, and it has not yet been used by an engine that is not
116
+ arche. Until it has, expect the rough edges of a tool with one user.
117
+
118
+ mache is written with heavy AI assistance.
119
+
120
+ ## Licence
121
+
122
+ MIT. See [LICENSE](LICENSE).
123
+
124
+ The generalized log likelihood ratio follows Van den Bergh's note on the
125
+ pentanomial model, written from the note and checked against fastchess: for
126
+ the same pairs the number here is the number it prints. Two test cases are
127
+ fastchess's own, attributed where they are used. fastchess is MIT.
mache-0.1.0/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # mache
2
+
3
+ Measure A CHess Engine.
4
+
5
+ A collection of tools for measuring and benchmarking chess engines. From mache
6
+ (μάχη), Greek for battle.
7
+
8
+ ## Why this exists
9
+
10
+ [OpenBench](https://github.com/AndyGrant/OpenBench) is how engine testing is
11
+ normally done: an instance hands out tests and client machines attach to it and
12
+ play the games. In almost every case using OpenBench is a far better choice.
13
+
14
+ This tool exists for two reasons.
15
+
16
+ The first is that it was not planned. It grew as I wrote
17
+ [arche](https://github.com/aywrite/arche), my first chess engine, starting as a
18
+ basic CI job that got out of hand.
19
+
20
+ The second is that OpenBench needs machines. There is a shared instance a good
21
+ many engines develop against, and a dozen or more projects run their own. mache
22
+ is for the case where you have neither. It has no instance and no clients, and
23
+ it runs on the hosted CI runners a repository already gets, as part of the pull
24
+ requests and releases it already runs, so testing an engine costs no machine you
25
+ have to own, administer or ask anyone to lend you. Both play their games with
26
+ fastchess underneath.
27
+
28
+ Hosted runners are the point of mache and also what make it awkward. They are
29
+ slow, they are noisy, they are shared, and a job is killed at a time limit often
30
+ long before a match worth reading has finished. So a match is split across jobs
31
+ that run at once and pooled afterwards, which is most of what the tools below
32
+ are for, and why they take the care described further down.
33
+
34
+ ## What is here
35
+
36
+ `mache`, a Python package with four tools:
37
+
38
+ | Tool | What it answers |
39
+ | --- | --- |
40
+ | `match-estimate` | How much stronger, over the pooled games of every shard. Or, with bounds, whether |
41
+ | `rating-estimate` | Where an engine sits on a published rating scale, from a gauntlet |
42
+ | `match-terminations` | How the games actually ended |
43
+ | `book-slice` | Which openings a shard plays, so that no two shards share one |
44
+
45
+ `actions/setup`, a composite action that builds
46
+ [fastchess](https://github.com/Disservin/fastchess) at a pinned commit,
47
+ fetches an opening book and checks it against a recorded hash, and puts the
48
+ package on `PYTHONPATH`. Nothing is installed at match time.
49
+
50
+ ## The part that is not obvious
51
+
52
+ A sharded match is not a long match cut up. Three things have to hold or the
53
+ number it produces is wrong.
54
+
55
+ **The estimate is over the pool.** fastchess prints one, but only for the
56
+ games its own process played. With five shards that is a fifth of the
57
+ evidence, and averaging five such figures is a different calculation.
58
+ `match-estimate` reads the games themselves.
59
+
60
+ **The error bar is over pairs, not games.** Under `-repeat` the two games of a
61
+ round are one opening with the colours reversed, so they are one observation.
62
+ Counting them as two understates the spread.
63
+
64
+ **A sequential test looks only at batch boundaries.** A per-shard SPRT that
65
+ stopped when its own games settled the question would be one look per shard at
66
+ a bound priced for one, on a sample chosen by what it said. Here the shards
67
+ play their slices out with nothing watching and the test is judged once over
68
+ all of them. A run is one batch, and `--prior-pairs` carries its pairs into
69
+ the next, so repeated runs accumulate into one test rather than several.
70
+
71
+ Openings follow from a seed rather than a shuffle, so a schedule can be played
72
+ again from what the run recorded.
73
+
74
+ ## Reading a rating estimate
75
+
76
+ `rating-estimate` holds every opponent at its published figure and fits the one
77
+ free parameter, so the figure is the rating at which the expected score equals
78
+ the score actually made.
79
+
80
+ The `±` is a 95% interval and it describes the games and nothing else. Whether
81
+ one rating can describe the results at all is asked separately: when the
82
+ opponents disagree with each other by more than chance allows, a note says so,
83
+ and the interval is an understatement rather than an estimate.
84
+
85
+ **A placement against a published list carries a systematic error no number of
86
+ games reduces.** The opponents earned their ratings on other hardware at
87
+ slower time controls. Treat the figure as a placement worth about a hundred
88
+ points either way, not as a rating.
89
+
90
+ ## Install
91
+
92
+ ```
93
+ pip install mache
94
+ ```
95
+
96
+ The engine side needs no install. The action puts the package on the path.
97
+
98
+ ## Status
99
+
100
+ Alpha. mache is used by [arche](https://github.com/aywrite/arche), which is
101
+ where it was written, and it has not yet been used by an engine that is not
102
+ arche. Until it has, expect the rough edges of a tool with one user.
103
+
104
+ mache is written with heavy AI assistance.
105
+
106
+ ## Licence
107
+
108
+ MIT. See [LICENSE](LICENSE).
109
+
110
+ The generalized log likelihood ratio follows Van den Bergh's note on the
111
+ pentanomial model, written from the note and checked against fastchess: for
112
+ the same pairs the number here is the number it prints. Two test cases are
113
+ fastchess's own, attributed where they are used. fastchess is MIT.
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2022-2026 Andrew Wright
3
+
4
+ """The tools a match is read with: the pooled estimate, the ccrl fit, the
5
+ terminations count and the book slice.
6
+
7
+ They are one package because they read one thing. The fit owns the two regular
8
+ expressions a fastchess pgn is split into games with, and the estimate reads
9
+ the terminations count as well as the fit.
10
+ """
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ # The version of the --json shape, and the one thing every --json object says
15
+ # about itself. It is 1 because the shape is released: a tagged version is what
16
+ # another repository can pin, and a shape nobody can pin is not a contract.
17
+ # Fields are added from here on. None is removed, and none is given a new
18
+ # meaning under the name it already has. A change that cannot be made that way
19
+ # raises this number, and a reader that finds a number it does not know should
20
+ # say so rather than read the object anyway.
21
+ JSON_FORMAT = 1
22
+
23
+
24
+ def tool(command: str) -> dict[str, str]:
25
+ """What produced a --json object, so a figure can be read back against the
26
+ version of the tooling that produced it."""
27
+ return {"name": "mache", "version": __version__, "command": command}
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (C) 2022-2026 Andrew Wright
3
+
4
+ """Print the opening a shard of a match starts at.
5
+
6
+ The shards of a match play at the same time and are pooled afterwards, so no
7
+ two of them may play the same opening: a position played twice would be counted
8
+ twice and the games would not be the independent sample the estimate reads them
9
+ as. The book is taken in order rather than drawn, and each shard is given a
10
+ slice of its own, which is where its games begin.
11
+
12
+ The run's first shard starts at a remainder of the seed, so two runs of the same
13
+ size play different regions of a book far larger than either of them uses, and
14
+ each shard after it starts a slice further along. The starts are worked out from
15
+ the seed and the counts alone, so the manifest is enough to play the schedule
16
+ again, without depending on how fastchess draws its own openings.
17
+
18
+ The index printed is one based, which is what fastchess's `start=` takes.
19
+ """
20
+
21
+ import argparse
22
+ import sys
23
+
24
+
25
+ def first_opening(openings: int, wanted: int, seed: int) -> int:
26
+ """Where the first shard begins. The offset leaves room for every shard, so
27
+ the last one still ends inside the book."""
28
+ room = openings - wanted
29
+ if room < 1:
30
+ return 1
31
+ return 1 + seed % room
32
+
33
+
34
+ def start(openings: int, pairs: int, shards: int, shard: int, seed: int) -> int:
35
+ """The opening this shard begins at, one based."""
36
+ return first_opening(openings, pairs * shards, seed) + shard * pairs
37
+
38
+
39
+ def main() -> None:
40
+ parser = argparse.ArgumentParser(description=__doc__)
41
+ parser.add_argument("--openings", type=int, required=True, help="what the book has")
42
+ parser.add_argument("--pairs", type=int, required=True, help="openings per shard")
43
+ parser.add_argument("--shards", type=int, required=True, help="shards in the run")
44
+ parser.add_argument("--shard", type=int, required=True, help="which one, from zero")
45
+ parser.add_argument("--seed", type=int, required=True, help="the run's seed")
46
+ args = parser.parse_args()
47
+
48
+ if min(args.openings, args.pairs, args.shards) < 1 or args.seed < 0:
49
+ sys.exit("the counts are all at least one and the seed is not negative")
50
+ if not 0 <= args.shard < args.shards:
51
+ sys.exit(f"shard {args.shard} is not one of {args.shards}")
52
+
53
+ wanted = args.pairs * args.shards
54
+ if args.openings - wanted < 1:
55
+ # fastchess reads on around the end of the book, so the match still
56
+ # plays. It is the shards no longer being disjoint that is worth saying
57
+ print(
58
+ f"the book holds {args.openings} openings and the run wants {wanted},"
59
+ " so the shards repeat each other",
60
+ file=sys.stderr,
61
+ )
62
+ print(start(args.openings, args.pairs, args.shards, args.shard, args.seed))
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()