mchammer-moves 0.6.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.
Files changed (32) hide show
  1. mchammer_moves-0.6.0/LICENSE +21 -0
  2. mchammer_moves-0.6.0/PKG-INFO +273 -0
  3. mchammer_moves-0.6.0/README.md +247 -0
  4. mchammer_moves-0.6.0/pyproject.toml +65 -0
  5. mchammer_moves-0.6.0/setup.cfg +4 -0
  6. mchammer_moves-0.6.0/src/mchammer_moves/__init__.py +64 -0
  7. mchammer_moves-0.6.0/src/mchammer_moves/ensemble.py +679 -0
  8. mchammer_moves-0.6.0/src/mchammer_moves/moves/__init__.py +7 -0
  9. mchammer_moves-0.6.0/src/mchammer_moves/moves/base.py +88 -0
  10. mchammer_moves-0.6.0/src/mchammer_moves/moves/cyclic_reflection.py +181 -0
  11. mchammer_moves-0.6.0/src/mchammer_moves/moves/cyclic_shift.py +146 -0
  12. mchammer_moves-0.6.0/src/mchammer_moves/moves/index_set_swap.py +222 -0
  13. mchammer_moves-0.6.0/src/mchammer_moves/moves/multi_pair_swap.py +186 -0
  14. mchammer_moves-0.6.0/src/mchammer_moves/moves/pair_swap.py +108 -0
  15. mchammer_moves-0.6.0/src/mchammer_moves/moves/site_permutation.py +221 -0
  16. mchammer_moves-0.6.0/src/mchammer_moves/py.typed +0 -0
  17. mchammer_moves-0.6.0/src/mchammer_moves.egg-info/PKG-INFO +273 -0
  18. mchammer_moves-0.6.0/src/mchammer_moves.egg-info/SOURCES.txt +30 -0
  19. mchammer_moves-0.6.0/src/mchammer_moves.egg-info/dependency_links.txt +1 -0
  20. mchammer_moves-0.6.0/src/mchammer_moves.egg-info/requires.txt +9 -0
  21. mchammer_moves-0.6.0/src/mchammer_moves.egg-info/top_level.txt +1 -0
  22. mchammer_moves-0.6.0/tests/test_boltzmann_sampling.py +63 -0
  23. mchammer_moves-0.6.0/tests/test_cyclic_reflection.py +254 -0
  24. mchammer_moves-0.6.0/tests/test_cyclic_shift.py +284 -0
  25. mchammer_moves-0.6.0/tests/test_ensemble.py +428 -0
  26. mchammer_moves-0.6.0/tests/test_index_set_swap.py +389 -0
  27. mchammer_moves-0.6.0/tests/test_move_dispatcher.py +163 -0
  28. mchammer_moves-0.6.0/tests/test_multi_pair_swap.py +266 -0
  29. mchammer_moves-0.6.0/tests/test_pair_swap.py +314 -0
  30. mchammer_moves-0.6.0/tests/test_picklability.py +36 -0
  31. mchammer_moves-0.6.0/tests/test_site_permutation.py +242 -0
  32. mchammer_moves-0.6.0/tests/test_wang_landau_ensemble.py +355 -0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Benjamin J. Morgan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: mchammer-moves
