aboutsummaryrefslogtreecommitdiff
path: root/src/speedcubing/coordinates/coordinates.md
blob: 3d1687c01b9e97510b9c4d6135d1f74a9a5c7a75 (plain)
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# Cube coordinates

In this page I describe the way I implemented *coordinates*, in the
sense of [Cube Explorer](http://kociemba.org/cube.htm), in one of the
versions of [Nissy](https://nissy.tronto.net).

Unfortunately, this implementation got lost between rewrites and is
currently not included in any working version of Nissy or other projects,
so it this page is of theoretical interest only. Some code is available
in the [nissy-nx repository](https://git.tronto.net/nissy-nx), but it
is currently broken (what is described in this page works, though).

## What are cube coordinates?

A *cube coordinate* is a way of associating to any legal position
of the Rubik's cube an integer in a certain range [0..n-1]. For
example, any way of encoding the permutation of the corners as a
number from 0 to 8!-1 is a valid coordinate; another example is
an encoding of the orientation of the edges with respect to a given
axis as a number in [0..2^11].

In other words, a coordinate is a function from the set of legal
configurations to [0..n-1]. Coordinates need not be surjective,
though it is more convenient if they are, and in my examples they
will be.

To work with cube coordinates, we need to implement the following:

* An integer `n`, the maximum value of a coordinate +1.
* A function `index()` that takes a cube as input and returns an integer
  in the range [0..n-1] as output.
* A function `to_cube()` that takes an integer in [0..n-1] as input and
  returns a cube `c` such that `index(to_cube(x)) == x` for every x in
  [0..n-1]. In other words, `to_cube()` is a *section* of `index()`.
  In practice, it is not necessary that the output of `to_cube()` is a
  valid cube object.
* A function `move()` that takes as input a coordinate, a move m and an
  integer x in [0..n-1] and returns an integer y in [0..n-1] such
  that `y == index(m(to_cube(x)))` (here `m(c)` is the cube c moved by m).
* If applicable, a `transform()` function that applies a transformation
  in a similar way as `move()` applies a move. For coordinates this is
  not possible, because they do not capture enough of the cube state.
  For some coordinates, only some transformations are possible. For
  example, for the edge orientation coordinate only the transformations
  that fix the axis with respect to which the edge orientation is defined
  can be applied.

For the implementation, see
[coord.c](https://git.tronto.net/nissy-nx/tree/src/coord.c)
and
[coord.h](https://git.tronto.net/nissy-nx/tree/src/coord.h),
as well as the definition of coordinate in cubetypes.h.

## Coordinate types

In my work I have defined four types of coordinates (but I actually use
only three of them).

### Basic coordinates

Basic coordinates are the simplest kind, and they are completely defined
by the integer `n` and the functions `index()` and `to_cube()` defined
above. This is the type of coordinate that "does not exist" in the code,
because they are just a special case of *composite coordinates*.

### Composite coordinates (COMP\_COORD)

Composite coordinates are, like the name says, a composition of many basic
coordinates. They are given by a list of basic coordinates (n\_1, index\_1(),
to\_cube\_1()), ..., (n\_k, index\_k(), to\_cube\_k()). The value of such a
composite coordinate on cube c is computed as
`index_1(c) + n_1 * (index_2(c) + n_2 * (...))`.

### Symmetric coordinates (SYM\_COORD)

A symmetric coordinate consists of a basic coordinate reduced by symmetry.
Symmetric coordinates must be initialized from a given set of *cube
transformations* before they can be used. The initialization step produces
the following data:

* A table that associates every possible value of the basic coordinate
  with its class in the symmetric coordinate. Computing the value of the
  symmetric coordinate will then amount to computing the basic coordinate
  via `index()` and looking up the value in this table.
* A table that associates every possible value of the basic coordinate
  with a fixed representative for its class.
* A table that associates every possible value of the basic coordinate
  the cube transformation that brings it to its representative.
* A table that associates every possible value of the basic coordinate
  with the list of cube transformations that do not affect it, that is
  a list of *self-symmetries*. This will be useful when computing the
  pruning table associated to this coordinate.

### Symmetric-composite coordinates (SYMCOMP\_COORD)

A symmetric-composite coordinate is based on two other coordinates, a
symmetric coordinate and a composite coordinate. To compute the value
of a symmetric-composite coordinate one must compute the symmetric
coordinate value first, then transform the cube using the transformation
that brings the basic coordinate associated to the symmetric coordinate
to its representative in order to compute the correct value for the
composite coordinate, and finally combine the two.

More precisely and with less tong-twisting, for a given cube c one must
take the following steps:

* Compute the value x\_s of the symmetric coordinate at c, the value
  x\_b of the *basic* coordinate associated with the symmetric coordinate
  and the value x\_c of the composite coordinate.
* Read from the table the transformation t that brings x\_b to its
  representative.
* Apply t to the composite coordinate value x\_c to obtain x\_t.
* Compute the value x\_s * n\_c + x\_t, where n\_c is the maximum value +1
  of the composite coordinate.

## Moving coordinates

A move is applied to a coordinate by lookup into a transition table,
pretty much as explained in Jaap's
[computer puzzling page](https://www.jaapsch.net/puzzles/compcube.htm#trans).
These transition tables are initialized once and for all at the beginning,
and in my implementation I actually saved them to a file to speed up
subsequent runs.

Some extra care has to be taken in dealing with symmetries.

### Basic and composite coordinates

This case is quite simple: the transition table with something along these
lines:

```
for i in [0..n-1]
	for m in Moves
		move_table[m][i] = index(m(to_cube(i)))
```

### Symmetric coordinates

As mentioned above, this case and the next are tricky, because we need
to keep track of which transformation has to be applied to get from the
representative of the symmetry class to our actual cube.

You can imagine a move applied to symmetric coordinate as if we just
move between different representatives. The actual cube can be recovered
from this by keeping track of an *offset transformation*, and updating
it after every move.

The necessary tables can be generated with something like this:

```
for i in [0..n-1]
	for m in Moves
		j = m(rep of i) /* Apply as basic coordinate */
		move_table[m][i] = class of j
		offset_table[m][i] = transformation bringing j to its rep
```

### Symmetric-composite coordinates

A symmetric-composite coordinate requires some extra work, but the hard
part is already handled by its base symmetric coordinate.

To move a symmetric-composite coordinate, first we recover the value s
of its symmetric coordinate's and the value c of its composite coordinate
by taking the quotient and the remainder of division by the maximum value
of the composite coordinate.

Then we apply the move to the values s and c using their respective
transition tables. The offset transformation is taken from the symmetric
coordinate, and apply it to the result of the move on the composite coordinate.

This may be summarized with the following pseudo-code:

```
s = ind / M /* M is the maximum value +1 of the composite coordinate */
c = ind % M
new_s = move_table_s[m][s] /* Using the table for the symmetric coordinate */
new_c = move_table_c[m][c] /* Using the table for the composite coordinate */
offset = offset_table[m][s]
new_c = offset(new_c) /* See next section for transformations */

return (new_s * M + new_c, offset)
```

## Transforming coordinates

When we talk about *cube transformations*, we are talking about
conjugating a cube by a rotation, possibly combined with a mirroring
along a fixed axis.  More precisely, transforming a cube c is done with
the following steps:

* Start from a solved cube
* (optional) Mirror it left-right
* Apply the required rotation
* Apply c (as a permutation / move sequence) to the rotated solved cube
* Apply the inverse of the rotation
* (optional) Mirror it left-right

When working with coordinates, transformations are applied pretty much
in the same way as moves, using a transition table. This time, though,
symmetric coordinates are much less of a problem: any transformation
on a symmetric coordinate is, by definition, trivial! Of course, this
is only true if we limit ourselves to applying transformations that are
part of the set used to "reduce" the symmetric coordinate; but we always
do this anyway.

## Pruning tables

I'd like to mention pruning tables here, because their computation is
quite straightforward when using coordinates.

Say you want to compute a table that associates every possible value i in
[0..n-1] of a coordinate with the minimum amount of moves required to
solve any cube c such that `index(c) == i`. For the purpose of this page
we assume one can afford to allocate enough bits so that the actual value
can be stored.

With coordinates, one can do the following, without ever going back to
the full cube representation:

```
set all values of the pruning table to infinity
set the value associated with the solved cube to 0
for d in [1..20]
	for i in [0..n-1]
		if pruning_table[i] == d-1
			for m in Moves
				old = pruning_table[m(i)]
				pruning_table[m(i)] = min(d, old)
```

This is conceptually very simple, but unfortunately it is not enough.
Because of self-symmetries, not every position will be reached in this
way for a symmetric-composite coordinate: there may be more ways to
bring the base of the symmetric coordinate to its representative, each
having a different effect on the composite coordinate, but we only pick
one of them when building the move tables.

Luckily, this can be solved because we have memorized for every position a
list of all the transformations that keep it invariant (see above). Then
it is enough to add the following loop at the end of every iteration of
the outermost loop above:

```
	for i in [0..n-1]
		for t in the set of self-symmetries of i/M
			old = pruning_table[t(i)]
			pruning_table[t(i)] = min(d, old)
```

The code for this can be found in
[pruning.c](https://git.tronto.net/nissy-nx/tree/src/pruning.c).
The use of pthread made it more complicated, so it can be hard to follow
without reading this page first.

## Why using coordinates?

Coordinates are necessary to compute pruning tables, which are fundamental
when solving a cube using the methods described in Jaap's
[computer puzzling page](https://www.jaapsch.net/puzzles/compcube.htm).

However, using transition tables for moves and transformations is
not necessary.  It used to be an efficient way to perform moves on a
cube, but with newer hardware the trade-offs between memory access and
instruction execution are changing.

One thing that is made particularly cumbersome by using coordinates
instead of a full representation of the cube is computing the
inverse. While it is possible to compute the inverse somewhat efficiently
from a representation of the cube made of a smart selection of coordinate
values, it is not necessarily efficient.
See [fst.c](https://git.tronto.net/nissy-nx/tree/src/fst.c)
for an implementation.

For these reasons, I am not using the coordinate approach described here
for my new work-in-progress solver (which can be found
[here](https://git.tronto.net/nissy-core)).

Generated with cgit - Back to sebastiano.tronto.net