1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Canonical-form predicates and finalizers for the rat enumeration.
//!
//! The DFS produces every cyclic rotation of every closed polygon by
//! default; canonical-form bookkeeping collapses each equivalence
//! class to one representative. Two equivalence flavors are
//! supported:
//!
//! - **Rotation-canonical**: one rep per cyclic rotation class.
//! Prefix prune is sound + complete (lex-min check on the walk
//! prefix is monotone: a rotation that already lost can never
//! recover). `canonicalize` is the identity.
//!
//! - **Free (full dihedral)**: one rep per cyclic + reflection class.
//! Prefix prune is sound but **incomplete** -- reflection involves
//! the unknown tail of the walk, so we can only check against
//! complement (negated) rotations at the prefix level. The closure-
//! time mapper [`free_canonical`] picks up the slack by mapping
//! every chirality-normalized rotation to its lex-min dihedral
//! form, so both members of every chiral pair hash to the same
//! bucket.
//!
//! # Why the dihedral prefix prune is heuristic
//!
//! For a closed walk `c_1..c_N`, the dihedral group has 2N elements
//! (N rotations + N reflections). For a *prefix* `c_1..c_d`:
//!
//! * Rotation k's prefix is `c_{k+1}..c_{k+d}` -- determined by
//! `c_1..c_d` for positions where `k+i < d`. The comparable region
//! is non-empty for every k > 0, and a verdict there is permanent
//! (no future angles can flip it), so the prefix prune is exact.
//! * Reflection's prefix is `-c_N, -c_{N-1}, ..., -c_{N-d+1}` --
//! every position lies in the unknown future of the current walk.
//! There is no comparable region until N is known.
//!
//! The closure-time `free_canonical` mapping handles what the
//! prefix prune can't see: it maps every closed canonical rotation
//! to the lex-min over rotations AND reversed rotations, so both
//! members of every chiral pair collapse to one rep in the result
//! HashSet.
/// Pair of canonical-check and output-mapping functions that
/// parameterise the DFS. Both rotation-canonical and free-
/// canonical enumeration share the same core walk; they differ only
/// in which pair of functions they supply.
///
/// * `is_canonical` -- prefix prune applied *before* `Snake::add`.
/// Returns `false` when the extended walk cannot be the lex-min
/// rotation (or dihedral image) of any closure it could grow into.
/// * `canonicalize` -- applied to the chirality-normalised canonical
/// rotation at closure, producing the key inserted into the result
/// `HashSet`. For rotation-canonical this is the identity; for
/// free (full dihedral symmetry reduction) it picks the lex-min over rotations and
/// reversed-rotations.
/// Bind `prefix ++ [new]` so that index `i` (for `i < prefix.len() + 1`)
/// resolves to the corresponding walk angle. Out-of-range indices are
/// not handled -- callers must keep accesses within `0..d` where
/// `d = prefix.len() + 1`.
/// For the extended walk `prefix ++ [new]` of length `d`, return
/// `false` if some rotation `k > 0` makes the rotation lex-smaller
/// than the identity within the fully-known comparable region. The
/// caller pairs this with a complementary loop for the free case
/// (see [`is_free_canonical_extended`]).
/// Canonical-rotation prune for the open walk `prefix` virtually
/// extended by one more angle `new`. Lets the caller skip walks
/// that cannot be the lex-min cyclic rotation of any closure they
/// could grow into, *before* paying the cyclotomic intersect cost
/// of `Snake::add`.
///
/// Returns `false` when there's a rotation index `k > 0` such that
/// the rotation `[prefix++new][k..]` is strictly lex-less than
/// `[prefix++new][0..]` within the wrap-free comparable region; that
/// decision is permanent (no future angles can flip it) so the
/// eventual closed polygon's canonical rotation cannot start at
/// position 0 -- pruning is safe.
/// Heuristic dihedral prefix prune. Like [`is_canonical_extended`]
/// but also compares the walk prefix against complement rotations
/// (negated angle sequences). Returns `false` when any complement
/// rotation of the eventual closure has a lex-smaller prefix than
/// the identity walk, indicating the walk is suboptimal under the
/// dihedral group.
///
/// This is sound (never prunes a walk that could produce the
/// free (lex-min) output) but incomplete (does not eliminate all
/// chiral duplicates -- see module-level comment above). At depth 0
/// it prunes all positive first angles (`-a_0 < a_0`), roughly
/// halving the root branching factor.
///
/// # Index bounds
///
/// Complement rotation `k` compares `-a_{k - i}` with `a_i` at
/// positions `i = 0..=k`. Both sides are decided by the known walk
/// only when `0 <= k - i < d` and `0 <= i < d`; for the inner-loop
/// range `i = 0..=k` to stay within the prefix we need `k < d`.
/// Hence the outer loop is `for k in 0..d` (NOT `0..=d`) -- accessing
/// `walk_get(prefix, new, d)` would silently return `new` again for
/// a position whose value is genuinely undetermined, which would
/// over-prune.
/// Correctness layer for the free DFS. Computes the lex-minimum
/// over all cyclic rotations and reversed rotations of `seq`.
///
/// The inputs are already chirality-normalized to CCW by the DFS
/// before this function runs (see `dfs.rs`: a closed rat with
/// `chirality() <= 0` is replaced by its `reversed()`). Under that
/// normalization, two mirror images of the same polygon shape (an
/// enantiomer pair) end up as CCW walks of the original and the
/// mirror. Algebraically these CCW walks differ by SEQUENCE
/// REVERSAL (one polygon walked CCW from vertex V0, vs its mirror
/// walked CCW from V0' = mirror of V0, gives sequences related by
/// reverse). Plain reversal -- not revcomp -- is therefore the
/// right relation here; this reverse (orientation-preserving
/// mirror), as opposed to the revcomp that flips CW<->CCW, is
/// load-bearing -- do not "simplify" it away.
///
/// Both members of an enantiomer pair map to the same value, so the
/// HashSet deduplicates them. Without this step, the output would
/// contain both members of every non-achiral chiral pair.
/// Build the [`CanonicalOps`] pair from the `free` CLI flag.