\def\mod{\mathop{mod}} @s cubepos int @s moveseq int @s kocsymm int @s permcube int @* Introduction. The |cubepos| package provides a rich and fast representation of the Rubik's cube, with a full set of operations including moves, multiplication, and inversion. (Please read that document if you haven't already, because |kocsymm| builds on that.) While |cubepos| is rich and efficient, sometimes it is not exactly what you need. For instance, if you wanted to use the |cubepos| structure to create an index into an array based on the $12!$ possible permutations of the edges, you would have to do a fair amount of work. Other representations of the cube provide faster indexing operations. The two classes defined here, |kocsymm| and |permcube|, provide an alternative representation of the cube that is particularly suitable for implementations of Herbert Kociemba's two-phase algorithm. @(kocsymm.h@>= #ifndef KOCSYMM_H #define KOCSYMM_H #include "cubepos.h" @ The two-phase algorithm is based on the subgroup generated by $\{U,F2,R2,D,B2,L2\}$. We refer to this group as $H$. The idea behind the two-phase algorithm is to find a way to take an arbitrary cube position and find a sequence to bring it into the subgroup, and then solve it within the subgroup using moves within the subgroup. The first phase is just finding a path within the Schreier coset graph of the group $H$ to the trivial coset; the second part is solving within that coset. The orientation conventions in |cubepos| were carefully chosen so that none of the moves that generate $H$ change the orientation of any of the cubies in the solved position; thus, all positions in $H$, when represented by |cubepos|, have the same orientation (both edge and corners) as the solved position. Furthermore, none of the moves that generate $H$ move any of the cubies in the middle slice out of the middle slice; thus, this is also preserved for all positions in $H$. It turns out (but we will not prove it here) that every position that meets these conditions is in the subgroup $H$. Each of the $8!$ permutations of the corners is reachable, in combination with each of the $8!$ permutations of the top and bottom edges, and also in combination with all $4!$ permutations of the middle edges subject that the overall parity of the corners and edges match. Thus, the size of $H$ is $8!\cdot 8!\cdot 4!/2$ or 19,508,428,800. Given a representative position $p$, the right coset of $H$ corresponding to $p$ is just $Hp$ (where the multiplication operation of the group is extended to sets in the usual way). Since all the positions in $H$ have the same, solved, orientation, this means all the positions in any particular coset of $H$ (identified by $p$) share the same orientation. Similarly, the set of cubie slots that contain middle edge cubies is also the same for every element of a particular coset of $H$. Indeed, these three things, the edge orientation, the corner orientation, and the cubie slots containing the middle edge cubies, fully define a coset of $H$. The |kocsymm| class contains three integer coordinates that each represent one of these characteristics, and thus, an entire coset of $H$. The |move| operation on a |kocsymm| instance is just an edge on the coset graph of $H$ induced by the set of move generators we have chosen. By using simple integer coordinates, we allow for very fast operations of |move| and indexing. The |csymm| coordinate represents the corner orientation and has $3^7$ possible values; the |eosymm| coordinate represents the edge orientation and has $2^{11}$ possible values; the $epsymm$ coordinate has $12\choose 4$ possible values and represents the slots that contain the middle four slice cubies. Where order matters, we will always give the coordinates in this order. (They are ordered from largest range to smallest range.) We define a type that is large enough for these ranges (and also $8!$, incidentally) yet still storage efficient, and use this type for most of our tables. For the trivial coset, the value of all three coordinates is chosen to be zero. @(kocsymm.h@>= const int CORNERSYMM = 2187 ; const int EDGEOSYMM = 2048 ; const int EDGEPERM = 495 ; @/ @ @; typedef unsigned short lookup_type ; class kocsymm { public: @/ kocsymm() : csymm(0), eosymm(0), epsymm(0) {} @; kocsymm(int c, int eo, int ep) : csymm(c), eosymm(eo), epsymm(ep) {} @; @ @; @ @; lookup_type csymm, eosymm, epsymm ; @; } ; @ We have the same static initialization issue with |kocsymm| that we did with |cubepos|, so we declare a special initializer that forces an initialization routine to be called, as well as that initialization routine itself. @= kocsymm(int) : csymm(0), eosymm(0), epsymm(0) { init() ; } static void init() ; @ To force initialization in the proper order for all users of this include file, we declare a file-static instance here. Since we need an identity anyway, we go ahead and make this the identity object for this class (although there will be multiple instances, they will all have the same value). @(kocsymm.h@>= static kocsymm identity_kc(1) ; @ We use simple ordering, equality, and inequality methods for this simple value class. @= inline bool operator<(const kocsymm &kc) const { if (csymm != kc.csymm) return csymm < kc.csymm ; if (eosymm != kc.eosymm) return eosymm < kc.eosymm ; return epsymm < kc.epsymm ; } inline bool operator==(const kocsymm &kc) const { return kc.csymm == csymm && kc.eosymm == eosymm && kc.epsymm == epsymm ; } inline bool operator!=(const kocsymm &kc) const { return kc.csymm != csymm || kc.eosymm != eosymm || kc.epsymm != epsymm ; } @ We want to implement a fast move operation, and the range of each of the coordinates is fairly small, so we use static tables to implement |move|. We choose to use the coordinate value as the first index, as many of our searches will be depth-first search, and this will give us some cache locality we would otherwise lose out on. @= static lookup_type cornermove[CORNERSYMM][NMOVES] ; static lookup_type edgeomove[EDGEOSYMM][NMOVES] ; static lookup_type edgepmove[EDGEPERM][NMOVES] ; @ These arrays need to be allocated, so it is time to introduce our |cpp| source. @(kocsymm.cpp@>= #include "kocsymm.h" #include using namespace std ; @ @; @ @; @ @; void kocsymm::init() { static int initialized = 0 ; if (initialized) return ; initialized = 1 ; @ @; permcube::init() ; } @ We need to instantiate these arrays. @= lookup_type kocsymm::cornermove[CORNERSYMM][NMOVES] ; lookup_type kocsymm::edgeomove[EDGEOSYMM][NMOVES] ; lookup_type kocsymm::edgepmove[EDGEPERM][NMOVES] ; @ With these arrays, the |move| operation is very simple. @= void move(int mv) { csymm = cornermove[csymm][mv] ; eosymm = edgeomove[eosymm][mv] ; epsymm = edgepmove[epsymm][mv] ; } @ The easiest way to initialize these arrays are to introduce conversion routines that allow us to extract the coordinates from a |cubepos|, and allow us to set up a |cubepos| with those characteristics. We can use a constructor to go from |cubepos| to |kocsymm|, but we use a |set_coset| to modify an existing |cubepos| so it is in the coset represented by the current |kocsymm|. @= kocsymm(const cubepos &cp) ; void set_coset(cubepos &cp) ; @* Numbering the coordinates. For the corner symmetries, the easiest numbering representation is just as base-3 number, where the least significant digit comes from corner 0, and so on, and with the value from corner 7 ignored (since it must the the negative sum of the other corners). Similarly, the edge symmetries are most easily handled as a base-2 number from the first 11 edges. The slots holding middle edge cubies is just a bit more complicated. We insist that the zero value be the solved position. First we build a bitmask that always has four bits set; the least significant four bits represent edge slots 4 to 7 (the middle slots), the next four bits represent edge slots 8 to 11, and the final four bits represent edge slots 0 to 3. We sort all possible 12-bit values in increasing numerical value, and use the index into this array to determine the value for |epsymm|. To support this, we need two arrays, one to compress the bits from 12 bits down to an |epsymm| value, and one to expand the |epsymm| back into a bitmask. The rotations are done in the arrays, so the values you will obtain from the array and/or pass into the array all have the bits in normal, 0 though 12, order. @= static lookup_type epsymm_compress[1<<12] ; static lookup_type epsymm_expand[EDGEOSYMM] ; @ The usual instantiation. @= lookup_type kocsymm::epsymm_compress[1<<12] ; lookup_type kocsymm::epsymm_expand[EDGEOSYMM] ; @ To help us fill these arrays, we need a generic bit counting function. This is not used in any performance-critical code, so we can be a bit slow. @= static int bc(int v) { int r = 0 ; while (v) { v &= v - 1 ; r++ ; } return r ; } @ Filling these two arrays is straightforward. We also fill the entry without the high bit set, just in case we decide to only look at 11 cubies rather than 12. @= int c = 0 ; for (int i=0; i<1<<12; i++) if (bc(i) == 4) { int rotval = ((i << 4) + (i >> 8)) & 0xfff ; epsymm_compress[rotval] = c ; epsymm_compress[rotval & 0x7ff] = c ; epsymm_expand[c] = rotval ; c++ ; } @ With that done, we are now ready to obtain a |kocsymm| object from a |cubepos|. This routine does not have to be dramatically fast. We use a little trick; of the edge indices 0 through 11, only those in the middle edge, with values 4 though 7, have the bit with value 4 set. Since the cubie numbering for edges has the orientation in the low bit, this means we actually need to use the bit with value 8. @= kocsymm::kocsymm(const cubepos &cp) { int c=0, eo=0, ep=0 ; for (int i=6; i>=0; i--) c = 3 * c + cubepos::corner_ori(cp.c[i]) ; for (int i=10; i>=0; i--) { eo = 2 * eo + cubepos::edge_ori(cp.e[i]) ; ep = 2 * ep + (cp.e[i] & 8) ; } csymm = c ; eosymm = eo ; epsymm = epsymm_compress[ep >> 3] ; } @ Setting a cubepos to be in the coset is also straightforward. We completely destroy the pre-existing permutation in the |cubepos| as we do this. This routine is not particularly fast. The only complexity in this routine is recovering the orientation of the last corner and edge. @= void kocsymm::set_coset(cubepos &cp) { int c=csymm, eo=eosymm, ep=epsymm_expand[epsymm] ; int s = 0 ; for (int i=0; i<7; i++) { int ori = c % 3 ; cp.c[i] = cubepos::corner_val(i, ori) ; s += ori ; c = c / 3 ; } cp.c[7] = cubepos::corner_val(7, (8*3-s) % 3) ; s = 0 ; int nextmid = 4 ; int nextud = 0 ; for (int i=0; i<12; i++) { if (i == 11) eo = s ; int ori = eo & 1 ; if (ep & 1) cp.e[i] = cubepos::edge_val(nextmid++, ori) ; else { cp.e[i] = cubepos::edge_val(nextud++, ori) ; if (nextud == 4) nextud = 8 ; } s ^= ori ; eo >>= 1 ; ep >>= 1 ; } } @ With these two routines in place, we can fill out our move arrays. Note that we have to use |movepc|, since the coset space (which is not a group) doesn't know where the cubies are, only what the orientations are in specific slots (and a bit more information about the middle cubies). @= cubepos cp, cp2 ; for (int i=0; i= const int KOCSYMM = 16 ; const int CORNERRSYMM = 168 ; @ We need a set of arrays to manage the canonicalization. We need remapping arrays for the edge orientation and permutation. We need an array for the edge permutation that says what bits to flip (but we only need one entry, used only if we are remapping to 8 through 15). For corner remapping, we have two cases. The most common case is there is a unique remapping that minimizes the corner coordinate, in which case canonicalization is quick and easy. The other case is when there are multiple distinct remappings that all generate the same minimal corner coordinate. In this case, we store a bitmask indicating which remappings to consider, and we must iterate through them all. From the corner coordinate, we compute three data items: |minbits|, a set of 16 bits, one per symmetry, that generates the minimum corner coordinate value; |csymm|, the corner symm we get as a result (after compaction), and |mimap|, the minimum mapping that generates that value. @= struct corner_mapinfo { unsigned short minbits ; unsigned char csymm, minmap ; } ; @ We need the following arrays to support canonicalization. @= static lookup_type cornersymm_expand[CORNERRSYMM] ; static corner_mapinfo cornersymm[CORNERSYMM] ; static lookup_type edgeomap[EDGEOSYMM][KOCSYMM] ; static lookup_type edgepmap[EDGEPERM][KOCSYMM] ; static lookup_type edgepxor[EDGEPERM][2] ; @ We need to instantiate those arrays. @= lookup_type kocsymm::cornersymm_expand[CORNERRSYMM] ; corner_mapinfo kocsymm::cornersymm[CORNERSYMM] ; lookup_type kocsymm::edgeomap[EDGEOSYMM][KOCSYMM] ; lookup_type kocsymm::edgepmap[EDGEPERM][KOCSYMM] ; lookup_type kocsymm::edgepxor[EDGEPERM][2] ; @ Our strategy for initializing these is very similar to what we did for moves: use the |cubepos| class and the two conversion routines to do the heavy lifting. We start by figuring out the corner compaction values. @= c = 0 ; for (int cs=0; cs= for (int ep=0; ep= void canon_into(kocsymm &kc) const ; @ The implementation first checks if we can do it quickly, and if not, iterates. @= void kocsymm::canon_into(kocsymm &kc) const { corner_mapinfo &cm = cornersymm[csymm] ; kc.csymm = cornersymm_expand[cm.csymm] ; kc.eosymm = edgeomap[edgepxor[epsymm][cm.minmap>>3]^eosymm][cm.minmap] ; kc.epsymm = edgepmap[epsymm][cm.minmap] ; for (int m=cm.minmap+1; cm.minbits>>m; m++) if ((cm.minbits >> m) & 1) { int neo = edgeomap[edgepxor[epsymm][m>>3]^eosymm][m] ; if (neo > kc.eosymm) continue ; int nep = edgepmap[epsymm][m] ; if (neo < kc.eosymm || nep < kc.epsymm) { kc.eosymm = neo ; kc.epsymm = nep ; } } } @ We need a method that returns how much symmetry this |kocsymm| has. @= int calc_symm() const ; @ The implementation is just a slight rewriting of |canon_into|. @= int kocsymm::calc_symm() const { int r = 1 ; corner_mapinfo &cm = cornersymm[csymm] ; int teosymm = edgeomap[edgepxor[epsymm][cm.minmap>>3]^eosymm][cm.minmap] ; int tepsymm = edgepmap[epsymm][cm.minmap] ; for (int m=cm.minmap+1; cm.minbits>>m; m++) if (((cm.minbits >> m) & 1) && edgeomap[edgepxor[epsymm][m>>3]^eosymm][m] == teosymm && edgepmap[epsymm][m] == tepsymm) r++ ; return r ; } @ We need a method that tells us if a move is in the Kociemba group or not. We can just determine if a transition from the default state of |epsymm| is zero or not. @= static inline int in_Kociemba_group(int mv) { return edgepmove[0][mv] == 0 ; } @* Storing permutations with |permcube|. With |kocsymm| working, we can turn our attention to storing those bits of the state that are not stored in it---the permutation information. While |kocsymm| does store a limited amount of permutation information (what slots the middle four cubies are in), |permcube| stores all of the permutation information. We design |permcube| to enable fast moves and indexing of the resulting state, with the tradeoff that it is not as rich as |cubepos|; for instance, we do not define inversion. We store edge permutation information and corner permutation information separately. The |kocsymm| class already defines the ability to maintain the position of four cubies at a time (as a group); we exploit that to maintain the slots for the upper edges and the lower edges as well. We store this information in the three fields |et|, |em|, and |eb| (edge top, edge middle, and edge bottom). For all three groups of four cubies, we store in addition the order that the cubies occur within that group of four; we store this in the fields |etp|, |emp|, and |ebp|. The information in |et|, |em|, and |eb| is redundant; if we know the slots holding either two sets, we also know the sets holding the other. Nonetheless, dividing the $12!$ or 479,001,600 possible states into six smaller chunks, three of 495 values and 3 of 24 values, makes our transition tables much smaller, and we share the same transition tables for the top, middle, and edge. For the corners, we use a similar approach: we store which four of the eight slots contain top corner cubies in |c8_4|, and separately, we store the order of the top cubies in |ctp|, and the order of the bottom cubies in |cbp|. @(kocsymm.h@>= const int FACT4 = 24 ; const int C8_4 = 70 ; class permcube { public: @/ permcube() ; @ @; static void init() ; @ @; unsigned short et, em, eb ; unsigned char etp, emp, ebp ; unsigned char c8_4, ctp, cbp ; } ; @ We allocate a file-scope identity instance statically. We don't actually need this one to work around the static initialization fiasco, but it's always good to have a cheap identity object. @(kocsymm.h@>= static permcube identity_pc ; @ To manage all the permutations of four elements, we need to build the multiplication and inversion table for this group, called $S_4$. We also declare two arrays, one which takes an eight-byte value, two bits per element, that gives the permutation (the identity element would be |0b11100100| or |0xe4|; the least significant bits represent the first element) and gives the corresponding index for that permutation, and one that does the inverse of that. @= static unsigned char s4inv[FACT4] ; static unsigned char s4mul[FACT4][FACT4] ; static unsigned char s4compress[256] ; static unsigned char s4expand[FACT4] ; @ Next, we declare these. @= unsigned char permcube::s4inv[FACT4] ; unsigned char permcube::s4mul[FACT4][FACT4] ; unsigned char permcube::s4compress[256] ; unsigned char permcube::s4expand[FACT4] ; @ We need an initialization routine for |permcube|. This is called automatically by |kocsymm::init()| so we don't need a static initialization hack. @= void permcube::init() { @ ; } @ Permutation numbering. Normally we would number $S_4$ is lexicographical order. But for various reasons we need to compute the parity of the permutation quickly, so we use bit 0 of the indexing for that purpose; this avoids a table lookup. We have another requirement, however, introduced by |hcoset|. Let $i(p)$ be the integer index assigned to permutation $p$, $p(i)$ to be the permutation associated with integer index $i$, and $a\cdot b$ to be the multiplication of the permutation $a$ by the permutation $b$, and $j\oplus k$ to be the bit-wise exclusive-or of $j$ and $k$. We want $p(i(a)\oplus 1)\cdot b=p(i(a\cdot b)\oplus 1)$. Essentially, we want to group our permutations into pairs, the first even and the second odd, such that right multiplication preserves the pairs. We need this so we can collect certain pairs of permutations into a 24-bit word, perform an operation on them, and be assured that the result will still fall into a single 24-bit word, rather than different halves of two different 24-bit words. It turns out both of these are easy to arrange. We generate the permutations in lexicographical order, but use the inverse permutation rather than the forward permutation, and store the parity. For the |c| loop below, there are only two values left for |c| and |d|, so the two permutations generated in sequence will have these values swapped, which is precisely what we need. The parity is just the exclusive or of the least significant two bits of the lexicographical order index. @= int cc = 0 ; for (int a=0; a<4; a++) for (int b=0; b<4; b++) if (a != b) for (int c=0; c<4; c++) if (a != c && b != c) { int d = 0 + 1 + 2 + 3 - a - b - c ; int coor = cc ^ ((cc >> 1) & 1) ; int expanded = (1 << (2 * b)) + (2 << (2 * c)) + (3 << (2 * d)) ; s4compress[expanded] = coor ; s4expand[coor] = expanded ; cc++ ; } for (int i=0; i= int muls4(int a, int b) { int r = 3 & (b >> (2 * (a & 3))) ; r += (3 & (b >> (2 * ((a >> 2) & 3)))) << 2 ; r += (3 & (b >> (2 * ((a >> 4) & 3)))) << 4 ; r += (3 & (b >> (2 * ((a >> 6) & 3)))) << 6 ; return r ; } @ For the edge groups of four, we use the same arrays as |kocsymm|; these have already been defined and initialized. For the corner groups, we need to write compaction and move arrays. @= static unsigned char c8_4_compact[256] ; static unsigned char c8_4_expand[C8_4] ; static unsigned char c8_4_parity[C8_4] ; @ Next, we declare these. @= unsigned char permcube::c8_4_compact[256] ; unsigned char permcube::c8_4_expand[C8_4] ; unsigned char permcube::c8_4_parity[C8_4] ; @ To initialize these arrays, we again need to track the parity. The pattern is more complex for the eight-bit words that have four bits set, so we simply count inversions. @= int c = 0 ; for (int i=0; i<256; i++) if (bc(i) == 4) { int parity = 0 ; for (int j=0; j<8; j++) if (1 & (i >> j)) for (int k=0; k> k))) parity++ ; c8_4_parity[c] = parity & 1 ; c8_4_compact[i] = c ; c8_4_expand[c] = i ; c++ ; } @ The usual use for |permcube| is to handle operations within the Kociemba group $H$, where the middle edge positions are always in the middle edge. Thus, the group information for the top edges is just $8\choose 4$ rather than $12\choose 4$, so we need an array to compress the $12\choose 4$ index (which ranges from 0 to 494) to a $8\choose 4$ index. @= static unsigned char c12_8[EDGEPERM] ; static lookup_type c8_12[C8_4] ; @ Next, we declare these. @= unsigned char permcube::c12_8[EDGEPERM] ; lookup_type permcube::c8_12[C8_4] ; @ Initializing this is straightforward; we expand the bits, remove the middle four, and compress them again. @= for (int i=0; i> 4) + (expbits & 15)] ; c12_8[i] = ii ; c8_12[ii] = i ; } } @ We need equality and ordering routines. These are a bit long because of the count of fields. Note that we cannot use |memcmp| reliably because there might be indeterminate padding. @= inline bool operator<(const permcube &pc) const { if (et != pc.et) return et < pc.et ; if (em != pc.em) return em < pc.em ; if (eb != pc.eb) return eb < pc.eb ; if (etp != pc.etp) return etp < pc.etp ; if (emp != pc.emp) return emp < pc.emp ; if (ebp != pc.ebp) return ebp < pc.ebp ; if (c8_4 != pc.c8_4) return c8_4 < pc.c8_4 ; if (ctp != pc.ctp) return ctp < pc.ctp ; return cbp < pc.cbp ; } inline bool operator==(const permcube &pc) const { return et == pc.et && em == pc.em && eb == pc.eb && etp == pc.etp && emp == pc.emp && ebp == pc.ebp && c8_4 == pc.c8_4 && ctp == pc.ctp && cbp == pc.cbp ; } inline bool operator!=(const permcube &pc) const { return et != pc.et || em != pc.em || eb != pc.eb || etp != pc.etp || emp != pc.emp || ebp != pc.ebp || c8_4 != pc.c8_4 || ctp != pc.ctp || cbp != pc.cbp ; } @ To write our move method, we need arrays that give the action of moves on our various fields. For the edge group movement, the |kocsymm| class already provides this information, but it does not provide information on how the permutation of the constituent cubies changes. We need a move array that provides both pieces of information. The new coordinate requires nine bits to represent, and the $S_4$ index requires five bits to represent. We could use a three byte struct that would blow up to four bytes total for alignment, or we can use bit fields. We prefer bit fields; we code our own to make sure they fit in a short. We also need an array to manage the corner moves, with the same basic structure. We use file statics for these; no need to expose them. @= static unsigned short eperm_move[EDGEPERM][NMOVES] ; static int cperm_move[C8_4][NMOVES] ; @ We instantiate those arrays here. @= unsigned short permcube::eperm_move[EDGEPERM][NMOVES] ; int permcube::cperm_move[C8_4][NMOVES] ; @ The move routine is declared here. @= void move(int mv) ; @ The move routine is pretty simple; for each group field, we calculate its new value, and extract the appropriate $S_4$ effect on the permutation of its elements from the low order five bits. @= void permcube::move(int mv) { #ifdef SAFETY_CHECKS if ((kocsymm::epsymm_expand[et]|kocsymm::epsymm_expand[em]| kocsymm::epsymm_expand[eb]) != 0xfff) error("! bad pc in move") ; #endif int t = eperm_move[et][mv] ; et = t >> 5 ; etp = s4mul[etp][t & 31] ; t = eperm_move[em][mv] ; em = t >> 5 ; emp = s4mul[emp][t & 31] ; t = eperm_move[eb][mv] ; eb = t >> 5 ; ebp = s4mul[ebp][t & 31] ; t = cperm_move[c8_4][mv] ; c8_4 = t >> 10 ; ctp = s4mul[ctp][(t >> 5) & 31] ; cbp = s4mul[cbp][t & 31] ; } @ In order to fill in these arrays, it's easiest to have a pair of routines that gets a permutation from a |cubepos|, and another that sets a permutation from a |cubepos|. Unlike |kocsymm::set_coset|, the |set_perm| routine will preserve the orientation, only affecting the cubie permutations. So if you call both |set_coset| and |set_perm|, make sure to call |set_coset| first and |set_perm| second. We also provide routines to get only the corner information and only the edge information because sometimes that's all we need, and these routines can make a major difference in performance. We also provide a routine that gets just the up/down permutation and another that gets just the middle permutation for those specific cases where the position is guaranteed to be already in the Kociemba group. Similar routines exist for setting just the edge information and setting just the corner information. @= void init_edge_from_cp(const cubepos &cp) ; void init_corner_from_cp(const cubepos &cp) ; permcube(const cubepos &cp) ; void set_edge_perm(cubepos &cp) const ; void set_corner_perm(cubepos &cp) const ; void set_perm(cubepos &cp) const ; @ The constructor from a basic cube simply iterates through the cubies, keeping track of which groups each cubie belongs to and the order that the cubies are seen in. We iterate backwards so the least significant cubie ends up in the low order bits. Edges first. @= void permcube::init_edge_from_cp(const cubepos &cp) { et = em = eb = 0 ; etp = emp = ebp = 0 ; for (int i=11; i>=0; i--) { int perm = cubepos::edge_perm(cp.e[i]) ; if (perm & 4) { // middle layer em |= 1<= void permcube::init_corner_from_cp(const cubepos &cp) { c8_4 = 0 ; ctp = cbp = 0 ; for (int i=7; i>=0; i--) { int perm = cubepos::corner_perm(cp.c[i]) ; if (perm & 4) { // bottom layer cbp = 4 * cbp + (perm & 3) ; } else { c8_4 |= 1<= void permcube::set_edge_perm(cubepos &cp) const { int et_bits = kocsymm::epsymm_expand[et] ; int em_bits = kocsymm::epsymm_expand[em] ; int et_perm = s4expand[etp] ; int em_perm = s4expand[emp] ; int eb_perm = s4expand[ebp] ; for (int i=0; i<12; i++) if ((et_bits >> i) & 1) { // top layer cp.e[i] = cubepos::edge_val((3 & et_perm), cubepos::edge_ori(cp.e[i])) ; et_perm >>= 2 ; } else if ((em_bits >> i) & 1) { // middle layer cp.e[i] = cubepos::edge_val((3 & em_perm) + 4, cubepos::edge_ori(cp.e[i])) ; em_perm >>= 2 ; } else { // bottom layer cp.e[i] = cubepos::edge_val((3 & eb_perm) + 8, cubepos::edge_ori(cp.e[i])) ; eb_perm >>= 2 ; } } @ Corners next, and we put it together. @= void permcube::set_corner_perm(cubepos &cp) const { int c8_4_bits = c8_4_expand[c8_4] ; int ct_perm = s4expand[ctp] ; int cb_perm = s4expand[cbp] ; for (int i=0; i<8; i++) if ((c8_4_bits >> i) & 1) { // top layer cp.c[i] = cubepos::corner_val((3 & ct_perm), cubepos::corner_ori(cp.c[i])) ; ct_perm >>= 2 ; } else { cp.c[i] = cubepos::corner_val((3 & cb_perm) + 4, cubepos::corner_ori(cp.c[i])) ; cb_perm >>= 2 ; } } void permcube::set_perm(cubepos &cp) const { set_edge_perm(cp) ; set_corner_perm(cp) ; } @ Our base constructor is next. Everything can be set to zero, except for the |et| and |eb| bits, which must be set to values pulled from the |epsymm_expand| arrays. Since we are using file static objects for |kocsymm| that are defined in every compilation unit, we can assume that |kocsymm| and thus |permcube| is initialized at any point this constructor is called. @= permcube::permcube() { c8_4 = 0 ; ctp = cbp = 0 ; et = kocsymm::epsymm_compress[0xf] ; em = 0 ; eb = kocsymm::epsymm_compress[0xf00] ; etp = emp = ebp = 0 ; } @ Now we are prepared to initialize the move arrays. We need to be careful to initialize the edge group variables to consistent values. We do the edges first. @= cubepos cp, cp2 ; for (int i=0; i= for (int i=0; i= #endif @* Testing. We do some basic unit tests to verify functionality. First we do some basic tests; do our basic constructors generate the same thing as the identity cube? @= { cubepos cpi ; permcube pci(cpi) ; kocsymm kci(cpi) ; permcube pct ; kocsymm kct ; if (pct != pci || kct != kci) error("! problem with default constructors") ; if (permcube::c12_8[pc.et] != 0) error("! bad mapping in 12->8") ; } @ Next we test conversions. If we start with a random |cubepos|, can we convert it to a pair of |kocsymm| and |permcube| structures, and then back again, with no loss of data? Also, does the edge permutation coordinate in the |kocsymm| match the |em| field of the |permcube| ; @= for (int i=0; i<100000; i++) { cp.randomize() ; kocsymm kc(cp) ; permcube pc(cp) ; if (kc.epsymm != pc.em) error("! mismatch in edge middle occupancy") ; kc.set_coset(cp2) ; pc.set_perm(cp2) ; if (cp != cp2) error("! mismatch in conversion and back") ; } @ Next we test the move routines. Do we get the same results when we use |permcube| and |kocsymm| as we do when we use |cubepos|? @= for (int i=0; i<1000; i++) { cp.randomize() ; kocsymm kc(cp) ; permcube pc(cp) ; int mv = random_move_ext() ; cp.movepc(mv) ; cp2 = cp ; kc.move(mv) ; pc.move(mv) ; kc.set_coset(cp2) ; pc.set_perm(cp2) ; if (cp != cp2) error("! mismatch in move test") ; } @ The next thing we test is canonicalization in |kocsymm|. @= for (int i=0; i<1000; i++) { cp.randomize() ; kocsymm kc(cp) ; for (int m=1; m= int s = 0 ; for (int c=0; c= int mvs[10000] ; for (int i=0; i<10000; i++) mvs[i] = random_move() ; duration() ; for (int i=0; i<10000; i++) for (int j=0; j<10000; j++) kc.move(mvs[j]) ; cout << "Moving 100M kc took " << duration() << endl ; for (int i=0; i<10000; i++) for (int j=0; j<10000; j++) pc.move(mvs[j]) ; cout << "Moving 100M pc took " << duration() << endl ; for (int i=0; i<10000; i++) for (int j=0; j<10000; j++) cp.move(mvs[j]) ; cout << "Moving 100M cp (move) took " << duration() << endl ; for (int i=0; i<10000; i++) for (int j=0; j<10000; j++) cp.movepc(mvs[j]) ; cout << "Moving 100M cp (movepc) took " << duration() << endl ; if (cp < cp2 && pc < pc2 && kc < kc2) cout << "(Ignore this message.)" << endl ; @ We put all the pieces together in our main test routine. @(kocsymm_test.cpp@>= #include "kocsymm.h" #include using namespace std ; int main(int argc, char *argv[]) { if (lrand48() == 0) srand48(getpid()+time(0)) ; kocsymm kc, kc2 ; permcube pc, pc2 ; cubepos cp, cp2 ; @ @; @ @; @ @; @ @; @ @; @ @; }