sebastiano.tronto.net

Source files and build scripts for my personal website
git clone https://git.tronto.net/sebastiano.tronto.net
Download | Log | Files | Refs | README

coordinates.md (11762B)


      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)).