diff options
| author | Sebastiano Tronto <sebastiano@tronto.net> | 2023-12-04 16:16:01 +0100 |
|---|---|---|
| committer | Sebastiano Tronto <sebastiano@tronto.net> | 2023-12-04 16:16:01 +0100 |
| commit | 2b30bffc6cfef47c785c9bd3d5aa365750c0d533 (patch) | |
| tree | 338deb63f3f6d045c7190c47580424d7ce7b7265 /src/speedcubing/coordinates | |
| parent | af6193e3fca6e5ec5e9442af601a026ea328b19b (diff) | |
| download | sebastiano.tronto.net-2b30bffc6cfef47c785c9bd3d5aa365750c0d533.tar.gz sebastiano.tronto.net-2b30bffc6cfef47c785c9bd3d5aa365750c0d533.zip | |
Added coordinate writeup
Diffstat (limited to 'src/speedcubing/coordinates')
| -rw-r--r-- | src/speedcubing/coordinates/coordinates.md | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/src/speedcubing/coordinates/coordinates.md b/src/speedcubing/coordinates/coordinates.md new file mode 100644 index 0000000..5640bca --- /dev/null +++ b/src/speedcubing/coordinates/coordinates.md | |||
| @@ -0,0 +1,280 @@ | |||
| 1 | # Cube coordinates | ||
| 2 | |||
| 3 | In this page I describe the way I implemented *coordinates*, in the | ||
| 4 | sense of [Cube Explorer](http://kociemba.org/cube.htm), in one of the | ||
| 5 | versions of [Nissy](https://nissy.tronto.net). | ||
| 6 | |||
| 7 | Unfortunately, this implementation got lost between rewrites and is | ||
| 8 | currently not included in any working version of Nissy or other projects, | ||
| 9 | so it this page is of theoretical interest only. Some code is available | ||
| 10 | in the [nissy-nx repository](https://git.tronto.net/nissy-nx), but it | ||
| 11 | is currently broken (what is described in this page works, though). | ||
| 12 | |||
| 13 | ## What are cube coordinates? | ||
| 14 | |||
| 15 | A *cube coordinate* is a way of associating to any legal position | ||
| 16 | of the Rubik's cube an integer in a certain range [0..n-1]. For | ||
| 17 | example, any way of encoding the permutation of the corners as a | ||
| 18 | number from 0 to 8!-1 is a valid coordinate; another example is | ||
| 19 | an encoding of the orientation of the edges with respect to a given | ||
| 20 | axis as a number in [0..2^11]. | ||
| 21 | |||
| 22 | In other words, a coordinate is a function from the set of legal | ||
| 23 | configurations to [0..n-1]. Coordinates need not be surjective, | ||
| 24 | though it is more convenient if they are, and in my examples they | ||
| 25 | will be. | ||
| 26 | |||
| 27 | To work with cube coordinates, we need to implement the following: | ||
| 28 | |||
| 29 | * An integer `n`, the maximum value of a coordinate +1. | ||
| 30 | * A function `index()` that takes a cube as input and returns an integer | ||
| 31 | in the range [0..n-1] as output. | ||
| 32 | * A function `to_cube()` that takes an integer in [0..n-1] as input and | ||
| 33 | returns a cube `c` such that `index(to_cube(x)) == x` for every x in | ||
| 34 | [0..n-1]. In other words, `to_cube()` is a *section* of `index()`. | ||
| 35 | In practice, it is not necessary that the output of `to_cube()` is a | ||
| 36 | valid cube object. | ||
| 37 | * A function `move()` that takes as input a coordinate, a move m and an | ||
| 38 | integer x in [0..n-1] and returns an integer y in [0..n-1] such | ||
| 39 | that `y == index(m(to_cube(x)))` (here m(c) is the cube c moved by m). | ||
| 40 | * If applicable, a `transform()` function that applies a transformation | ||
| 41 | in a similar way as `move()` applies a move. For coordinates this is | ||
| 42 | not possible, because they do not capture enough of the cube state. | ||
| 43 | For some coordinates, only some transformations are possible. For | ||
| 44 | example, for the edge orientation coordinate only the transformations | ||
| 45 | that fix the axis with respect to which the edge orientation is defined | ||
| 46 | can be applied. | ||
| 47 | |||
| 48 | For the implementation, see | ||
| 49 | [coord.c](https://git.tronto.net/nissy-nx/file/src/coord.c.html) | ||
| 50 | and | ||
| 51 | [coord.h](https://git.tronto.net/nissy-nx/file/src/coord.h.html), | ||
| 52 | as well as the definition of | ||
| 53 | [coordinate](https://git.tronto.net/nissy-nx/file/src/cubetypes.h.html#l181) | ||
| 54 | in cubetypes.h. | ||
| 55 | |||
| 56 | ## Coordinate types | ||
| 57 | |||
| 58 | In my work I have defined four types of coordinates (but I actually use | ||
| 59 | only three of them). | ||
| 60 | |||
| 61 | ### Basic coordinates | ||
| 62 | |||
| 63 | Basic coordinates are the simplest kind, and they are completely defined | ||
| 64 | by the integer `n` and the functions `index()` and `to_cube()` defined | ||
| 65 | above. This is the type of coordinate that "does not exist" in the code, | ||
| 66 | because they are just a special case of *composite coordinates*. | ||
| 67 | |||
| 68 | ### Composite coordinates (COMP_COORD) | ||
| 69 | |||
| 70 | Composite coordinates are, like the name says, a composition of many basic | ||
| 71 | coordinates. They are given by a list of basic coordinates (n_1, index_1(), | ||
| 72 | to_cube_1()), ..., (n_k, index_k(), to_cube_k()). The value of such a | ||
| 73 | composite coordinate on cube c is computed as | ||
| 74 | index_1(c) + n_1 * (index_2(c) + n_2 * (...)). | ||
| 75 | |||
| 76 | ### Symmetric coordinates (SYM_COORD) | ||
| 77 | |||
| 78 | A symmetric coordinate consists of a basic coordinate reduced by symmetry. | ||
| 79 | Symmetric coordinates must be initialized from a given set of *cube | ||
| 80 | transformations* before they can be used. The initialization step produces | ||
| 81 | the following data: | ||
| 82 | |||
| 83 | * A table that associates every possible value of the basic coordinate | ||
| 84 | with its class in the symmetric coordinate. Computing the value of the | ||
| 85 | symmetric coordinate will then amount to computing the basic coordinate | ||
| 86 | via `index()` and looking up the value in this table. | ||
| 87 | * A table that associates every possible value of the basic coordinate | ||
| 88 | with a fixed representative for its class. | ||
| 89 | * A table that associates every possible value of the basic coordinate | ||
| 90 | the cube transformation that brings it to its representative. | ||
| 91 | * A table that associates every possible value of the basic coordinate | ||
| 92 | with the list of cube transformations that do not affect it, that is | ||
| 93 | a list of *self-symmetries*. This will be useful when computing the | ||
| 94 | pruning table associated to this coordinate. | ||
| 95 | |||
| 96 | ### Symmetric-composite coordinates (SYMCOMP_COORD) | ||
| 97 | |||
| 98 | A symmetric-composite coordinate is based on two other coordinates, a | ||
| 99 | symmetric coordinate and a composite coordinate. To compute the value | ||
| 100 | of a symmetric-composite coordinate one must compute the symmetric | ||
| 101 | coordinate value first, then transform the cube using the transformation | ||
| 102 | that brings the basic coordinate associated to the symmetric coordinate | ||
| 103 | to its representative in order to compute the correct value for the | ||
| 104 | composite coordinate, and finally combine the two. | ||
| 105 | |||
| 106 | More precisely and with less tong-twisting, for a given cube c one must | ||
| 107 | take the following steps: | ||
| 108 | |||
| 109 | * Compute the value x_s of the symmetric coordinate at c, the value | ||
| 110 | x_b of the *basic* coordinate associated with the symmetric coordinate | ||
| 111 | and the value x_c of the composite coordinate. | ||
| 112 | * Read from the table the transformation t that brings x_b to its | ||
| 113 | representative. | ||
| 114 | * Apply t to the composite coordinate value x_c to obtain x_t. | ||
| 115 | * Compute the value x_s * n_c + x_t, where n_c is the maximum value +1 | ||
| 116 | of the composite coordinate. | ||
| 117 | |||
| 118 | ## Moving coordinates | ||
| 119 | |||
| 120 | A move is applied to a coordinate by lookup into a transition table, | ||
| 121 | pretty much as explained in Jaap's | ||
| 122 | [computer puzzling page](https://www.jaapsch.net/puzzles/compcube.htm#trans). | ||
| 123 | These transition tables are initialized once and for all at the beginning, | ||
| 124 | and in my implementation I actually saved them to a file to speed up | ||
| 125 | subsequent runs. | ||
| 126 | |||
| 127 | Some extra care has to be taken in dealing with symmetries. | ||
| 128 | |||
| 129 | ### Basic and composite coordinates | ||
| 130 | |||
| 131 | This case is quite simple: the transition table with something along these | ||
| 132 | lines: | ||
| 133 | |||
| 134 | ``` | ||
| 135 | for i in [0..n-1] | ||
| 136 | for m in Moves | ||
| 137 | move_table[m][i] = index(m(to_cube(i))) | ||
| 138 | ``` | ||
| 139 | |||
| 140 | ### Symmetric coordinates | ||
| 141 | |||
| 142 | As mentioned above, this case and the next are tricky, because we need | ||
| 143 | to keep track of which transformation has to be applied to get from the | ||
| 144 | representative of the symmetry class to our actual cube. | ||
| 145 | |||
| 146 | You can imagine a move applied to symmetric coordinate as if we just | ||
| 147 | move between different representatives. The actual cube can be recovered | ||
| 148 | from this by keeping track of an *offset transformation*, and updating | ||
| 149 | it after every move. | ||
| 150 | |||
| 151 | The necessary tables can be generated with something like this: | ||
| 152 | |||
| 153 | ``` | ||
| 154 | for i in [0..n-1] | ||
| 155 | for m in Moves | ||
| 156 | j = m(rep of i) /* Apply as basic coordinate */ | ||
| 157 | move_table[m][i] = class of j | ||
| 158 | offset_table[m][i] = transformation bringing j to its rep | ||
| 159 | ``` | ||
| 160 | |||
| 161 | ### Symmetric-composite coordinates | ||
| 162 | |||
| 163 | A symmetric-composite coordinate requires some extra work, but the hard | ||
| 164 | part is already handled by its base symmetric coordinate. | ||
| 165 | |||
| 166 | To move a symmetric-composite coordinate, first we recover the value s | ||
| 167 | of its symmetric coordinate's and the value c of its composite coordinate | ||
| 168 | by taking the quotient and the remainder of division by the maximum value | ||
| 169 | of the composite coordinate. | ||
| 170 | |||
| 171 | Then we apply the move to the values s and c using their respective | ||
| 172 | transition tables. The offset transformation is taken from the symmetric | ||
| 173 | coordinate, and apply it to the result of the move on the composite coordinate. | ||
| 174 | |||
| 175 | This may be summarized with the following pseudo-code: | ||
| 176 | |||
| 177 | ``` | ||
| 178 | s = ind / M /* M is the maximum value +1 of the composite coordinate */ | ||
| 179 | c = ind % M | ||
| 180 | new_s = move_table_s[m][s] /* Using the table for the symmetric coordinate */ | ||
| 181 | new_c = move_table_c[m][c] /* Using the table for the composite coordinate */ | ||
| 182 | offset = offset_table[m][s] | ||
| 183 | new_c = offset(new_c) /* See next section for transformations */ | ||
| 184 | |||
| 185 | return (new_s * M + new_c, offset) | ||
| 186 | ``` | ||
| 187 | |||
| 188 | ## Transforming coordinates | ||
| 189 | |||
| 190 | When we talk about *cube transformations*, we are talking about | ||
| 191 | conjugating a cube by a rotation, possibly combined with a mirroring | ||
| 192 | along a fixed axis. More precisely, transforming a cube c is done with | ||
| 193 | the following steps: | ||
| 194 | |||
| 195 | * Start from a solved cube | ||
| 196 | * (optional) Mirror it left-right | ||
| 197 | * Apply the required rotation | ||
| 198 | * Apply c (as a permutation / move sequence) to the rotated solved cube | ||
| 199 | * Apply the inverse of the rotation | ||
| 200 | * (optional) Mirror it left-right | ||
| 201 | |||
| 202 | When working with coordinates, transformations are applied pretty much | ||
| 203 | in the same way as moves, using a transition table. This time, though, | ||
| 204 | symmetric coordinates are much less of a problem: any transformation | ||
| 205 | on a symmetric coordinate is, by definition, trivial! Of course, this | ||
| 206 | is only true if we limit ourselves to applying transformations that are | ||
| 207 | part of the set used to "reduce" the symmetric coordinate; but we always | ||
| 208 | do this anyway. | ||
| 209 | |||
| 210 | ## Pruning tables | ||
| 211 | |||
| 212 | I'd like to mention pruning tables here, because their computation is | ||
| 213 | quite straightforward when using coordinates. | ||
| 214 | |||
| 215 | Say you want to compute a table that associates every possible value i in | ||
| 216 | [0..n-1] of a coordinate with the minimum amount of moves required to | ||
| 217 | solve any cube c such that `index(c) == i`. For the purpose of this page | ||
| 218 | we assume one can afford to allocate enough bits so that the actual value | ||
| 219 | can be stored. | ||
| 220 | |||
| 221 | With coordinates, one can do the following, without ever going back to | ||
| 222 | the full cube representation: | ||
| 223 | |||
| 224 | ``` | ||
| 225 | set all values of the pruning table to infinity | ||
| 226 | set the value associated with the solved cube to 0 | ||
| 227 | for d in [1..20] | ||
| 228 | for i in [0..n-1] | ||
| 229 | if pruning_table[i] == d-1 | ||
| 230 | for m in Moves | ||
| 231 | old = pruning_table[m(i)] | ||
| 232 | pruning_table[m(i)] = min(d, old) | ||
| 233 | ``` | ||
| 234 | |||
| 235 | This is conceptually very simple, but unfortunately it is not enough. | ||
| 236 | Because of self-symmetries, not every position will be reached in this | ||
| 237 | way for a symmetric-composite coordinate: there may be more ways to | ||
| 238 | bring the base of the symmetric coordinate to its representative, each | ||
| 239 | having a different effect on the composite coordinate, but we only pick | ||
| 240 | one of them when building the move tables. | ||
| 241 | |||
| 242 | Luckily, this can be solved because we have memorized for every position a | ||
| 243 | list of all the transformations that keep it invariant (see above). Then | ||
| 244 | it is enough to add the following loop at the end of every iteration of | ||
| 245 | the outermost loop above: | ||
| 246 | |||
| 247 | ``` | ||
| 248 | for i in [0..n-1] | ||
| 249 | for t in the set of self-symmetries of i/M | ||
| 250 | old = pruning_table[t(i)] | ||
| 251 | pruning_table[t(i)] = min(d, old) | ||
| 252 | ``` | ||
| 253 | |||
| 254 | The code for this can be found in | ||
| 255 | [pruning.c](https://git.tronto.net/nissy-nx/file/src/pruning.c.html). | ||
| 256 | The use of pthread made it more complicated, so it can be hard to follow | ||
| 257 | without reading this page first. | ||
| 258 | |||
| 259 | ## Why using coordinates? | ||
| 260 | |||
| 261 | Coordinates are necessary to compute pruning tables, which are fundamental | ||
| 262 | when solving a cube using the methods described in Jaap's | ||
| 263 | [computer puzzling page](https://www.jaapsch.net/puzzles/compcube.htm). | ||
| 264 | |||
| 265 | However, using transition tables for moves and transformations is | ||
| 266 | not necessary. It used to be an efficient way to perform moves on a | ||
| 267 | cube, but with newer hardware the trade-offs between memory access and | ||
| 268 | instruction execution are changing. | ||
| 269 | |||
| 270 | One thing that is made particularly cumbersome by using coordinates | ||
| 271 | instead of a full representation of the cube is computing the | ||
| 272 | inverse. While it is possible to compute the inverse somewhat efficiently | ||
| 273 | from a representation of the cube made of a smart selection of coordinate | ||
| 274 | values, it is not necessarily efficient. | ||
| 275 | See [fst.c](https://git.tronto.net/nissy-nx/file/src/fst.c.html#l113) | ||
| 276 | for an implementation. | ||
| 277 | |||
| 278 | For these reasons, I am not using the coordinate approach described here | ||
| 279 | for my new work-in-progress solver (temporarily named | ||
| 280 | [h48](https://git.tronto.net/h48)). | ||
