h48

A prototype for an optimal Rubik's cube solver, work in progress.
git clone https://git.tronto.net/h48
Download | Log | Files | Refs | README | LICENSE

commit 9c5bdcb09dc56104f7ea34c71a9418c7908ed227
parent b940b93f6f17d1ac65ca51ba9ec82a1cce0f5017
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Fri, 23 Aug 2024 10:41:34 +0200

Added h48 solver description - work in progress

Diffstat:
Adoc/h48.md | 330+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 330 insertions(+), 0 deletions(-)

diff --git a/doc/h48.md b/doc/h48.md @@ -0,0 +1,330 @@ +# The H48 optimal solver + +This document contains information on the H48 Rubik's Cube optimal solver. +The implementation of the solver is still in progress. This document +partly describes ideas that have not been implemented yet, and it will +be updated to reflect the actual implementation. + +I highly encourage the reader to check out Jaap Scherphuis' +[Computer Puzzling page](https://www.jaapsch.net/puzzles/compcube.htm) +before reading this document. + +Other backround material that will be referenced throughout the text includes: + +* [Cube coordinates](https://sebastiano.tronto.net/speedcubing/coordinates) + by Sebastiano Tronto +* [Nxopt](https://github.com/rokicki/cube20src/blob/master/nxopt.md) + by Tomas Rokicki +* [The Mathematics behind Cube Explorer](https://kociemba.org/cube.htm) + by Herber Kociemba + +Initially I intended to give minimum information here, citing the +above resources when needed, but I ended up rewriting even some of the +basic things in this document. + +## Optimal solvers + +### IDA* + +The basic idea behind the solver is an iterative-deepening +depth-first search with pruning tables, also known as IDA*. You +can find a detailed explanation of this idea in +[Jaap's page](https://www.jaapsch.net/puzzles/compcube.htm#tree). + +To summarize, the IDA* algorithm finds the shortest solution(s) for a +scrambled puzzle by using multiple depth-first searches at increasing +depth. A breadth-first search is not applicable in this scenario because +of the large amount of memory required. At each depth, the puzzle's state +is evaluated to obtain an estimated lower bound for how many moves are +required to solve the puzzle; this is done by employing large pruning +tables, as well as other techniques. If the lower bound exceeds the +number of moves required to reach the current target depth, the branch is +pruned. Otherwise, every possible move is tried, except those that would +"cancel" with the preceding moves, obtaining new positions at depth N+1. + +### Pruning tables and coordinates + +A pruning table associates to a cube position a value that is a +lower bound for the number of moves it takes to solve that position. +To acces this value, the cube must be turned into an index for the +given table. This is done using a +[*coordinate*](https://sebastiano.tronto.net/speedcubing/coordinates), +by which we mean a function from the set of (admissible, solvable) cube +positions to the set of non-negative integers. The maximum value N of a +coordinate must be less than the size of the pruning table; preferably +there are no "gaps", that is the coordinate reaches all values between +0 and N and the pruning table has size N+1. + +Coordinates are often derived from the cosets of a *target subgroup*. +As an example, consider the pruning table that has one entry for each +possible corner position, and the stored values denote the length of +an optimal corner solution for that position. In this situation, the +target subgroup is the set of all cube positions with solved corners +(for the Mathematician: this is indeed a subgroup of the group of all +cube positions). This group consists of `12! * 2^10` positions. The set +of cosets of this subgroup can be identified with the set of corner +configurations, and it has size `8! * 3^7`. By looking at corners, +from each cube position we can compute a number from `0` to `8! * 3^7 - 1` +and use it as an index for the pruning table. + +The pruning table can be filled in many ways. A common way is starting +with the coordinate of the solved cube (often 0) and visiting the set +of all coordinates with a breadth-first search. To do this, one needs +to apply a cube move to a coordinate; this can either be done directly +(for example using transistion tables, as explained in Jaap's page) or +by converting the coordinate back into a valid cube position, applying +the move and then computing the coordinate of the new position. + +The largest a pruning table is, the more information it contains, +the faster the solvers that uses it is. It is therefore convenient to +compress the tables as much as possible. This can be achieved by using +4 bits per position to store the lower bound, or as little as 2 bits +per position with some caveats (see Jaap's page or Rokicki's nxopt +description for two possible ways to achieve this). Tables that use +only 1 bit per position are possible, but the only information they +can provide is wether the given position requires more or less than a +certain number of moves to be solved. + +### Symmetries + +Another, extremely effective way of reducing the size of a pruning table +is using symmetries. In the example above of the corner table, one can +notice that all corner positions that are one quarter-turn away from being +solved are essentially the same: they can be turned one into the other +by rotating the cube and optionally applying a mirroring transformation. +Indeed, every position belongs to a set of up to 48 positions that are +"essentially the same" in this way. Keeping a separate entry in the +pruning table for each of these positions is a waste of memory. + +There are ways to reduce a coordinate by symmetry, grouping every +transformation-equivalent position into the same index. You can +find a description in my +[cube coordinates page](https://sebastiano.tronto.net/speedcubing/coordinates). +By doing so, the size of the pruning table is reduced without loss of +information. + +Some well-known solvers (Cube Explorer, nxopt) do not take advantage +of the full group of 48 symmetries of the cube, but they use only the +16 symmetries that fix the UD axis. However, they make up for this by +doing 3 pruning table lookups, one for each axis of the cube. + +Some cube positions are self-symmetric: when applying certain +transformations to them, they remain the same. For example, the +cube obtained by applying U2 to a solved cube is invariant with +respect to the mirroring along the RL-plane. This fact has a couple +of consequences: + +* Reducing by a group of N symmetries does not reduce the size of + the pruning table by a factor of N, but by a slightly smaller + factor. If the size of the coordinate is large, this fact is + harmless. +* When combining symmetry-reduced coordinates with other coordinates, + one has to be extra careful with handling self-symmetric coordinates. + This is a much more painful point, but a precise description of + the adjustments needed to handle these cases is out of the scope + of this document. + +## The H48 solver + +### The target subgroup: H48 coordinates + +The H48 solver uses a target group that is invariant under all 48 +symmetries. This group is defined as follows: + +* Each corner is placed in the *corner tetrad* it belongs to, and it + is oriented. Here by corner tetrad we means one of the two sets of + corners {UFR, UBL, DFL, DBR} and {UFL, UBR, DFR, DBL}. For a corner + that is placed in its own tetrad, the orientation does not depend on + the reference axis chosen, and an oriented corner can be defined + has having the U or D sticker facing U or D. +* Each edge is placed in the *slice* it belongs to. The three edge slices + are M = {UF, UB, DB, DF}, S = {UR, UL, DL, DR} and E = {FR, FL, BL, BR}. + +Other options are available for corners: for example, we could +impose that the corner permutation is even or that corners are in +*half-turn reduction* state, that is they are in the group generated +by {U2, D2, R2, L2, F2, B2}. We settled on the corner group described +above because it is easy to calculate and it gives enough options +for pruning table sizes, depending on how edge orientation is +considered (see the next section). + +We call the coordinate obtained from the target subgroup described +above the **h0** coordinate. This coordinate has `C(8,4) * 3^7 * +C(12,8) * C(8,4) = 5.304.568.500` possible values. If the corner +part of the coordinate is reduced by symmetry, it consists of only +3393 elements, giving a total of `3393 * C(12,8) * C(8,4) = +117.567.450` values. This is not very large for a pruning table, +as with 2 bits per entry it would occupy less than 30MB. However, +it is possible to take edge orientation (partially) into account +to produce larger tables. + +### Edge orientation + +Like for corners, the orientation of an edge is well-defined independently +of any axis when said edge is placed in the slice it belongs to. We may +modify the target subgroup defined in the previous section by imposing +that the edges are all oriented. This yields a new coordinate, which we +call **h11**, whose full size with symmetry-reduced corners is `3393 * +C(12,8) * C(8,4) * 2^11 = 240.778.137.600`. A pruning table based on +this coordinate with 2 bits per entry would occupy around 60GB, which +is a little too much for most personal computers. + +One can wonder if it is possible to use a coordinate that considers +the orientation of only *some* of the edges, which we may call **h1** +to **h10**. Such coordinates do exist, but it is not invariant under the +full symmetry group: indeed an edge whose orientation we keep track of +could be moved to any of the untracked edges' positions by one of the +symmetries, making the whole coordinate ill-defined. + +It is however possible to compute the symmetry-reduced pruning tables +for these coordinates. One way to construct them is by taking the **h11** +pruning table and "forgetting" about some of the edge orientation values, +collapsing 2 (or a power thereof) values to one by taking the minimum. +It is also possible to compute these tables directly, as explained in +the **Pruning table computation** section below (work in progress). + +### Coordinate computation for pruning value estimation + +In order to access the pruning value for a given cube position, we first +need to compute its coordinate value. Let's use for simplicity the **h0** +coordinate, but everything will be valid for any other coordinate of +the type described in the previous section. + +As described in the symmetric-composite coordinate section of +[my coordinates page](https://sebastiano.tronto.net/speedcubing/coordinates), +we first need to compute the part of the coordinate where the symmetry +is applied, that is the corner separation + orientation (also called +*cocsep*). The value of the coordinate depends on the equivalence +class of the corner configuration, and is memorized in a table called +`cocsepdata`. To avoid lengthy computations, the index for a cube position +in this table is determined by the corner orientation (between `0` and +`3^7-1`) and a binary representation of the corner separation part (an +8-bit number with 4 zero digits and 4 one digits, where zeros denote +corners in one tetrad and 1 denotes corners in the other; the last bit +can be ignored, as it can easily be deduced from the other 7). This is +slightly less space-efficient than computing the actual corner-separation +coordinate as a value between `0` and `C(8,4)`, but it is much faster. + +We can now access the value in the `cocsepdata` table corresponding to +our cube position. This table contains the value of the symmetry-reduced +coordinate and the so-called *ttrep* (transformation to representative) +necessary to compute the full **h0** coordinate. For convenience, +this table also stores a preliminary pruning value based on the corner +coordinate. If this value is large enough, we may skip the computation of +the **h0** coordinate and any further estimate. These three values can be +stored in a single 32 bit integer, so that the table uses less than 12MB. + +### Getting the pruning value + +Once we have computed the full h0 coordinate, we can access the correct +entry in the full pruning table. As mentioned above, the pruning table +can be one of three kinds: + +(Work in progress - the only kind of table currently implemented is +the 4 bits per entry table) + +* 4 bits per entry, or `k4`: In this case the pruning value (between 0 + and 15) can be simply read off the table. +* 2 bits per entry, or `k2`: Tables of this kind work as described by + Rokicki in the + [nxopt document](https://github.com/rokicki/cube20src/blob/master/nxopt.md). + In this case the pruning table also has a *base value*, that determines + the offset to be added to each entry (each entry can only be 0, 1, 2 or 3). + If the base value is `b`, a pruning value of 1, 2 or 3 can be used directly + as a lower bound of b+1, b+2 and b+3 respectively. However, a value of 0 + could mean that the actual lower bound is anything between 0 and b, so we + cannot take b as a lower bount. Instead we have to use a pruning value from + another table, for example the corner-only table mentioned in the previous + section, or a completely new one. + (Work in progress - `k2` tables not available in the code yet) +* 1 bit per entry, or `k1`: With one bit per entry, the only information we + can get from the pruning table is wether or not the current position + requires more or fewer moves than a fixed base value b. This can still be + valuable if most positions are more or less equally split between two + pruning values. + (Work in progress - `k1` tables not available in the code yet) + +### Estimation refinements + +After computing the pruning value, there are a number of different tricks +that can be used to improve the estimation. + +(Work in progress - the following techniques are not implemented yet) + +#### Inverse estimate + +A cube position and its inverse will, in general, give a different +pruning value. If the normal estimate is not enough to prune the branch, +the inverse of the position can be computed and a new estimate can be +obtained from there. + +#### Reducing the branching factor - tight bounds + +*This trick and the next are and adaptation of a similar technique +introduced by Rokicki in nxopt.* + +We can take advantage of the fact that the **h0** (and **h11**) coordinate +is invariant under the subgroup `<U2, D2, R2, L2, F2, B2>`. Suppose we +are looking for a solution of length `m`, we are at depth `d` and the +pruning value `p` *for the inverse position* is a strict bound, that is +`d+p = m`. In this situation, from the inverse point of view the solution +cannot end with a 180° move: if this was the case, since these moves are +contained in the target group for **h0** (or **h11**), it must be that +we were in the target subgroup as well *before the last move*, i.e. we +have found a solution for the **h0** coordinate of length `p-1`. But this +is in contradiction with the fact that the inverse pruning value is `p`. + +From all of this, we conclude that trying any 180° move as next move is +useless (because it would be the last move from the inverse position). +We can therefore reduce the branching factor significantly and only try +quarter-turn moves. + +#### Reducing the branching factor - switching to inverse + +We can expand on the previous trick by using a technique similar to NISS. + +Suppose that we have a strict bound for the *normal position*. As above, +we can deduce that the last move cannot be a 180° move, but this does not +tell us anything about the possibilities for the next move. However, if +we *switch to the inverse scramble* we can take advantage of this fact +as described above. For doing this, we need to replace the cube with its +inverse, and keep track of the moves done from now on so that we can +invert them at the end to construct the final solution. + +### Other optimizations + +Other possible (low-level) optimizations include: + +* **Avoid inverse computation**: computing the inverse of a cube position + is expensive. We can avoid doing that (for the inverse pruning value + estimate) if we bring along both the normal and the inverse cube during + the search, and we use *premoves* to apply moves to the inverse scramble. + (Work in progress - premoves are not implemented yet, but it will take + little work to add them) +* **Multi-threading (multiple scrambles)**: It is easy to parallelize this + algorithm when solving multiple cubes at once, by firing up multiple + instances of the solver. It is important to make sure that the same + (read-only) pruning table is used for all instances, to avoid expensive + memory duplication. +* **Multi-threading (single scramble)**: It is also possible to parallelize + the search for a single scramble. For example, we can generate 18 different + cubes, one for each possible starting move, and solve each of them in a + separate thread. Some coordination between threads is necessary to stop + the search when the desired number of solutions has been found. + +## Pruning table computation + +### The h0 table + +TODO - short explanation of how this is computed + +### The intermediate tables (h1, ... h10) + +TODO - explain why these are more complicated (if one does not +want to compute the full **h11** table first) + +Work in progress - these tables are not implemented yet. + +### The h11 table + +Work in progress - this tables is not implemented yet.