3
+ Version: 0.6.0
4
+ Summary: Custom Monte Carlo moves for icet/mchammer.
5
+ Author-email: "Benjamin J. Morgan" <b.j.morgan@bath.ac.uk>
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: icet>=3.2
18
+ Requires-Dist: numpy
19
+ Requires-Dist: ase
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: scipy; extra == "dev"
23
+ Requires-Dist: mypy; extra == "dev"
24
+ Requires-Dist: ruff; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # mchammer-moves
28
+
29
+ Custom Monte Carlo trial moves for [icet/mchammer](https://icet.materialsmodeling.org/).
30
+ The `Move` base class defines a sampler-agnostic proposal contract; ensemble
31
+ adapters consume moves and handle acceptance, bookkeeping, and data-container
32
+ integration for canonical and Wang-Landau sampling, without modification of the
33
+ mchammer source or downstream wrappers such as `mchammer-pt`.
34
+
35
+ The package provides:
36
+
37
+ - a `Move` abstract base class for user-defined trial moves;
38
+ - five built-in moves:
39
+ - `PairSwap` — the standard canonical two-site swap;
40
+ - `MultiPairSwap` — `k` site-disjoint pair swaps applied as one
41
+ atomic proposal; useful when single-pair swaps are kinetically
42
+ blocked between adjacent minima in deep basins;
43
+ - `CyclicShift` — single-step shift of the species pattern along a
44
+ user-supplied index cycle, with periodic boundaries within the
45
+ cycle; useful for row or ring translations on chain-like or
46
+ ring-like sublattices;
47
+ - `CyclicReflection` — long-range reflection of the species pattern
48
+ along an index cycle around a randomly-chosen pivot; complements
49
+ `CyclicShift`'s nearest-neighbour shifts by enabling species to
50
+ hop across a chain in a single accepted move;
51
+ - `IndexSetSwap` — swaps occupations between two equal-length index
52
+ sets drawn uniformly from a user-supplied list of groups; a
53
+ generic primitive for chain-, motif-, or layer-swap moves;
54
+ - `SitePermutation` — applies a caller-supplied permutation of site
55
+ occupations, drawn uniformly from a list of operations, with an
56
+ unconditional forward/inverse direction draw; covers reflections
57
+ (e.g. across a `<100>` plane), point inversion, and proper or
58
+ improper rotations of any order;
59
+ - `CustomCanonicalEnsemble`, a drop-in replacement for
60
+ `mchammer.ensembles.CanonicalEnsemble` that draws moves from a
61
+ user-supplied weighted list and tracks per-move acceptance;
62
+ - `CustomWangLandauEnsemble`, a drop-in replacement for
63
+ `mchammer.ensembles.WangLandauEnsemble` with the same weighted-move
64
+ dispatch, plus per-move window-vs-WL rejection classification;
65
+ - `MoveDispatcher`, the shared weighted-selection and per-move
66
+ bookkeeping engine used by both ensemble adapters.
67
+
68
+ Installation (editable):
69
+
70
+ ```bash
71
+ pip install -e .
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ `structure`, `ce`, and `cycles` below are placeholders for your atoms
77
+ object, cluster expansion, and chain definitions respectively; the
78
+ package contains no system-specific geometry, so you supply them
79
+ yourself.
80
+
81
+ ```python
82
+ from mchammer.calculators import ClusterExpansionCalculator
83
+ from mchammer_moves import CustomCanonicalEnsemble, CyclicShift, PairSwap
84
+
85
+ calc = ClusterExpansionCalculator(structure, ce)
86
+
87
+ ensemble = CustomCanonicalEnsemble(
88
+ structure=structure,
89
+ calculator=calc,
90
+ temperature=600.0,
91
+ moves=[
92
+ (PairSwap(sublattice_index=0), 1.0),
93
+ (CyclicShift(cycles=cycles), 0.05),
94
+ ],
95
+ )
96
+ ensemble.run(10_000)
97
+
98
+ print(ensemble.acceptance_rates())
99
+ ```
100
+
101
+ ## Use with `mchammer-pt`
102
+
103
+ `mchammer-pt` (v0.2+) accepts a custom ensemble class via its native
104
+ `ensemble_cls=` parameter, with constructor arguments forwarded via
105
+ `ensemble_kwargs=`:
106
+
107
+ ```python
108
+ from mchammer_pt import CanonicalParallelTempering
109
+ from mchammer_moves import CustomCanonicalEnsemble, CyclicShift, PairSwap
110
+
111
+ with CanonicalParallelTempering.process_pool(
112
+ cluster_expansion=ce,
113
+ atoms=initial_structure,
114
+ temperatures=temperatures,
115
+ block_size=block_size,
116
+ random_seed=42,
117
+ ensemble_cls=CustomCanonicalEnsemble,
118
+ ensemble_kwargs={
119
+ "moves": [
120
+ (PairSwap(sublattice_index=anion_sl), 1.0),
121
+ (CyclicShift(cycles=cycles), 0.05),
122
+ ],
123
+ },
124
+ ) as pt:
125
+ history = pt.run(n_cycles=N_CYCLES)
126
+ ```
127
+
128
+ Per-move acceptance and null-proposal rates are recorded into each
129
+ replica's `mchammer.BaseDataContainer` at every
130
+ `ensemble_data_write_interval` as `<move>_acceptance_rate` and
131
+ `<move>_null_rate` columns, so they survive the `ProcessPool`
132
+ boundary and are recoverable from the HDF5 bundle written by
133
+ `mchammer-pt` without observer forwarding. The two are tracked
134
+ separately: a move that returns `None` (e.g. a `PairSwap` on a
135
+ single-species sublattice, a `MultiPairSwap` on a sublattice with
136
+ fewer than `k` of one species, an `IndexSetSwap` whose drawn pair
137
+ already holds identical occupations) increments the null counter rather
138
+ than the rejection counter, so `null_rate` distinguishes a
139
+ structurally-infeasible move (`null_rate ≈ 1`) from a
140
+ low-temperature trapped chain (`acceptance_rate ≈ 0`,
141
+ `null_rate ≈ 0`).
142
+
143
+ For multiprocess runs, `CustomCanonicalEnsemble` and every `Move`
144
+ subclass must be importable by fully qualified name in spawn workers
145
+ (i.e. defined in `.py` module files, not in `__main__` or notebook
146
+ cells). `mchammer-pt`'s `ProcessPool` rejects interactive-`__main__`
147
+ and function-local classes up-front.
148
+
149
+ ## Use with Wang-Landau
150
+
151
+ `CustomWangLandauEnsemble` accepts the same `moves` list as
152
+ `CustomCanonicalEnsemble` and forwards all other parameters to
153
+ `WangLandauEnsemble`:
154
+
155
+ ```python
156
+ from mchammer.calculators import ClusterExpansionCalculator
157
+ from mchammer_moves import CustomWangLandauEnsemble, PairSwap
158
+
159
+ calc = ClusterExpansionCalculator(structure, ce)
160
+
161
+ mc = CustomWangLandauEnsemble(
162
+ structure=structure,
163
+ calculator=calc,
164
+ energy_spacing=0.1,
165
+ moves=[
166
+ (PairSwap(sublattice_index=0), 1.0),
167
+ ],
168
+ energy_limit_left=-100.0,
169
+ energy_limit_right=-90.0,
170
+ )
171
+ mc.run(1_000_000)
172
+
173
+ print(mc.acceptance_rates())
174
+ print(mc.rejection_breakdown())
175
+ ```
176
+
177
+ Per-move acceptance, null, window-rejection, and WL-rejection rates are
178
+ recorded into the `WangLandauDataContainer` at every
179
+ `ensemble_data_write_interval` as `<move>_acceptance_rate`,
180
+ `<move>_null_rate`, `<move>_window_rejection_rate`, and
181
+ `<move>_wl_rejection_rate` columns. The `rejection_breakdown()` method
182
+ provides cumulative window-vs-WL rejection counts for interactive use.
183
+
184
+ `<move>_acceptance_rate` and `<move>_null_rate` use total proposals
185
+ (accepted + rejected + null) as the denominator. `<move>_window_rejection_rate`
186
+ and `<move>_wl_rejection_rate` use classified in-window rejections as the
187
+ denominator — they do not share a denominator with the first two columns and
188
+ do not sum with them to any fixed value.
189
+
190
+ Rejection classification is only performed once the walker has reached
191
+ the energy window. Pre-window search-phase rejections are counted in the
192
+ aggregate `MoveStats.rejected` counter but not broken down further.
193
+
194
+ ## Constructing cycles for `CyclicShift`
195
+
196
+ `CyclicShift` expects a list of *cycles*, where each cycle is a list of
197
+ site indices in the order along which species are to be shifted. Cycles
198
+ may have any length and may differ in length from one another; the move
199
+ treats each cycle as periodic in itself (the last site wraps to the
200
+ first). The supplied indices are opaque labels — there is no requirement
201
+ that they correspond to physically collinear sites.
202
+
203
+ The package contains no system-specific geometry. Cycle construction is
204
+ the caller's responsibility. The recipe for a typical anion-ordered
205
+ ReO3-type supercell, where each cycle corresponds to a one-dimensional
206
+ chain of anion sites, is:
207
+
208
+ 1. Identify a single-axis chain of anion sites — for example, all sites of
209
+ the form `(i, 0, 0), (i, 0, 1), …, (i, 0, N-1)` along the *z* axis at
210
+ `(x=i, y=0)` — and list their flat site indices in geometric order.
211
+ 2. Repeat for each starting `(x, y)` to obtain the full set of *z*-cycles.
212
+ 3. Repeat the procedure for *x*-cycles and *y*-cycles if your problem has
213
+ chain ordering along multiple axes.
214
+ 4. Pass the combined list to `CyclicShift(cycles=...)`.
215
+
216
+ For NbO2F at 6×6×6, the relevant cycles are anion chains along each cubic
217
+ axis (108 cycles per axis, 324 cycles total). See the integration script
218
+ in the `data_NbO2F` project for a concrete construction.
219
+
220
+ ## Detailed balance
221
+
222
+ All built-in moves have proposal probabilities that depend only on lattice
223
+ geometry and composition, not on the current configuration:
224
+
225
+ - `PairSwap`: at fixed canonical composition, the number of distinct-species
226
+ pairs on a sublattice is composition-invariant, so the probability of
227
+ selecting any specific pair is symmetric in the forward and reverse
228
+ directions.
229
+ - `MultiPairSwap`: each pair is drawn by picking site 1 uniformly from
230
+ the non-used sublattice sites and site 2 uniformly from the non-used
231
+ sites of differing species. Summed over the `k!` orderings of the
232
+ same site-disjoint pair-set, the forward and reverse proposal
233
+ probabilities are equal: composition is invariant under any valid
234
+ swap, and the dependence on already-used sites cancels by symmetry
235
+ between the two directions.
236
+ - `CyclicShift`: a cycle and direction are chosen uniformly at random.
237
+ The reverse of a `+1` shift along cycle *c* is a `-1` shift along the
238
+ same cycle, with the same selection probability.
239
+ - `CyclicReflection`: a cycle and integer pivot are chosen uniformly
240
+ at random. Cyclic reflection is an involution, so the reverse of a
241
+ reflection along `(c, p)` is the same reflection along `(c, p)`,
242
+ with the same selection probability.
243
+ - `IndexSetSwap`: an unordered pair of index sets is drawn uniformly
244
+ from `C(N, 2)` distinct pairs. Selection probability depends only
245
+ on the fixed list of index sets, not on the configuration, so
246
+ `P(A → B) = P(B → A)` directly. The optional
247
+ `require_matching_composition` filter (off by default) does not
248
+ break this: swapping any pair only exchanges the two groups'
249
+ contents, so the multiset of compositions held across the groups
250
+ is invariant under the move, and a pair filtered out in one
251
+ direction is also filtered out in the other.
252
+ - `SitePermutation`: an operation is drawn uniformly from the fixed
253
+ list, then applied forward or inverted, each with probability one
254
+ half. The applied-permutation multiset is closed under inversion with
255
+ equal weights, so `P(A → B) = P(B → A)` for any permutation.
256
+
257
+ Standard Metropolis acceptance therefore satisfies detailed balance for any
258
+ weighted combination of these moves. A symmetry test that empirically
259
+ verifies this property is provided in the test suite for each move and
260
+ should be the first thing you run when adding a new move.
261
+
262
+ For Wang-Landau sampling, `CustomWangLandauEnsemble` replaces the
263
+ Metropolis criterion with the WL entropy-based acceptance condition
264
+ inherited from `WangLandauEnsemble`. The symmetric-proposal property
265
+ of each move still holds, so the WL algorithm's convergence guarantees
266
+ are preserved for any weighted combination of the built-in moves.
267
+
268
+ ## Running tests
269
+
270
+ ```bash
271
+ pip install -e ".[dev]"
272
+ pytest -q
273
+ ```
@@ -0,0 +1,247 @@
1
+ # mchammer-moves
2
+
3
+ Custom Monte Carlo trial moves for [icet/mchammer](https://icet.materialsmodeling.org/).
4
+ The `Move` base class defines a sampler-agnostic proposal contract; ensemble
5
+ adapters consume moves and handle acceptance, bookkeeping, and data-container
6
+ integration for canonical and Wang-Landau sampling, without modification of the
7
+ mchammer source or downstream wrappers such as `mchammer-pt`.
8
+
9
+ The package provides:
10
+
11
+ - a `Move` abstract base class for user-defined trial moves;
12
+ - five built-in moves:
13
+ - `PairSwap` — the standard canonical two-site swap;
14
+ - `MultiPairSwap` — `k` site-disjoint pair swaps applied as one
15
+ atomic proposal; useful when single-pair swaps are kinetically
16
+ blocked between adjacent minima in deep basins;
17
+ - `CyclicShift` — single-step shift of the species pattern along a
18
+ user-supplied index cycle, with periodic boundaries within the
19
+ cycle; useful for row or ring translations on chain-like or
20
+ ring-like sublattices;
21
+ - `CyclicReflection` — long-range reflection of the species pattern
22
+ along an index cycle around a randomly-chosen pivot; complements
23
+ `CyclicShift`'s nearest-neighbour shifts by enabling species to
24
+ hop across a chain in a single accepted move;
25
+ - `IndexSetSwap` — swaps occupations between two equal-length index
26
+ sets drawn uniformly from a user-supplied list of groups; a
27
+ generic primitive for chain-, motif-, or layer-swap moves;
28
+ - `SitePermutation` — applies a caller-supplied permutation of site
29
+ occupations, drawn uniformly from a list of operations, with an
30
+ unconditional forward/inverse direction draw; covers reflections
31
+ (e.g. across a `<100>` plane), point inversion, and proper or
32
+ improper rotations of any order;
33
+ - `CustomCanonicalEnsemble`, a drop-in replacement for
34
+ `mchammer.ensembles.CanonicalEnsemble` that draws moves from a
35
+ user-supplied weighted list and tracks per-move acceptance;
36
+ - `CustomWangLandauEnsemble`, a drop-in replacement for
37
+ `mchammer.ensembles.WangLandauEnsemble` with the same weighted-move
38
+ dispatch, plus per-move window-vs-WL rejection classification;
39
+ - `MoveDispatcher`, the shared weighted-selection and per-move
40
+ bookkeeping engine used by both ensemble adapters.
41
+
42
+ Installation (editable):
43
+
44
+ ```bash
45
+ pip install -e .
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ `structure`, `ce`, and `cycles` below are placeholders for your atoms
51
+ object, cluster expansion, and chain definitions respectively; the
52
+ package contains no system-specific geometry, so you supply them
53
+ yourself.
54
+
55
+ ```python
56
+ from mchammer.calculators import ClusterExpansionCalculator
57
+ from mchammer_moves import CustomCanonicalEnsemble, CyclicShift, PairSwap
58
+
59
+ calc = ClusterExpansionCalculator(structure, ce)
60
+
61
+ ensemble = CustomCanonicalEnsemble(
62
+ structure=structure,
63
+ calculator=calc,
64
+ temperature=600.0,
65
+ moves=[
66
+ (PairSwap(sublattice_index=0), 1.0),
67
+ (CyclicShift(cycles=cycles), 0.05),
68
+ ],
69
+ )
70
+ ensemble.run(10_000)
71
+
72
+ print(ensemble.acceptance_rates())
73
+ ```
74
+
75
+ ## Use with `mchammer-pt`
76
+
77
+ `mchammer-pt` (v0.2+) accepts a custom ensemble class via its native
78
+ `ensemble_cls=` parameter, with constructor arguments forwarded via
79
+ `ensemble_kwargs=`:
80
+
81
+ ```python
82
+ from mchammer_pt import CanonicalParallelTempering
83
+ from mchammer_moves import CustomCanonicalEnsemble, CyclicShift, PairSwap
84
+
85
+ with CanonicalParallelTempering.process_pool(
86
+ cluster_expansion=ce,
87
+ atoms=initial_structure,
88
+ temperatures=temperatures,
89
+ block_size=block_size,
90
+ random_seed=42,
91
+ ensemble_cls=CustomCanonicalEnsemble,
92
+ ensemble_kwargs={
93
+ "moves": [
94
+ (PairSwap(sublattice_index=anion_sl), 1.0),
95
+ (CyclicShift(cycles=cycles), 0.05),
96
+ ],
97
+ },
98
+ ) as pt:
99
+ history = pt.run(n_cycles=N_CYCLES)
100
+ ```
101
+
102
+ Per-move acceptance and null-proposal rates are recorded into each
103
+ replica's `mchammer.BaseDataContainer` at every
104
+ `ensemble_data_write_interval` as `<move>_acceptance_rate` and
105
+ `<move>_null_rate` columns, so they survive the `ProcessPool`
106
+ boundary and are recoverable from the HDF5 bundle written by
107
+ `mchammer-pt` without observer forwarding. The two are tracked
108
+ separately: a move that returns `None` (e.g. a `PairSwap` on a
109
+ single-species sublattice, a `MultiPairSwap` on a sublattice with
110
+ fewer than `k` of one species, an `IndexSetSwap` whose drawn pair
111
+ already holds identical occupations) increments the null counter rather
112
+ than the rejection counter, so `null_rate` distinguishes a
113
+ structurally-infeasible move (`null_rate ≈ 1`) from a
114
+ low-temperature trapped chain (`acceptance_rate ≈ 0`,
115
+ `null_rate ≈ 0`).
116
+
117
+ For multiprocess runs, `CustomCanonicalEnsemble` and every `Move`
118
+ subclass must be importable by fully qualified name in spawn workers
119
+ (i.e. defined in `.py` module files, not in `__main__` or notebook
120
+ cells). `mchammer-pt`'s `ProcessPool` rejects interactive-`__main__`
121
+ and function-local classes up-front.
122
+
123
+ ## Use with Wang-Landau
124
+
125
+ `CustomWangLandauEnsemble` accepts the same `moves` list as
126
+ `CustomCanonicalEnsemble` and forwards all other parameters to
127
+ `WangLandauEnsemble`:
128
+
129
+ ```python
130
+ from mchammer.calculators import ClusterExpansionCalculator
131
+ from mchammer_moves import CustomWangLandauEnsemble, PairSwap
132
+
133
+ calc = ClusterExpansionCalculator(structure, ce)
134
+
135
+ mc = CustomWangLandauEnsemble(
136
+ structure=structure,
137
+ calculator=calc,
138
+ energy_spacing=0.1,
139
+ moves=[
140
+ (PairSwap(sublattice_index=0), 1.0),
141
+ ],
142
+ energy_limit_left=-100.0,
143
+ energy_limit_right=-90.0,
144
+ )
145
+ mc.run(1_000_000)
146
+
147
+ print(mc.acceptance_rates())
148
+ print(mc.rejection_breakdown())
149
+ ```
150
+
151
+ Per-move acceptance, null, window-rejection, and WL-rejection rates are
152
+ recorded into the `WangLandauDataContainer` at every
153
+ `ensemble_data_write_interval` as `<move>_acceptance_rate`,
154
+ `<move>_null_rate`, `<move>_window_rejection_rate`, and
155
+ `<move>_wl_rejection_rate` columns. The `rejection_breakdown()` method
156
+ provides cumulative window-vs-WL rejection counts for interactive use.
157
+
158
+ `<move>_acceptance_rate` and `<move>_null_rate` use total proposals
159
+ (accepted + rejected + null) as the denominator. `<move>_window_rejection_rate`
160
+ and `<move>_wl_rejection_rate` use classified in-window rejections as the
161
+ denominator — they do not share a denominator with the first two columns and
162
+ do not sum with them to any fixed value.
163
+
164
+ Rejection classification is only performed once the walker has reached
165
+ the energy window. Pre-window search-phase rejections are counted in the
166
+ aggregate `MoveStats.rejected` counter but not broken down further.
167
+
168
+ ## Constructing cycles for `CyclicShift`
169
+
170
+ `CyclicShift` expects a list of *cycles*, where each cycle is a list of
171
+ site indices in the order along which species are to be shifted. Cycles
172
+ may have any length and may differ in length from one another; the move
173
+ treats each cycle as periodic in itself (the last site wraps to the
174
+ first). The supplied indices are opaque labels — there is no requirement
175
+ that they correspond to physically collinear sites.
176
+
177
+ The package contains no system-specific geometry. Cycle construction is
178
+ the caller's responsibility. The recipe for a typical anion-ordered
179
+ ReO3-type supercell, where each cycle corresponds to a one-dimensional
180
+ chain of anion sites, is:
181
+
182
+ 1. Identify a single-axis chain of anion sites — for example, all sites of
183
+ the form `(i, 0, 0), (i, 0, 1), …, (i, 0, N-1)` along the *z* axis at
184
+ `(x=i, y=0)` — and list their flat site indices in geometric order.
185
+ 2. Repeat for each starting `(x, y)` to obtain the full set of *z*-cycles.
186
+ 3. Repeat the procedure for *x*-cycles and *y*-cycles if your problem has
187
+ chain ordering along multiple axes.
188
+ 4. Pass the combined list to `CyclicShift(cycles=...)`.
189
+
190
+ For NbO2F at 6×6×6, the relevant cycles are anion chains along each cubic
191
+ axis (108 cycles per axis, 324 cycles total). See the integration script
192
+ in the `data_NbO2F` project for a concrete construction.
193
+
194
+ ## Detailed balance
195
+
196
+ All built-in moves have proposal probabilities that depend only on lattice
197
+ geometry and composition, not on the current configuration:
198
+
199
+ - `PairSwap`: at fixed canonical composition, the number of distinct-species
200
+ pairs on a sublattice is composition-invariant, so the probability of
201
+ selecting any specific pair is symmetric in the forward and reverse
202
+ directions.
203
+ - `MultiPairSwap`: each pair is drawn by picking site 1 uniformly from
204
+ the non-used sublattice sites and site 2 uniformly from the non-used
205
+ sites of differing species. Summed over the `k!` orderings of the
206
+ same site-disjoint pair-set, the forward and reverse proposal
207
+ probabilities are equal: composition is invariant under any valid
208
+ swap, and the dependence on already-used sites cancels by symmetry
209
+ between the two directions.
210
+ - `CyclicShift`: a cycle and direction are chosen uniformly at random.
211
+ The reverse of a `+1` shift along cycle *c* is a `-1` shift along the
212
+ same cycle, with the same selection probability.
213
+ - `CyclicReflection`: a cycle and integer pivot are chosen uniformly
214
+ at random. Cyclic reflection is an involution, so the reverse of a
215
+ reflection along `(c, p)` is the same reflection along `(c, p)`,
216
+ with the same selection probability.
217
+ - `IndexSetSwap`: an unordered pair of index sets is drawn uniformly
218
+ from `C(N, 2)` distinct pairs. Selection probability depends only
219
+ on the fixed list of index sets, not on the configuration, so
220
+ `P(A → B) = P(B → A)` directly. The optional
221
+ `require_matching_composition` filter (off by default) does not
222
+ break this: swapping any pair only exchanges the two groups'
223
+ contents, so the multiset of compositions held across the groups
224
+ is invariant under the move, and a pair filtered out in one
225
+ direction is also filtered out in the other.
226
+ - `SitePermutation`: an operation is drawn uniformly from the fixed
227
+ list, then applied forward or inverted, each with probability one
228
+ half. The applied-permutation multiset is closed under inversion with
229
+ equal weights, so `P(A → B) = P(B → A)` for any permutation.
230
+
231
+ Standard Metropolis acceptance therefore satisfies detailed balance for any
232
+ weighted combination of these moves. A symmetry test that empirically
233
+ verifies this property is provided in the test suite for each move and
234
+ should be the first thing you run when adding a new move.
235
+
236
+ For Wang-Landau sampling, `CustomWangLandauEnsemble` replaces the
237
+ Metropolis criterion with the WL entropy-based acceptance condition
238
+ inherited from `WangLandauEnsemble`. The symmetric-proposal property
239
+ of each move still holds, so the WL algorithm's convergence guarantees
240
+ are preserved for any weighted combination of the built-in moves.
241
+
242
+ ## Running tests
243
+
244
+ ```bash
245
+ pip install -e ".[dev]"
246
+ pytest -q
247
+ ```
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mchammer-moves"
7
+ version = "0.6.0"
8
+ description = "Custom Monte Carlo moves for icet/mchammer."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Benjamin J. Morgan", email = "b.j.morgan@bath.ac.uk" }]
14
+ dependencies = [
15
+ "icet>=3.2",
16
+ "numpy",
17
+ "ase",
18
+ ]
19
+ classifiers = [
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Operating System :: OS Independent",
26
+ "Topic :: Scientific/Engineering",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7",
32
+ "scipy",
33
+ "mypy",
34
+ "ruff",
35
+ ]
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+
43
+ [tool.mypy]
44
+ python_version = "3.11"
45
+ packages = ["mchammer_moves"]
46
+ strict = true
47
+ warn_return_any = false
48
+
49
+ # `mchammer` (icet) ships without a `py.typed` marker, so mypy
50
+ # treats it as an untyped third-party dependency. The exact error
51
+ # code mypy assigns depends on how the package is installed
52
+ # (`import-untyped` for site-packages installs, `import-not-found`
53
+ # for editable / source-tree installs); ignoring missing imports
54
+ # here covers both and removes the need for per-import
55
+ # `# type: ignore` suppressions that drift across mypy versions.
56
+ [[tool.mypy.overrides]]
57
+ module = ["mchammer.*"]
58
+ ignore_missing_imports = true
59
+
60
+ [tool.ruff]
61
+ target-version = "py311"
62
+ line-length = 88
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "W", "I", "B", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,64 @@
1
+ """Custom Monte Carlo moves for icet/mchammer.
2
+
3
+ Provides a small framework for plugging user-defined trial moves into
4
+ mchammer sampling. The :class:`Move` base class defines a
5
+ sampler-agnostic proposal contract; ensemble adapters consume moves
6
+ and handle acceptance, bookkeeping, and data-container integration:
7
+
8
+ * :class:`Move` — abstract base class for trial moves.
9
+ * :class:`PairSwap` — standard two-site canonical swap on a sublattice.
10
+ * :class:`MultiPairSwap` — ``k`` site-disjoint pair swaps applied as
11
+ one atomic proposal, for larger jumps in configuration space than a
12
+ single pair swap can provide.
13
+ * :class:`CyclicShift` — single-step cyclic shift of species along
14
+ one of a user-supplied set of index cycles. Useful for row /
15
+ ring translations on lattice sublattices with chain-like or
16
+ ring-like topology when standard single-site swaps are
17
+ kinetically blocked.
18
+ * :class:`CyclicReflection` — long-range reflection of the species
19
+ pattern along an index cycle around a randomly-chosen pivot;
20
+ complement to ``CyclicShift``'s nearest-neighbour shifts.
21
+ * :class:`IndexSetSwap` — generic group-permutation primitive that
22
+ swaps occupations between two equal-length index sets drawn
23
+ uniformly from a user-supplied list.
24
+ * :class:`SitePermutation` — applies a caller-supplied permutation of
25
+ site occupations (reflections, point inversion, rotations) drawn
26
+ uniformly from a list, applying each operation or its inverse with
27
+ equal probability so detailed balance holds for any permutation.
28
+ * :class:`MoveDispatcher` — weighted move selection and per-move
29
+ bookkeeping, used internally by ensemble adapters.
30
+ * :class:`CustomCanonicalEnsemble` — drop-in replacement for
31
+ :class:`mchammer.ensembles.CanonicalEnsemble` that draws moves from a
32
+ user-supplied weighted list and tracks per-move acceptance.
33
+ * :class:`CustomWangLandauEnsemble` — drop-in replacement for
34
+ :class:`mchammer.ensembles.WangLandauEnsemble` with the same
35
+ weighted-move dispatch, plus window-vs-WL rejection classification.
36
+ """
37
+
38
+ from mchammer_moves.ensemble import (
39
+ CustomCanonicalEnsemble,
40
+ CustomWangLandauEnsemble,
41
+ MoveDispatcher,
42
+ MoveStats,
43
+ )
44
+ from mchammer_moves.moves.base import Move
45
+ from mchammer_moves.moves.cyclic_reflection import CyclicReflection
46
+ from mchammer_moves.moves.cyclic_shift import CyclicShift
47
+ from mchammer_moves.moves.index_set_swap import IndexSetSwap
48
+ from mchammer_moves.moves.multi_pair_swap import MultiPairSwap
49
+ from mchammer_moves.moves.pair_swap import PairSwap
50
+ from mchammer_moves.moves.site_permutation import SitePermutation
51
+
52
+ __all__ = [
53
+ "CustomCanonicalEnsemble",
54
+ "CustomWangLandauEnsemble",
55
+ "CyclicReflection",
56
+ "CyclicShift",
57
+ "IndexSetSwap",
58
+ "Move",
59
+ "MoveDispatcher",
60
+ "MoveStats",
61
+ "MultiPairSwap",
62
+ "PairSwap",
63
+ "SitePermutation",
64
+ ]