diff --git a/Cargo.toml b/Cargo.toml index d028b19..9704df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cube-lib" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,5 +9,6 @@ edition = "2021" thiserror = "1.0.50" [dev-dependencies] +itertools = "0.14.0" proptest = "1.7.0" proptest-derive = "0.6.0" diff --git a/src/coord.rs b/src/coord.rs new file mode 100644 index 0000000..dc7309c --- /dev/null +++ b/src/coord.rs @@ -0,0 +1,36 @@ +//! We give a general description of a coordinate, which is a type used to encode coset information +//! of a puzzle. + +/// A coordinate type, encoding cosets of the puzzle P. +pub trait Coordinate

: Copy + Default + Eq { + // TODO this API assumes that coord conversion doesn't require any additional data, perhaps + // this should be changed + /// Obtain the coordinate that corresponds to the given puzzle. + fn from_puzzle(puzzle: &P) -> Self; + + // TODO should this be kept + /// Determine whether the given coordinate represents a solved state + fn solved(self) -> bool { + self.repr() == 0 + } + + /// The number of possible coordinate states. + fn count() -> usize; + + /// A representation of this coordinate as a usize, for use in table lookups. + fn repr(self) -> usize; + + // TODO this might not be ideal it's not very type safe idk + /// Convert the representation of a coordinate to the coordinate itself. We assume 0 + /// corresponds to the solved state. + fn from_repr(n: usize) -> Self; +} + +/// Gives the ability to set a coordinate onto a puzzle. +pub trait FromCoordinate: Sized +where + C: Coordinate, +{ + /// Modify the puzzle so that its coordinate for `C` is `coord`. + fn set_coord(&mut self, coord: C); +} diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs index 2e5e431..b45e53a 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -1,13 +1,182 @@ use super::{Corner, CornerTwist, CubieCube, Edge, EdgeFlip}; +use crate::coord::{Coordinate, FromCoordinate}; -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -struct COCoord(u16); -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -struct CPCoord(u16); -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -struct EOCoord(u16); -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -struct EPCoord(u32); +/// A coordinate representation of the corner orientation of a cube with respect to the U/F faces. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct COCoord(u16); + +/// A coordinate representation of the corner permutation of a cube with respect to the U/F faces. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct CPCoord(u16); + +/// A coordinate representation of the edge orientation of a cube with respect to the U/F faces. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct EOCoord(u16); + +/// A coordinate representation of the edge permutation of a cube with respect to the U/F faces. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct EPCoord(u32); + +// TODO AHHHH +// this is copied from the two phase solver!!! so messy!!! AHHHH +fn set_p_coord( + mut c: usize, + perm: &mut [P; COUNT], + mut bag: Vec

, +) { + let mut n = [0; COUNT]; + for (i, n) in n.iter_mut().enumerate() { + *n = c % (i + 1); + c /= i + 1; + } + + for i in (LOWER..=UPPER).rev() { + let index = n[i - LOWER]; + perm[i] = bag[index]; + bag.remove(index); + } +} + +impl Coordinate for COCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + COCoord(to_o_coord::<8, 3>(&puzzle.co.map(|n| n.into()))) + } + + fn count() -> usize { + // 3^7 + 2187 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + COCoord(n as u16) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: COCoord) { + let mut first = CornerTwist::Oriented; + let mut n = coord.0; + + for i in (1..8).rev() { + self.co[i] = match n % 3 { + 0 => CornerTwist::Oriented, + 1 => { + first = first.anticlockwise(); + CornerTwist::Clockwise + } + 2 => { + first = first.clockwise(); + CornerTwist::AntiClockwise + } + _ => unreachable!(), + }; + n /= 3; + } + + self.co[0] = first; + } +} + +impl Coordinate for CPCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + CPCoord(to_p_coord::<8>(&puzzle.cp.map(|n| n.into())) as u16) + } + + fn count() -> usize { + 40320 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + CPCoord(n as u16) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: CPCoord) { + use crate::cube333::corner::Corner as C; + #[rustfmt::skip] + let bag = vec![C::DBR, C::DBL, C::DFL, C::DFR, C::UBR, C::UBL, C::UFL, C::UFR]; + + set_p_coord::<8, 0, 7, C>(coord.repr(), &mut self.cp, bag); + } +} + +impl Coordinate for EOCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + EOCoord(to_o_coord::<12, 2>(&puzzle.eo.map(|n| n.into()))) + } + + fn count() -> usize { + // 2^11 + 2048 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + EOCoord(n as u16) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: EOCoord) { + let mut first = EdgeFlip::Oriented; + let mut n = coord.0; + + for i in (1..12).rev() { + self.eo[i] = match n % 2 { + 0 => EdgeFlip::Oriented, + 1 => { + first = first.flip(); + EdgeFlip::Flipped + } + _ => unreachable!(), + }; + n /= 2; + } + + self.eo[0] = first; + } +} + +impl Coordinate for EPCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + EPCoord(to_p_coord::<12>(&puzzle.ep.map(|n| n.into()))) + } + + fn count() -> usize { + // a lot + 479001600 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + EPCoord(n as u32) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: EPCoord) { + use crate::cube333::edge::Edge as E; + #[rustfmt::skip] + let bag = vec![E::BR, E::BL, E::FL, E::FR, E::DR, E::DB, E::DL, E::DF, E::UR, E::UB, E::UL, E::UF]; + + set_p_coord::<12, 0, 11, E>(coord.repr(), &mut self.ep, bag); + } +} /// Implementation of a coord cube, representing pieces using coordinates, which are values which /// are isomorphic to arrays represented in a cubie cube. @@ -146,10 +315,10 @@ impl CubieCube { }); } - let co = COCoord(to_o_coord::<8, 3>(&self.co.map(|n| n.into()))); - let cp = CPCoord(to_p_coord::<8>(&self.cp.map(|n| n.into())) as u16); - let eo = EOCoord(to_o_coord::<12, 2>(&self.eo.map(|n| n.into()))); - let ep = EPCoord(to_p_coord::<12>(&self.ep.map(|n| n.into()))); + let co = COCoord::from_puzzle(self); + let cp = CPCoord::from_puzzle(self); + let eo = EOCoord::from_puzzle(self); + let ep = EPCoord::from_puzzle(self); Ok(CoordCube { co, cp, eo, ep }) } @@ -157,10 +326,11 @@ impl CubieCube { #[cfg(test)] mod tests { + use super::*; use crate::cube333::{ + Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube, coordcube::{CoordCube, CubieToCoordError}, moves::{Move333, Move333Type}, - Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube, }; use crate::mv; @@ -246,4 +416,25 @@ mod tests { swap.cp[3] = Corner::UFR; assert!(swap.to_coord().is_ok()); } + + use proptest::prelude::*; + + proptest! { + // TODO this test is not good, it should take a *random state* as input to test that the + // last piece is set correctly, but I haven't been bothered to make that yet... + + #[test] + fn convert_invertible_co(c in (0..2187u16).prop_map(COCoord)) { + let mut cube = CubieCube::SOLVED; + cube.set_coord(c); + assert_eq!(c, COCoord::from_puzzle(&cube)); + } + + #[test] + fn convert_invertible_eo(c in (0..2048u16).prop_map(EOCoord)) { + let mut cube = CubieCube::SOLVED; + cube.set_coord(c); + assert_eq!(c, EOCoord::from_puzzle(&cube)); + } + } } diff --git a/src/cube333/corner.rs b/src/cube333/corner.rs index c2dc4b4..1721117 100644 --- a/src/cube333/corner.rs +++ b/src/cube333/corner.rs @@ -4,15 +4,16 @@ use crate::error::TryFromIntToEnumError; /// An enum for every corner piece location. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[allow(missing_docs)] +#[repr(u8)] pub enum Corner { - UFR, - UFL, - UBL, - UBR, - DFR, - DFL, - DBL, - DBR, + UFR = 0, + UFL = 1, + UBL = 2, + UBR = 3, + DFR = 4, + DFL = 5, + DBL = 6, + DBR = 7, } use Corner as C; @@ -47,16 +48,7 @@ impl Corner { impl From for u8 { fn from(value: Corner) -> Self { - match value { - C::UFR => 0, - C::UFL => 1, - C::UBL => 2, - C::UBR => 3, - C::DFR => 4, - C::DFL => 5, - C::DBL => 6, - C::DBR => 7, - } + value as u8 } } @@ -81,21 +73,18 @@ impl TryFrom for Corner { /// An enum for every corner twist case. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[allow(missing_docs)] +#[repr(u8)] pub enum CornerTwist { - Oriented, - Clockwise, - AntiClockwise, + Oriented = 0, + Clockwise = 1, + AntiClockwise = 2, } use CornerTwist as CT; impl From for u8 { fn from(value: CornerTwist) -> Self { - match value { - CornerTwist::Oriented => 0, - CornerTwist::Clockwise => 1, - CornerTwist::AntiClockwise => 2, - } + value as u8 } } @@ -139,6 +128,15 @@ impl CornerTwist { CT::AntiClockwise => self.anticlockwise(), } } + + /// Calculate the inverse twist of a given twist. + pub fn inverse(self) -> CornerTwist { + match self { + CT::Oriented => CT::Oriented, + CT::Clockwise => CT::AntiClockwise, + CT::AntiClockwise => CT::Clockwise, + } + } } /// An enum to represent every corner sticker position. diff --git a/src/cube333/edge.rs b/src/cube333/edge.rs index 585e5c5..3cc01bd 100644 --- a/src/cube333/edge.rs +++ b/src/cube333/edge.rs @@ -4,19 +4,20 @@ use crate::error::TryFromIntToEnumError; /// An enum for every edge piece location. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[allow(missing_docs)] +#[repr(u8)] pub enum Edge { - UF, - UL, - UB, - UR, - DF, - DL, - DB, - DR, - FR, - FL, - BL, - BR, + UF = 0, + UL = 1, + UB = 2, + UR = 3, + DF = 4, + DL = 5, + DB = 6, + DR = 7, + FR = 8, + FL = 9, + BL = 10, + BR = 11, } use Edge as E; @@ -55,24 +56,26 @@ impl Edge { E::BL, E::BR, ]; + + /// Determines whether this edge sits on the E slice. + pub fn e_slice(&self) -> bool { + matches!(self, E::FR | E::FL | E::BL | E::BR) + } + + /// Determines whether this edge sits on the M slice. + pub fn m_slice(&self) -> bool { + matches!(self, E::UF | E::UB | E::DF | E::DB) + } + + /// Determines whether this edge sits on the S slice. + pub fn s_slice(&self) -> bool { + matches!(self, E::UL | E::UR | E::DL | E::DR) + } } impl From for u8 { fn from(value: Edge) -> Self { - match value { - E::UF => 0, - E::UL => 1, - E::UB => 2, - E::UR => 3, - E::DF => 4, - E::DL => 5, - E::DB => 6, - E::DR => 7, - E::FR => 8, - E::FL => 9, - E::BL => 10, - E::BR => 11, - } + value as u8 } } @@ -101,19 +104,17 @@ impl TryFrom for Edge { /// An enum to tell if an edge is flipped or not. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[allow(missing_docs)] +#[repr(u8)] pub enum EdgeFlip { - Oriented, - Flipped, + Oriented = 0, + Flipped = 1, } use EdgeFlip as EF; impl From for u8 { fn from(value: EdgeFlip) -> Self { - match value { - EF::Oriented => 0, - EF::Flipped => 1, - } + value as u8 } } diff --git a/src/cube333/mod.rs b/src/cube333/mod.rs index 29a86cb..8217729 100644 --- a/src/cube333/mod.rs +++ b/src/cube333/mod.rs @@ -14,6 +14,8 @@ pub mod edge; /// Defines move types and implements application of moves to the CubieCube. pub mod moves; +pub mod two_phase_solver; + mod cubiecube; use corner::{Corner, CornerPos, CornerTwist}; @@ -417,8 +419,8 @@ impl std::fmt::Display for StickerCube { mod tests { #[test] fn pieces_on_solved_cube() { - use super::edge::EdgePos::*; use super::StickerCube; + use super::edge::EdgePos::*; assert_eq!(StickerCube::SOLVED.edge_at(UB).unwrap(), UB, "UB"); assert_eq!(StickerCube::SOLVED.edge_at(UR).unwrap(), UR, "UR"); assert_eq!(StickerCube::SOLVED.edge_at(UF).unwrap(), UF, "UF"); diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 6fd99a3..9df0a29 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -23,6 +23,20 @@ pub enum Move333Type { B, } +impl Move333Type { + /// The move type on the face opposite to the given one. + pub fn opposite(self) -> Move333Type { + match self { + Move333Type::R => Move333Type::L, + Move333Type::L => Move333Type::R, + Move333Type::U => Move333Type::D, + Move333Type::D => Move333Type::U, + Move333Type::F => Move333Type::B, + Move333Type::B => Move333Type::F, + } + } +} + /// Stores a move type and counter. An anti-clockwise move will have a count of 3. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(test, derive(Arbitrary))] @@ -50,10 +64,7 @@ impl crate::moves::Move for Move333 { } } - fn cancel(self, b: Self) -> Cancellation - where - Self: Sized, - { + fn cancel(self, b: Self) -> Cancellation { if self.ty == b.ty { let count = (self.count + b.count) % 4; if count == 0 { @@ -155,7 +166,7 @@ const CO_OFFSETS: [[u8; 8]; 6] = [ [1, 2, 0, 0, 2, 1, 0, 0], [0, 0, 1, 2, 0, 0, 2, 1], ]; -const CP_OFFSETS: [[usize; 8]; 6] = [ +const CP_OFFSETS: [[u8; 8]; 6] = [ [4, 1, 2, 0, 7, 5, 6, 3], [0, 2, 6, 3, 4, 1, 5, 7], [3, 0, 1, 2, 4, 5, 6, 7], @@ -171,7 +182,7 @@ const EO_OFFSETS: [[u8; 12]; 6] = [ [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0], [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1], ]; -const EP_OFFSETS: [[usize; 12]; 6] = [ +const EP_OFFSETS: [[u8; 12]; 6] = [ [0, 1, 2, 8, 4, 5, 6, 11, 7, 9, 10, 3], [0, 10, 2, 3, 4, 9, 6, 7, 8, 1, 5, 11], [3, 0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11], @@ -181,27 +192,21 @@ const EP_OFFSETS: [[usize; 12]; 6] = [ ]; impl CubieCube { - // This should really not be borrowing... - /// Copy the cube and apply an algorithm to it. - pub fn make_moves(&self, mvs: MoveSequence) -> CubieCube { - let mut r = self.clone(); - for mv in mvs.0 { - r = r.make_move(mv); - } - r + /// Apply an algorithm to a cube + pub fn make_moves(self, mvs: MoveSequence) -> CubieCube { + mvs.0.into_iter().fold(self, |c, m| c.make_move(m)) } // This function doesn't really need to be fast since coordinates exist - /// Copy the cube, apply a move to it, then return the new cube. - pub fn make_move(&self, mv: Move333) -> CubieCube { - let mut r = self.clone(); - for _ in 0..mv.count { - r = r.make_move_type(mv.ty); - } - r + /// Apply a move to a cube. + pub fn make_move(self, mv: Move333) -> CubieCube { + (0..mv.count).fold(self, |c, _| c.make_move_type(mv.ty)) } - fn make_move_type(&self, mv: Move333Type) -> CubieCube { + // TODO make this const please that would be very handy :) + + /// Make a single application of a move + pub fn make_move_type(self, mv: Move333Type) -> CubieCube { let co_offsets = CO_OFFSETS[mv as usize]; let cp_offsets = CP_OFFSETS[mv as usize]; let eo_offsets = EO_OFFSETS[mv as usize]; @@ -218,13 +223,13 @@ impl CubieCube { let mut ep = [0; 12]; for i in 0..8 { - co[i] = (selfco[cp_offsets[i]] + co_offsets[i]) % 3; - cp[i] = selfcp[cp_offsets[i]]; + co[i] = (selfco[cp_offsets[i] as usize] + co_offsets[i]) % 3; + cp[i] = selfcp[cp_offsets[i] as usize]; } for i in 0..12 { - eo[i] = (selfeo[ep_offsets[i]] + eo_offsets[i]) % 2; - ep[i] = selfep[ep_offsets[i]]; + eo[i] = (selfeo[ep_offsets[i] as usize] + eo_offsets[i]) % 2; + ep[i] = selfep[ep_offsets[i] as usize]; } let co = co.map(|n| n.try_into().unwrap()); @@ -234,6 +239,46 @@ impl CubieCube { CubieCube { co, cp, eo, ep } } + + /// Multiply two cube states in the Rubik's cube group. + pub fn multiply_cube(self, other: CubieCube) -> CubieCube { + let mut result = CubieCube::SOLVED; + + for i in 0..8 { + let oa = self.co[other.cp[i] as usize]; + let ob = other.co[i]; + let o = oa.twist_by(ob); + result.co[i] = o; + result.cp[i] = self.cp[other.cp[i] as usize]; + } + + for i in 0..12 { + let oa = self.eo[other.ep[i] as usize]; + let ob = other.eo[i]; + let o = oa.flip_by(ob); + result.eo[i] = o; + result.ep[i] = self.ep[other.ep[i] as usize]; + } + + result + } + + /// Get the inverse in the Rubik's cube group. + pub fn inverse(self) -> CubieCube { + let mut result = CubieCube::SOLVED; + + for i in 0..8 { + result.co[self.cp[i] as usize] = self.co[i].inverse(); + result.cp[self.cp[i] as usize] = (i as u8).try_into().unwrap(); + } + + for i in 0..12 { + result.eo[self.ep[i] as usize] = self.eo[i]; + result.ep[self.ep[i] as usize] = (i as u8).try_into().unwrap(); + } + + result + } } #[cfg(test)] @@ -257,23 +302,31 @@ mod tests { proptest! { #[test] - fn cancel_same_moves(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) { + fn cancel_same_moves(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) { let cancelled = mvs.clone().cancel(); assert!(cancelled.len() <= mvs.len()); assert_eq!(CubieCube::SOLVED.make_moves(mvs), CubieCube::SOLVED.make_moves(cancelled)); } #[test] - fn invert_identity(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) { + fn invert_identity(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) { let cancelled = mvs.clone().cancel(); assert_eq!(CubieCube::SOLVED.make_moves(mvs.clone()).make_moves(mvs.inverse()), CubieCube::SOLVED); assert!(cancelled.clone().append(cancelled.clone().inverse()).cancel().is_empty()); } #[test] - fn cancel_idemotent(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) { + fn cancel_idemotent(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) { let cancelled = mvs.clone().cancel(); assert_eq!(cancelled.clone().cancel(), cancelled); } + + #[test] + fn inverse_apply(mvs in vec(any::(), 0..20).prop_map(MoveSequence)) { + // TODO use real random state for this proptest! doing random moves is silly! + let fake_random_state = CubieCube::SOLVED.make_moves(mvs); + assert_eq!(CubieCube::SOLVED, fake_random_state.clone().multiply_cube(fake_random_state.clone().inverse())); + assert_eq!(CubieCube::SOLVED, fake_random_state.clone().inverse().multiply_cube(fake_random_state.clone())); + } } } diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs new file mode 100644 index 0000000..14d2f5e --- /dev/null +++ b/src/cube333/two_phase_solver/coords.rs @@ -0,0 +1,507 @@ +//! This module contains the coordinate representations of cube states relevant to the two phases +//! of these solver. + +use super::symmetry::{DrSymmetry, HalfSymmetry, Symmetry}; +use crate::coord::{Coordinate, FromCoordinate}; +use crate::cube333::{ + CubieCube, + coordcube::{COCoord, CPCoord, EOCoord}, +}; + +// TODO this is kinda unreadable lol +// this is copied from coordcube.rs then modified hmmm maybe copy pasting isn't ideal +fn to_p_coord( + arr: &[u8; COUNT], +) -> u16 { + (0..UPPER - LOWER).rev().fold(0, |acc, idx| { + (acc * (idx + 1) as u16) + + arr[LOWER..LOWER + idx] + .iter() + .filter(|&&x| x > arr[LOWER + idx]) + .count() as u16 + }) +} + +fn set_p_coord( + mut c: usize, + perm: &mut [P; COUNT], + mut bag: Vec

, +) { + let mut n = [0; COUNT]; + for (i, n) in n.iter_mut().enumerate() { + *n = c % (i + 1); + c /= i + 1; + } + + for i in (LOWER..=UPPER).rev() { + let index = n[i - LOWER]; + perm[i] = bag[index]; + bag.remove(index); + } +} + +/// Coordinate for positions of E slice edges (ignoring what the edges actually are) +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct ESliceEdgeCoord(u16); + +/// Coordinate for positions of U/D layer edges, assuming the cube is in and says in domino +/// reduction. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct DominoEPCoord(u16); + +/// Coordinate for positions of the E slice edges, assuming the cube is in and says in domino +/// reduction. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct DominoESliceCoord(u16); + +impl Coordinate for ESliceEdgeCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + // https://kociemba.org/math/UDSliceCoord.htm + let mut r = 0; + + // c is meant to be n choose (k-1) if k >= 1 + let mut k = 0; + let mut c = 0u16; + + for n in 0..12 { + if puzzle.ep[n as usize].e_slice() { + // every time we reach an e slice edge, we increment k, and then have to fix the + // value of c. + k += 1; + if k == 1 { + // if k was previously zero, c was 0, so just set c to be the next n value + c = 1; + } else { + // Otherwise, c was previously equal to n choose k-1, and we want to update it + // to be n+1 choose k. To do this we can use the identity + // n choose k = (n/k) * (n-1 choose k-1) + // we have to divide at the end do dodge floor division + debug_assert!((c * (n + 1)).is_multiple_of(k - 1)); + c = c * (n + 1) / (k - 1); + } + } else if k > 0 { + r += c; + // In this case we want to update n choose k-1 to be n+1 choose k-1. To do this we + // can use the identity + // (n choose k) = (n/(n-k)) (n-1 choose k) + debug_assert!((c * (n + 1)).is_multiple_of(n + 1 - k + 1)); + c = c * (n + 1) / (n + 1 - k + 1); + } + } + + ESliceEdgeCoord(r) + } + + fn count() -> usize { + 495 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + ESliceEdgeCoord(n as u16) + } +} + +fn binom(n: usize, k: usize) -> usize { + (n - k + 1..=n).product::() / (1..=k).product::() +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: ESliceEdgeCoord) { + self.ep = CubieCube::SOLVED.ep; + // Identify the locations of each e slice edge. + let mut c = coord.repr(); + let mut poses = [0; 4]; + let (mut n, mut k) = (11, 3); + let mut p = 0; + for i in (0..12).rev() { + let b = binom(n, k); + if b > c { + poses[p] = i; + p += 1; + if k == 0 { + break; + } + k -= 1; + } else { + c -= b; + } + n -= 1; + } + // swap e slice edges (8..=11 is the e slice edge positions) + for (a, b) in poses.into_iter().rev().zip(8..=11) { + self.ep.swap(a, b); + } + } +} + +impl Coordinate for DominoEPCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + DominoEPCoord(to_p_coord::<12, 0, 8>(&puzzle.ep.map(|n| n.into()))) + } + + fn count() -> usize { + 40320 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + DominoEPCoord(n as u16) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: DominoEPCoord) { + // UHHH i hope this is fine... + // self.ep = CubieCube::SOLVED.ep; + + use crate::cube333::edge::Edge as E; + let bag = vec![E::DR, E::DB, E::DL, E::DF, E::UR, E::UB, E::UL, E::UF]; + + set_p_coord::<12, 0, 7, E>(coord.repr(), &mut self.ep, bag); + } +} + +impl Coordinate for DominoESliceCoord { + fn from_puzzle(puzzle: &CubieCube) -> Self { + DominoESliceCoord(to_p_coord::<12, 8, 12>(&puzzle.ep.map(|n| n.into()))) + } + + fn count() -> usize { + 24 + } + + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + DominoESliceCoord(n as u16) + } +} + +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: DominoESliceCoord) { + //self.ep = CubieCube::SOLVED.ep; + + use crate::cube333::edge::Edge as E; + let bag = vec![E::BR, E::BL, E::FL, E::FR]; + + set_p_coord::<12, 8, 11, E>(coord.repr(), &mut self.ep, bag); + } +} + +/// A symmetry coordinate over a `Symmetry`. A SymCoordinate should include both what equivalence +/// class the coordinate is in, along with the symmetry it has from the representant. +pub trait SymCoordinate: Copy + Default + Eq { + type Sym: Symmetry; + type Raw: Coordinate; + + /// The number of equivalence classes this coordinate encodes modulo symmetry. + fn classes() -> usize; + + /// Determine if the given coordinate represents a solved state + fn solved(self) -> bool { + self.class() == 0 + } + + /// A representation of this coordinate as a usize, for use, in table lookups. + fn repr(self) -> (usize, Self::Sym) { + (self.class(), self.sym()) + } + + /// Convert the representation of a coordinate to the coordinate itself. We assume 0 with the + /// identity symmetry corresponds to the solved state. + fn from_repr(idx: usize, sym: Self::Sym) -> Self; + + /// Obtain the equivalence class this coordinate represents. + fn class(self) -> usize; + + /// Obtain the underlying symmetry of this ooordinate. + fn sym(self) -> Self::Sym; +} + +pub struct RawSymTable +where + CubieCube: FromCoordinate, +{ + raw_to_sym: Box<[S]>, + class_to_repr: Box<[S::Raw]>, +} + +impl RawSymTable +where + CubieCube: FromCoordinate, +{ + pub fn generate() -> Self { + let mut raw_to_sym = vec![S::default(); S::Raw::count()].into_boxed_slice(); + let mut class_to_repr = vec![S::Raw::default(); S::classes()].into_boxed_slice(); + + let mut sym_idx = 0; + + for raw in (0..S::Raw::count()).map(S::Raw::from_repr) { + // Skip entries we have already initialised (note that states symmetric to the solved + // state will not have solved SymCoordinate since a SymCoordinate will include its + // symmetry) + if raw_to_sym[raw.repr()] != S::default() { + continue; + } + + let mut c = CubieCube::SOLVED; + c.set_coord(raw); + + // Then we go over every coordinate symmetric to this one, and update the tables based + // on them. + for sym in S::Sym::get_all() { + let d = c.clone().conjugate_symmetry(sym); + let raw2 = S::Raw::from_puzzle(&d); + raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); + } + + class_to_repr[sym_idx] = raw; + sym_idx += 1; + } + + RawSymTable { + raw_to_sym, + class_to_repr, + } + } + + pub fn raw_to_sym(&self, raw: S::Raw) -> S { + self.raw_to_sym[raw.repr()] + } + + pub fn index_to_repr(&self, idx: usize) -> S::Raw { + self.class_to_repr[idx] + } + + pub fn puzzle_to_sym(&self, p: &CubieCube) -> S { + self.raw_to_sym(S::Raw::from_puzzle(p)) + } + + #[cfg(test)] + pub fn sym_to_raw(&self, sym: S) -> S::Raw { + let repr = self.index_to_repr(sym.class()); + let mut c = CubieCube::SOLVED; + c.set_coord(repr); + let d = c.conjugate_symmetry(sym.sym()); + S::Raw::from_puzzle(&d) + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct COSymCoord(u16); + +impl SymCoordinate for COSymCoord { + type Sym = HalfSymmetry; + + type Raw = COCoord; + + fn classes() -> usize { + 324 + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + COSymCoord((idx as u16) << 3 | (sym.repr() as u16)) + } + + fn class(self) -> usize { + (self.0 >> 3) as usize + } + + fn sym(self) -> Self::Sym { + HalfSymmetry::from_repr((self.0 & 7) as usize) + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct EOSymCoord(u16); + +impl SymCoordinate for EOSymCoord { + type Sym = HalfSymmetry; + + type Raw = EOCoord; + + fn classes() -> usize { + 336 + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + EOSymCoord((idx as u16) << 3 | (sym.repr() as u16)) + } + + fn class(self) -> usize { + (self.0 >> 3) as usize + } + + fn sym(self) -> Self::Sym { + HalfSymmetry::from_repr((self.0 & 7) as usize) + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct CPSymCoord(u16); + +impl SymCoordinate for CPSymCoord { + type Sym = DrSymmetry; + + type Raw = CPCoord; + + fn classes() -> usize { + 2768 + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + CPSymCoord((idx as u16) << 4 | (sym.repr() as u16)) + } + + fn class(self) -> usize { + (self.0 >> 4) as usize + } + + fn sym(self) -> Self::Sym { + DrSymmetry::from_repr((self.0 & 15) as usize) + } +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct DominoEPSymCoord(u16); + +impl SymCoordinate for DominoEPSymCoord { + type Sym = DrSymmetry; + + type Raw = DominoEPCoord; + + fn classes() -> usize { + 2768 + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + DominoEPSymCoord((idx as u16) << 4 | (sym.repr() as u16)) + } + + fn class(self) -> usize { + (self.0 >> 4) as usize + } + + fn sym(self) -> Self::Sym { + DrSymmetry::from_repr((self.0 & 15) as usize) + } +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use itertools::Itertools; + + use super::super::move_tables::{DrMove, SubMove}; + use super::*; + use crate::{ + coord::Coordinate, + cube333::{CubieCube, moves::Move333}, + moves::MoveSequence, + }; + + use proptest::collection::vec; + use proptest::prelude::*; + + fn sets_coord + std::fmt::Debug>(mvs: MoveSequence) + where + CubieCube: FromCoordinate, + { + let coord = C::from_puzzle(&CubieCube::SOLVED.make_moves(mvs)); + let mut d = CubieCube::SOLVED; + d.set_coord(coord); + assert_eq!(C::from_puzzle(&d), coord); + } + + #[test] + fn from_coord() { + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + sets_coord::(mvs); + }); + } + + #[test] + fn from_coord_dr() { + proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| { + sets_coord::(mvs.clone()); + sets_coord::(mvs); + }); + } + + #[test] + fn e_slice_edge_uniqueness() { + let mut coords = HashSet::new(); + for poses in (0..12).combinations(4) { + let mut cube = CubieCube::SOLVED; + + for (a, b) in poses.into_iter().zip(8..12) { + cube.ep.swap(a, b); + } + + let coord = ESliceEdgeCoord::from_puzzle(&cube); + assert!(!coords.contains(&coord)); + coords.insert(coord); + } + assert!(coords.len() == ESliceEdgeCoord::count()); + assert!(coords.iter().all(|c| c.repr() < ESliceEdgeCoord::count())); + } + + #[test] + fn sym_table_generates() { + RawSymTable::::generate(); + RawSymTable::::generate(); + RawSymTable::::generate(); + RawSymTable::::generate(); + } + + #[test] + fn sym_class_repr_in_class() { + let co_sym = RawSymTable::::generate(); + let eo_sym = RawSymTable::::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + let c = CubieCube::SOLVED.make_moves(mvs); + let co = COCoord::from_puzzle(&c); + assert_eq!(co_sym.raw_to_sym(co_sym.index_to_repr(co_sym.raw_to_sym(co).class())).class(), co_sym.raw_to_sym(co).class()); + let eo = EOCoord::from_puzzle(&c); + assert_eq!(eo_sym.raw_to_sym(eo_sym.index_to_repr(eo_sym.raw_to_sym(eo).class())).class(), eo_sym.raw_to_sym(eo).class()); + }); + let cp_sym = RawSymTable::::generate(); + let ep_sym = RawSymTable::::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| { + let c = CubieCube::SOLVED.make_moves(mvs); + let cp = CPCoord::from_puzzle(&c); + assert_eq!(cp_sym.raw_to_sym(cp_sym.index_to_repr(cp_sym.raw_to_sym(cp).class())).class(), cp_sym.raw_to_sym(cp).class()); + let ep = DominoEPCoord::from_puzzle(&c); + assert_eq!(ep_sym.raw_to_sym(ep_sym.index_to_repr(ep_sym.raw_to_sym(ep).class())).class(), ep_sym.raw_to_sym(ep).class()); + }); + } + + fn raw_to_sym_right_inverse() + where + CubieCube: FromCoordinate, + S::Raw: std::fmt::Debug, + { + let table = RawSymTable::::generate(); + for raw in (0..S::Raw::count()).map(S::Raw::from_repr) { + assert_eq!(raw, table.sym_to_raw(table.raw_to_sym(raw))); + } + } + + #[test] + fn raw_to_sym_right_inverse_all() { + raw_to_sym_right_inverse::(); + raw_to_sym_right_inverse::(); + raw_to_sym_right_inverse::(); + raw_to_sym_right_inverse::(); + } +} diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs new file mode 100644 index 0000000..bc0bf69 --- /dev/null +++ b/src/cube333/two_phase_solver/mod.rs @@ -0,0 +1,356 @@ +//! An implementation of the two phase solver described [here](https://kociemba.org/cube.htm). + +mod coords; +mod move_tables; +mod prune; +mod symmetry; + +use coords::SymCoordinate; +use move_tables::{DrMove, SubMove}; + +use super::{CubieCube, moves::Move333}; +use crate::coord::Coordinate; +use crate::moves::MoveSequence; + +use std::rc::Rc; + +struct Phase1; +struct Phase2; + +/// Organisation trait for each phase +trait Phase { + type Cube: PhaseCube; + type Move: SubMove; + type Prune: PhasePrune; + + /// Convert a cubiecube into a phase encoding cube + fn get_cube(sym_tables: &SymTables, cube: &CubieCube) -> Self::Cube; + + /// Apply a move to a cube + fn make_move(mover: &Mover, cube: Self::Cube, m: Self::Move) -> Self::Cube; + + /// Get the initial pruning value of a cube + fn init_prune(pruner: &Pruner, c: Self::Cube) -> Self::Prune; + + /// Update a pruning value based on a next state + fn update_prune(pruner: &Pruner, p: Self::Prune, c: Self::Cube) -> Self::Prune; +} + +impl Phase for Phase1 { + type Cube = P1Cube; + type Move = Move333; + type Prune = P1PruneState; + + fn get_cube(sym_tables: &SymTables, c: &CubieCube) -> P1Cube { + P1Cube { + co: sym_tables.co.puzzle_to_sym(c), + eo: sym_tables.eo.puzzle_to_sym(c), + slice: coords::ESliceEdgeCoord::from_puzzle(c), + } + } + + fn make_move(mover: &Mover, cube: P1Cube, m: Move333) -> P1Cube { + P1Cube { + co: mover.p1_co.make_move(cube.co, m), + eo: mover.p1_eo.make_move(cube.eo, m), + slice: mover.p1_slice.make_move(cube.slice, m), + } + } + + fn init_prune(pruner: &Pruner, c: P1Cube) -> P1PruneState { + P1PruneState { + co_slice: pruner.p1_co.bound(c.co, c.slice), + eo_slice: pruner.p1_eo.bound(c.eo, c.slice), + } + } + + fn update_prune(pruner: &Pruner, p: P1PruneState, c: P1Cube) -> P1PruneState { + P1PruneState { + co_slice: pruner.p1_co.update(p.co_slice, c.co, c.slice), + eo_slice: pruner.p1_eo.update(p.eo_slice, c.eo, c.slice), + } + } +} + +impl Phase for Phase2 { + type Cube = P2Cube; + type Move = DrMove; + type Prune = P2PruneState; + + fn get_cube(sym_tables: &SymTables, c: &CubieCube) -> P2Cube { + P2Cube { + cp: sym_tables.cp.puzzle_to_sym(c), + ep: sym_tables.ep.puzzle_to_sym(c), + slice: coords::DominoESliceCoord::from_puzzle(c), + } + } + + fn make_move(mover: &Mover, cube: P2Cube, m: DrMove) -> P2Cube { + P2Cube { + cp: mover.p2_cp.make_move(cube.cp, m), + ep: mover.p2_ep.make_move(cube.ep, m), + slice: mover.p2_slice.make_move(cube.slice, m), + } + } + + fn init_prune(pruner: &Pruner, c: P2Cube) -> P2PruneState { + P2PruneState { + cp_slice: pruner.p2_cp.bound(c.cp, c.slice), + ep_slice: pruner.p2_ep.bound(c.ep, c.slice), + } + } + + fn update_prune(pruner: &Pruner, p: P2PruneState, c: P2Cube) -> P2PruneState { + P2PruneState { + cp_slice: pruner.p2_cp.update(p.cp_slice, c.cp, c.slice), + ep_slice: pruner.p2_ep.update(p.ep_slice, c.ep, c.slice), + } + } +} + +trait PhaseCube: Copy { + fn is_solved(self) -> bool; +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +struct P1Cube { + co: coords::COSymCoord, + eo: coords::EOSymCoord, + slice: coords::ESliceEdgeCoord, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +struct P2Cube { + cp: coords::CPSymCoord, + ep: coords::DominoEPSymCoord, + slice: coords::DominoESliceCoord, +} + +impl PhaseCube for P1Cube { + fn is_solved(self) -> bool { + self.co.solved() && self.eo.solved() && self.slice.solved() + } +} + +impl PhaseCube for P2Cube { + fn is_solved(self) -> bool { + self.cp.solved() && self.ep.solved() && self.slice.solved() + } +} + +trait PhasePrune: Copy { + fn val(self) -> usize; +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +struct P1PruneState { + co_slice: usize, + eo_slice: usize, +} + +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +struct P2PruneState { + cp_slice: usize, + ep_slice: usize, +} + +impl PhasePrune for P1PruneState { + fn val(self) -> usize { + self.co_slice.max(self.eo_slice) + } +} + +impl PhasePrune for P2PruneState { + fn val(self) -> usize { + self.cp_slice.max(self.ep_slice) + } +} + +struct Mover { + p1_co: Rc, + p1_eo: Rc, + p1_slice: Rc, + p2_cp: Rc, + p2_ep: Rc, + p2_slice: Rc, +} + +struct Pruner { + p1_co: prune::ESliceTwistPruneTable, + p1_eo: prune::ESliceFlipPruneTable, + p2_cp: prune::DominoSliceCPPruneTable, + p2_ep: prune::DominoSliceEPPruneTable, +} + +struct SymTables { + co: Rc>, + eo: Rc>, + cp: Rc>, + ep: Rc>, +} + +/// A cube solver that uses Kociemba's two phase algorithm. +pub struct Solver { + mover: Mover, + pruner: Pruner, + sym_tables: SymTables, +} + +impl Default for Solver { + fn default() -> Self { + Self::new() + } +} + +enum SearchResult { + Found, + Bound(usize), +} + +impl Solver { + /// Create a solver. + pub fn new() -> Self { + // this is so ugly... + let co_sym = Rc::new(coords::RawSymTable::generate()); + let eo_sym = Rc::new(coords::RawSymTable::generate()); + let cp_sym = Rc::new(coords::RawSymTable::generate()); + let ep_sym = Rc::new(coords::RawSymTable::generate()); + let p1_co = Rc::new(move_tables::SymMoveTable::generate(&co_sym)); + let p1_eo = Rc::new(move_tables::SymMoveTable::generate(&eo_sym)); + let p1_slice = Rc::new(move_tables::MoveTable::generate()); + let p2_cp = Rc::new(move_tables::SymMoveTable::generate(&cp_sym)); + let p2_ep = Rc::new(move_tables::SymMoveTable::generate(&ep_sym)); + let p2_slice = Rc::new(move_tables::MoveTable::generate()); + + let mover = Mover { + p1_co, + p1_eo, + p1_slice, + p2_cp, + p2_ep, + p2_slice, + }; + + let p1_co = prune::SymRawPruningTable::generate( + co_sym.clone(), + mover.p1_co.clone(), + mover.p1_slice.clone(), + ); + let p1_eo = prune::SymRawPruningTable::generate( + eo_sym.clone(), + mover.p1_eo.clone(), + mover.p1_slice.clone(), + ); + let p2_cp = prune::SymRawPruningTable::generate( + cp_sym.clone(), + mover.p2_cp.clone(), + mover.p2_slice.clone(), + ); + let p2_ep = prune::SymRawPruningTable::generate( + ep_sym.clone(), + mover.p2_ep.clone(), + mover.p2_slice.clone(), + ); + + let pruner = Pruner { + p1_co, + p1_eo, + p2_cp, + p2_ep, + }; + + let sym_tables = SymTables { + co: co_sym, + eo: eo_sym, + cp: cp_sym, + ep: ep_sym, + }; + + Self { + mover, + pruner, + sym_tables, + } + } + + /// Obtain a solving sequence for the cube (such that applying the sequence solves the cube). + pub fn solve(&self, cube: CubieCube) -> MoveSequence { + let p1 = self.solve_phase::(&cube); + let cube = cube.make_moves(MoveSequence(p1.clone())); + let mut p2 = self.solve_phase::(&cube); + + let mut sol = p1; + sol.append(&mut p2); + MoveSequence(sol).cancel() + } + + fn solve_phase(&self, cube: &CubieCube) -> Vec { + // This is just ida*! go read about it yourself!! grrrrrr + let cube = P::get_cube(&self.sym_tables, cube); + let prune = P::init_prune(&self.pruner, cube); + let mut sol = Vec::new(); + + let mut depth = 0; + while depth < 20 { + match self.search_phase::

(cube, prune, &mut sol, 0, depth) { + SearchResult::Found => return sol.into_iter().map(|m| m.into_move()).collect(), + SearchResult::Bound(d) => depth = d, + } + } + + panic!("No phase solution found in 20 moves") + } + + fn search_phase( + &self, + cube: P::Cube, + prune: P::Prune, + sol: &mut Vec, + cost: usize, + depth: usize, + ) -> SearchResult { + if cube.is_solved() { + return SearchResult::Found; + } + let estimate = cost + prune.val(); + if estimate > depth { + return SearchResult::Bound(estimate); + } + let mut min = usize::MAX; + let last = sol.last().copied(); + for &m in P::Move::MOVE_LIST.iter().filter(|&&m| { + last.is_none_or(|l| l.axis() != m.axis() && l.axis() != m.axis().opposite()) + }) { + let cube2 = P::make_move(&self.mover, cube, m); + let prune2 = P::update_prune(&self.pruner, prune, cube2); + sol.push(m); + + match self.search_phase::

(cube2, prune2, sol, cost + 1, depth) { + SearchResult::Found => return SearchResult::Found, + SearchResult::Bound(depth) => min = min.min(depth), + } + + sol.pop(); + } + + SearchResult::Bound(min) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use proptest::collection::vec; + use proptest::prelude::*; + + #[test] + fn solve() { + let solver = Solver::new(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + let cube = CubieCube::SOLVED.make_moves(mvs.clone()); + let sol = solver.solve(cube.clone()); + assert_eq!(cube.make_moves(sol), CubieCube::SOLVED); + }); + } +} diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs new file mode 100644 index 0000000..880000e --- /dev/null +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -0,0 +1,395 @@ +//! Move tables for each coordinate type + +use crate::coord::{Coordinate, FromCoordinate}; +use crate::cube333::CubieCube; +use crate::cube333::moves::{Move333, Move333Type}; +#[cfg(test)] +use crate::moves::MoveSequence; +use crate::moves::{Cancellation, Move}; + +use super::coords::{ + COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, + RawSymTable, SymCoordinate, +}; +use super::symmetry::{SymMoveConjTable, SymMultTable}; + +use std::marker::PhantomData; + +#[cfg(test)] +use proptest::strategy::Strategy; +#[cfg(test)] +use proptest_derive::Arbitrary; + +// TODO This may be generalised later, but for now it'll be specialised to just `CubieCube` + +/// A type that encodes a subset of the set of 3x3 moves, e.g. DR moves. +pub trait SubMove: Move + Copy +where + Self: 'static, +{ + /// Interpret a move as a normal move to be applied to a `CubieCube`. + fn into_move(self) -> Move333; + + fn axis(self) -> Move333Type { + self.into_move().ty + } + + /// The list of all moves that this type encodes. The length of the returned vector should be + /// `count()`. + const MOVE_LIST: &'static [Self]; + + /// Returns all of the states that come from applying each move to the given puzzle, along with + /// the given move. + fn successor_states(puzzle: CubieCube) -> impl Iterator { + Self::MOVE_LIST + .iter() + .map(move |m| (*m, puzzle.clone().make_move(m.into_move()))) + } + + /// Get the index of this move in the move list. + fn index(self) -> usize; +} + +/// A move table, which stores mappings of coordinate + move pairs to the coordinate that results +/// from applying the move. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MoveTable, const MOVES: usize> { + table: Box<[[C; MOVES]]>, + _phantom: PhantomData, +} + +impl, const MOVES: usize> MoveTable { + /// Generate a move table. This is slightly expensive so making move tables repeatedly should + /// be avoided, since the resulting move table generated will always be identical. + pub fn generate() -> Self { + let mut visited = vec![false; C::count()]; + let mut stack = vec![CubieCube::SOLVED]; + visited[0] = true; + + let mut table: Box<[[C; MOVES]]> = + vec![std::array::from_fn(|_| Default::default()); C::count()].into_boxed_slice(); + + while let Some(cur_cube) = stack.pop() { + let c = C::from_puzzle(&cur_cube); + for (mv, next) in M::successor_states(cur_cube) { + let c2 = C::from_puzzle(&next); + + table[c.repr()][mv.index()] = c2; + + if !visited[c2.repr()] { + visited[c2.repr()] = true; + stack.push(next); + } + } + } + + debug_assert!(visited.into_iter().all(|b| b)); + + Self { + table, + _phantom: PhantomData, + } + } + + /// Determine what coordinate comes from applying a move. + pub fn make_move(&self, coord: C, mv: M) -> C { + self.table[coord.repr()][mv.index()] + } + + #[cfg(test)] + /// Determine what coordinate comes from applying a sequence of moves. + pub fn make_moves(&self, coord: C, alg: MoveSequence) -> C { + alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) + } +} + +/// A move table working over a symmetry coordinate. See `MoveTable`. +/// +/// Symmetry move tables only store one coordinate to move mapping per symmetry class, instead of +/// per coordinate. This makes it more compressed than a normal raw move table. The tradeoff is +/// that there is a very minor performance hit when computing moves as we need to adjust for +/// symmetry differences. +/// +/// In particular, if we have a symmetry coordinate S P S^-1, and we want to apply a move M to it, +/// notice that S P S^-1 M = S P S^-1 M S S^-1 = S (P S^-1 M S) S^-1. Hence, if we know P M' (where +/// M' = S^-1 M S) to be S' Q S'^-1, we can compute S P S^-1 M to be S S' Q S'^-1 S^-1. +pub struct SymMoveTable +where + CubieCube: FromCoordinate, +{ + table: Box<[[S; MOVES]]>, + sym_mult_table: SymMultTable, + sym_move_conj_table: SymMoveConjTable, + _phantom: PhantomData, +} + +impl + SymMoveTable +where + CubieCube: FromCoordinate, +{ + /// Generate a move table. This is slightly expensive so making move tables repeatedly should + /// be avoided, since the resulting move table generated will always be identical. + pub fn generate(sym_table: &RawSymTable) -> Self { + let table: Box<[[S; MOVES]]> = (0..S::classes()) + .map(|i| { + let raw = sym_table.index_to_repr(i); + let mut c = CubieCube::SOLVED; + c.set_coord(raw); + + let mut t: [S; MOVES] = std::array::from_fn(|_| Default::default()); + + for (mv, next) in M::successor_states(c) { + t[mv.index()] = sym_table.raw_to_sym(S::Raw::from_puzzle(&next)); + } + + t + }) + .collect::>() + .into_boxed_slice(); + let sym_mult_table = SymMultTable::generate(); + let sym_move_conj_table = SymMoveConjTable::generate(); + + SymMoveTable { + table, + sym_mult_table, + sym_move_conj_table, + _phantom: PhantomData, + } + } + + /// Determine what coordinate comes from applying a move. + pub fn make_move(&self, coord: S, mv: M) -> S { + let (idx, sym1) = coord.repr(); + let mv = self.sym_move_conj_table.conjugate(mv, sym1); + let (idx, sym2) = self.table[idx][mv.index()].repr(); + let sym = self.sym_mult_table.multiply(sym1, sym2); + + S::from_repr(idx, sym) + } + + #[cfg(test)] + /// Determine what coordinate comes from applying a sequence of moves. + pub fn make_moves(&self, coord: S, alg: MoveSequence) -> S { + alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) + } +} + +use crate::cube333::moves::MoveGenerator; +impl SubMove for Move333 { + fn into_move(self) -> Move333 { + self + } + + const MOVE_LIST: &'static [Move333] = crate::cube333::moves::Htm::MOVE_LIST; + + fn index(self) -> usize { + self.into() + } +} + +// TODO proptest DrMoves preserve phase 2 + +/// A move in domino reduction (phase 2). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(test, derive(Arbitrary))] +pub enum DrMove { + R2, + L2, + F2, + B2, + #[cfg_attr(test, proptest(strategy = "(1..=3u8).prop_map(DrMove::U)", weight = 3))] + U(u8), + #[cfg_attr(test, proptest(strategy = "(1..=3u8).prop_map(DrMove::D)", weight = 3))] + D(u8), +} + +impl Move for DrMove { + fn inverse(self) -> Self { + match self { + DrMove::U(n) => DrMove::U(4u8.wrapping_sub(n).rem_euclid(4)), + DrMove::D(n) => DrMove::D(4u8.wrapping_sub(n).rem_euclid(4)), + _ => self, + } + } + + fn commutes_with(&self, b: &Self) -> bool { + use DrMove as M; + match self { + M::R2 | M::L2 => matches!(b, M::R2 | M::L2), + M::F2 | M::B2 => matches!(b, M::F2 | M::B2), + M::U(_) | M::D(_) => matches!(b, M::U(_) | M::D(_)), + } + } + + fn cancel(self, b: Self) -> Cancellation { + use DrMove as M; + match (self, b) { + (M::R2, M::R2) => Cancellation::NoMove, + (M::L2, M::L2) => Cancellation::NoMove, + (M::F2, M::F2) => Cancellation::NoMove, + (M::U(n), M::U(m)) if (n + m).is_multiple_of(4) => Cancellation::NoMove, + (M::D(n), M::D(m)) if (n + m).is_multiple_of(4) => Cancellation::NoMove, + (M::U(n), M::U(m)) => Cancellation::OneMove(M::U((n + m) % 4)), + (M::D(n), M::D(m)) => Cancellation::OneMove(M::D((n + m) % 4)), + (M::B2, M::B2) => Cancellation::NoMove, + _ => Cancellation::TwoMove(self, b), + } + } +} + +impl SubMove for DrMove { + fn into_move(self) -> Move333 { + use crate::mv; + match self { + DrMove::R2 => mv!(R, 2), + DrMove::L2 => mv!(L, 2), + DrMove::F2 => mv!(F, 2), + DrMove::B2 => mv!(B, 2), + DrMove::U(n) => mv!(U, n), + DrMove::D(n) => mv!(D, n), + } + } + + fn axis(self) -> Move333Type { + match self { + DrMove::R2 => Move333Type::R, + DrMove::L2 => Move333Type::L, + DrMove::F2 => Move333Type::F, + DrMove::B2 => Move333Type::B, + DrMove::U(_) => Move333Type::U, + DrMove::D(_) => Move333Type::D, + } + } + + const MOVE_LIST: &'static [DrMove] = &[ + DrMove::R2, + DrMove::L2, + DrMove::F2, + DrMove::B2, + DrMove::U(1), + DrMove::U(2), + DrMove::U(3), + DrMove::D(1), + DrMove::D(2), + DrMove::D(3), + ]; + + fn index(self) -> usize { + match self { + DrMove::R2 => 0, + DrMove::L2 => 1, + DrMove::F2 => 2, + DrMove::B2 => 3, + // Technically this mod is unnecessary if the invariant that n is always in 1..=3 + // holds! But that's unsatisfying + DrMove::U(n) => 3 + (n % 4) as usize, + DrMove::D(n) => 6 + (n % 4) as usize, + } + } +} + +pub type COSymMoveTable = SymMoveTable; +pub type EOSymMoveTable = SymMoveTable; +pub type ESliceEdgeMoveTable = MoveTable; +pub type DominoCPSymMoveTable = SymMoveTable; +pub type DominoEPSymMoveTable = SymMoveTable; +pub type DominoESliceMoveTable = MoveTable; + +#[cfg(test)] +mod test { + use super::*; + + use proptest::collection::vec; + use proptest::prelude::*; + + #[test] + fn generates() { + ESliceEdgeMoveTable::generate(); + DominoESliceMoveTable::generate(); + COSymMoveTable::generate(&RawSymTable::generate()); + EOSymMoveTable::generate(&RawSymTable::generate()); + DominoCPSymMoveTable::generate(&RawSymTable::generate()); + DominoEPSymMoveTable::generate(&RawSymTable::generate()); + } + + /* We check that the following diagram commutes + * + * CubieCube --apply_move--> CubieCube + * | | + * | | + * from_puzzle from_puzzle + * | | + * | | + * v v + * Coord -----apply_move---> Coord + * + * Move application should be compatable with coordinate translation. + */ + + fn diagram_commutes< + M: SubMove, + C: Coordinate + std::fmt::Debug, + const MOVES: usize, + >( + table: &MoveTable, + p: CubieCube, + mvs: MoveSequence, + ) { + let l = table.make_moves(C::from_puzzle(&p), mvs.clone()); + let r = C::from_puzzle(&p.make_moves(MoveSequence( + mvs.0.into_iter().map(|m| m.into_move()).collect(), + ))); + assert_eq!(l, r); + } + + fn sym_diagram_commutes< + M: SubMove, + S: SymCoordinate + std::fmt::Debug, + const MOVES: usize, + const SYMS: usize, + >( + sym_table: &RawSymTable, + table: &SymMoveTable, + p: CubieCube, + mvs: MoveSequence, + ) where + CubieCube: FromCoordinate, + S::Raw: std::fmt::Debug, + { + let l = table.make_moves(sym_table.puzzle_to_sym(&p), mvs.clone()); + let r = sym_table.puzzle_to_sym(&p.make_moves(MoveSequence( + mvs.0.into_iter().map(|m| m.into_move()).collect(), + ))); + assert_eq!(sym_table.sym_to_raw(l), sym_table.sym_to_raw(r)); + } + + #[test] + fn commutes_normal() { + let eslice_table = ESliceEdgeMoveTable::generate(); + + let co_sym = RawSymTable::generate(); + let co_sym_table = COSymMoveTable::generate(&co_sym); + let eo_sym = RawSymTable::generate(); + let eo_sym_table = EOSymMoveTable::generate(&eo_sym); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); + sym_diagram_commutes(&co_sym, &co_sym_table, CubieCube::SOLVED, mvs.clone()); + sym_diagram_commutes(&eo_sym, &eo_sym_table, CubieCube::SOLVED, mvs.clone()); + }); + } + + #[test] + fn commutes_domino() { + let co_sym = RawSymTable::generate(); + let cp_table = DominoCPSymMoveTable::generate(&co_sym); + let eo_sym = RawSymTable::generate(); + let ep_table = DominoEPSymMoveTable::generate(&eo_sym); + let eslice_table = DominoESliceMoveTable::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + sym_diagram_commutes(&co_sym, &cp_table, CubieCube::SOLVED, mvs.clone()); + sym_diagram_commutes(&eo_sym, &ep_table, CubieCube::SOLVED, mvs.clone()); + diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); + }); + } +} diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs new file mode 100644 index 0000000..de38193 --- /dev/null +++ b/src/cube333/two_phase_solver/prune.rs @@ -0,0 +1,349 @@ +//! Pruning tables for the two phase solver. +//! +//! Choices of pruning tables are from cs0x7f's min2phase. + +use super::coords::{ + COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, + RawSymTable, SymCoordinate, +}; +use super::move_tables::{DrMove, MoveTable, SubMove, SymMoveTable}; +use super::symmetry::Symmetry; +use crate::coord::{Coordinate, FromCoordinate}; +use crate::cube333::{CubieCube, moves::Move333}; + +use std::marker::PhantomData; +use std::rc::Rc; + +// TODO future stuff: +// look into alternative pruning table choices +// look into alternative information to store in pruning tables +// look into alternative compression schemes + +/// A table storing results of conjugating raw coordinates by symmetries or inverses of symmetries. +pub struct SymConjTable, const SYMS: usize> { + table: Box<[[R; SYMS]]>, + inv_table: Box<[[R; SYMS]]>, + _phantom1: PhantomData, + _phantom2: PhantomData, +} + +impl, const SYMS: usize> SymConjTable +where + CubieCube: FromCoordinate, +{ + /// Generate the table + pub fn generate() -> Self { + let mut table: Box<[[R; SYMS]]> = + vec![std::array::from_fn(|_| Default::default()); R::count()].into_boxed_slice(); + let mut inv_table: Box<[[R; SYMS]]> = + vec![std::array::from_fn(|_| Default::default()); R::count()].into_boxed_slice(); + + for r1 in (0..R::count()).map(R::from_repr) { + let mut c = CubieCube::SOLVED; + c.set_coord(r1); + for s in S::get_all() { + let d = c.clone().conjugate_symmetry(s); + let r2 = R::from_puzzle(&d); + table[r1.repr()][s.repr()] = r2; + let d = c.clone().conjugate_inverse_symmetry(s); + let r2 = R::from_puzzle(&d); + inv_table[r1.repr()][s.repr()] = r2; + } + } + + Self { + table, + inv_table, + _phantom1: PhantomData, + _phantom2: PhantomData, + } + } + + /// Conjugate the given raw coordinate by the given symmetry (S R S^-1). + pub fn conjugate(&self, r: R, s: S) -> R { + self.table[r.repr()][s.repr()] + } + + /// Conjugate the given raw coordinate by the given symmetry's inverse (S^-1 R S). + pub fn conjugate_inverse(&self, r: R, s: S) -> R { + self.inv_table[r.repr()][s.repr()] + } +} + +/// A pruning table indexed by the class of a symmetry coordinate and a raw coordinate. +/// +/// COUNT should be the number of symmetry coordinate classes times the number of raw coordinates +/// divided by 4 (we store 4 entries per byte). An entry is the optimal search depth of the state +/// modulo 3, see https://kociemba.org/math/pruning.htm for a detailed explanation. Essentially +/// though, we can compute the whole pruning depth based on solely the pruning depth modulo 3 if we +/// know the pruning depth of the current state we are in, when searching. +pub struct SymRawPruningTable< + S: SymCoordinate, + R: Coordinate, + M: SubMove, + const SYMS: usize, + const MOVES: usize, +> where + CubieCube: FromCoordinate, + CubieCube: FromCoordinate, +{ + table: Box<[u8]>, + conj_table: SymConjTable, + sym_table: Rc>, + sym_move_table: Rc>, + raw_move_table: Rc>, +} + +impl, M: SubMove, const SYMS: usize, const MOVES: usize> + SymRawPruningTable +where + CubieCube: FromCoordinate, + CubieCube: FromCoordinate, +{ + /// Generate the table + pub fn generate( + sym_table: Rc>, + sym_move_table: Rc>, + raw_move_table: Rc>, + ) -> Self { + let table = vec![0xff; S::classes() * R::count() / 4].into_boxed_slice(); + let conj_table = SymConjTable::generate(); + let mut table = Self { + table, + conj_table, + sym_table, + sym_move_table, + raw_move_table, + }; + + let conj_table_s = SymConjTable::generate(); + + let s = table.sym_table.puzzle_to_sym(&CubieCube::SOLVED); + let r = R::from_puzzle(&CubieCube::SOLVED); + table.set(s, r, 0, &conj_table_s); + let mut stack = vec![(s, r)]; + let mut next = vec![]; + let mut depth = 1; + + while !stack.is_empty() { + while let Some((s, r)) = stack.pop() { + for &m in M::MOVE_LIST { + let s2 = table.sym_move_table.make_move(s, m); + let r2 = table.raw_move_table.make_move(r, m); + if table.query(s2, r2) == 3 { + next.push((s2, r2)); + table.set(s2, r2, depth % 3, &conj_table_s); + } + } + } + + stack = next; + next = vec![]; + depth += 1; + } + + table + } + + /// Compute the index and shift into the table given a coordinate pair. + fn index(&self, s: S, r: R) -> (usize, usize) { + let r2 = self.conj_table.conjugate_inverse(r, s.sym()); + let i = r2.repr() * S::classes() + s.class(); + (i >> 2, (i & 3) * 2) + } + + /// Set the depth in the search tree of this coordinate pair modulo 3. + fn set(&mut self, s: S, r: R, val: u8, conj_table_s: &SymConjTable) { + assert!(val & !3 == 0); + + // Some S::Raw coordinates can be represented in multiple ways by S (there can be multiple + // symmetries that give an equivalent raw coordinate when conjugating some representative, + // think the solved state for example which could be represented by any symmetry). Because + // of this, there could be multiple entries into our pruning table corresponding to the + // same state. With just s and r, we would only update the entry corresponding to (if S is + // the symmetry) S^-1 r S, and so we must iterate over all symmetries and find the + // duplicates we need to update. + + let repr_raw = self.sym_table.index_to_repr(s.class()); + let sinv_raw = conj_table_s.conjugate(repr_raw, s.sym()); + for sym in S::Sym::get_all() { + if conj_table_s.conjugate(repr_raw, sym) == sinv_raw { + let s2 = S::from_repr(s.class(), sym); + let (index, shift) = self.index(s2, r); + + self.table[index] &= !(3 << shift); + self.table[index] |= val << shift; + } + } + } + + /// Determine the bound of a coordinate pair modulo 3 with a lookup + fn query(&self, s: S, r: R) -> u8 { + let (index, shift) = self.index(s, r); + + (self.table[index] >> shift) & 3 + } + + /// Update a prune bound given the next state (fast) + pub fn update(&self, cur: usize, s: S, r: R) -> usize { + let n = self.query(s, r) as usize; + let c = cur % 3; + let d = (n + 3 - c).rem_euclid(3); + match d { + 0 => cur, + 1 => cur + 1, + 2 => cur - 1, + _ => unreachable!(), + } + } + + /// Compute the bound on a given coordinate pair (slow) + pub fn bound(&self, mut s: S, mut r: R) -> usize { + let mut bound = 0; + let solved = ( + self.sym_table.puzzle_to_sym(&CubieCube::SOLVED).class(), + R::from_puzzle(&CubieCube::SOLVED), + ); + while (s.class(), r) != solved { + let n = self.query(s, r); + // n - 1 but underflow + let goal = (n + 2).rem_euclid(3); + (s, r) = M::MOVE_LIST + .iter() + .map(|&m| { + ( + self.sym_move_table.make_move(s, m), + self.raw_move_table.make_move(r, m), + ) + }) + .find(|&(s, r)| self.query(s, r) == goal) + .unwrap(); + + bound += 1; + } + bound + } +} + +pub type ESliceTwistPruneTable = SymRawPruningTable; +pub type ESliceFlipPruneTable = SymRawPruningTable; +pub type DominoSliceCPPruneTable = + SymRawPruningTable; +pub type DominoSliceEPPruneTable = + SymRawPruningTable; + +#[cfg(test)] +mod test { + use super::super::move_tables::{DrMove, SubMove}; + use super::super::symmetry::HalfSymmetry; + use super::*; + use crate::coord::{Coordinate, FromCoordinate}; + use crate::cube333::coordcube::EOCoord; + use crate::cube333::moves::Move333; + use crate::moves::MoveSequence; + + use proptest::collection::vec; + use proptest::prelude::*; + + type SliceConjTable = SymConjTable; + type EoConjTable = SymConjTable; + type DominoESliceConjTable = SymConjTable; + + fn diagram_commutes< + S: Symmetry, + R: Coordinate + std::fmt::Debug, + const COUNT: usize, + >( + table: &SymConjTable, + cube: CubieCube, + ) where + CubieCube: FromCoordinate, + { + for s in S::get_all() { + let a = table.conjugate(R::from_puzzle(&cube), s); + let b = R::from_puzzle(&cube.clone().conjugate_inverse_symmetry(s)); + assert_eq!(a, b); + } + } + + #[test] + fn conj_commutes() { + let slice_conj_table = SliceConjTable::generate(); + let eo_conj_table = EoConjTable::generate(); + let domino_eo_conj_table = DominoESliceConjTable::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + diagram_commutes(&slice_conj_table, CubieCube::SOLVED.make_moves(mvs.clone())); + diagram_commutes(&eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone())); + }); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| v.into_iter().map(SubMove::into_move).collect()).prop_map(MoveSequence))| { + diagram_commutes(&domino_eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone())); + }); + } + + fn admissable_and_update_correct< + S: SymCoordinate, + R: Coordinate, + M: SubMove, + const SYMS: usize, + const MOVES: usize, + >( + prune_table: &SymRawPruningTable, + mvs: MoveSequence, + ) where + CubieCube: FromCoordinate, + CubieCube: FromCoordinate, + { + let s = prune_table.sym_table.puzzle_to_sym(&CubieCube::SOLVED); + let s = prune_table.sym_move_table.make_moves(s, mvs.clone()); + let r = R::from_puzzle(&CubieCube::SOLVED); + let r = prune_table.raw_move_table.make_moves(r, mvs.clone()); + let b = prune_table.bound(s, r); + assert!(b <= mvs.len()); + + for &m in M::MOVE_LIST { + let s2 = prune_table.sym_move_table.make_move(s, m); + let r2 = prune_table.raw_move_table.make_move(r, m); + let b2 = prune_table.update(b, s2, r2); + assert_eq!(b2, prune_table.bound(s2, r2)); + } + } + + #[test] + fn check_admissable_and_update() { + let co_sym_table = Rc::new(RawSymTable::generate()); + let co_sym_move_table = Rc::new(SymMoveTable::generate(&co_sym_table)); + let eo_sym_table = Rc::new(RawSymTable::generate()); + let eo_sym_move_table = Rc::new(SymMoveTable::generate(&eo_sym_table)); + let e_slice_move_table = Rc::new(MoveTable::generate()); + let c_prune = ESliceTwistPruneTable::generate( + co_sym_table, + co_sym_move_table, + e_slice_move_table.clone(), + ); + let e_prune = + ESliceFlipPruneTable::generate(eo_sym_table, eo_sym_move_table, e_slice_move_table); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + admissable_and_update_correct(&c_prune, mvs.clone()); + admissable_and_update_correct(&e_prune, mvs.clone()); + }); + let cp_sym_table = Rc::new(RawSymTable::generate()); + let cp_sym_move_table = Rc::new(SymMoveTable::generate(&cp_sym_table)); + let ep_sym_table = Rc::new(RawSymTable::generate()); + let ep_sym_move_table = Rc::new(SymMoveTable::generate(&ep_sym_table)); + let d_e_slice_move_table = Rc::new(MoveTable::generate()); + let d_c_prune = DominoSliceCPPruneTable::generate( + cp_sym_table, + cp_sym_move_table, + d_e_slice_move_table.clone(), + ); + let d_e_prune = DominoSliceEPPruneTable::generate( + ep_sym_table, + ep_sym_move_table, + d_e_slice_move_table, + ); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + admissable_and_update_correct(&d_c_prune, mvs.clone()); + admissable_and_update_correct(&d_e_prune, mvs.clone()); + }); + } +} diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs new file mode 100644 index 0000000..032c4ed --- /dev/null +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -0,0 +1,368 @@ +//! Implements symmetries for the `CubieCube` and symmetry tables for use in move table generation. + +use super::move_tables::SubMove; +use crate::cube333::{Corner as C, CornerTwist as CT, CubieCube, Edge as E, EdgeFlip as EF}; + +use std::marker::PhantomData; + +pub trait Symmetry: Copy + Default + Eq { + /// A representation of this symmetry as a usize, for use in table lookups. + fn repr(self) -> usize; + + /// Convert the representation of a symmetry to the symmetry itself. We assume 0 corresponds to + /// the identity symmetry. + fn from_repr(n: usize) -> Self; + + /// Iterator over every symmetry in order of representation + fn get_all() -> impl Iterator; + + /// Apply this symmetry to the given puzzle, written P S + fn apply(&self, cube: CubieCube) -> CubieCube; + + /// Apply the inverse of this symmetry to the given puzzle, written P S^-1 + fn apply_inverse(&self, cube: CubieCube) -> CubieCube; + + /// Conjugate the given puzzle by this symmetry, written S P S^-1 + fn conjugate(&self, cube: CubieCube) -> CubieCube { + self.apply_inverse(self.apply(CubieCube::SOLVED).multiply_cube(cube)) + } + + /// Conjugate the given puzzle by the inverse of this symmetry, written S^-1 P S^ + fn conjugate_inverse(&self, cube: CubieCube) -> CubieCube { + self.apply(self.apply_inverse(CubieCube::SOLVED).multiply_cube(cube)) + } +} + +impl CubieCube { + /* unused + /// Obtain the cube given by applying some symmetry + pub(super) fn apply_symmetry(self, sym: S) -> CubieCube { + sym.apply(self) + } + */ + + /// Obtain the cube given by conjugating by some symmetry. We conjugate in the order S C S^-1 + pub(super) fn conjugate_symmetry(self, sym: S) -> CubieCube { + sym.conjugate(self) + } + + /// Obtain the cube given by conjugating by the inverse of some symmetry. We hence conjugate in + /// the order S^-1 C S + pub(super) fn conjugate_inverse_symmetry(self, sym: S) -> CubieCube { + sym.conjugate_inverse(self) + } +} + +/// Multiplication table for a `Symmetry` group. +pub struct SymMultTable { + table: [[S; COUNT]; COUNT], +} + +impl SymMultTable { + /// Generate the table + pub fn generate() -> Self { + use std::array::from_fn; + + let cubie_syms: [_; COUNT] = from_fn(|n| S::from_repr(n).apply(CubieCube::SOLVED)); + + let table = from_fn(|a| { + from_fn(|b| { + let a = S::from_repr(a); + let b = S::from_repr(b); + let c = a + .apply(CubieCube::SOLVED) + .multiply_cube(b.apply(CubieCube::SOLVED)); + S::from_repr(cubie_syms.iter().position(|d| &c == d).unwrap()) + }) + }); + + SymMultTable { table } + } + + /// Multiply two symmetries + pub fn multiply(&self, a: S, b: S) -> S { + self.table[a.repr()][b.repr()] + } +} + +/// A table of conjugates of moves by symmetries i.e. we identify moves corresponding to S^-1 M S. +/// We use an inverse conjugate to the usual convention of this repository as it is what is needed +/// for the symmetry move table. +pub struct SymMoveConjTable { + table: [[M; MOVES]; SYMS], + _phantom: PhantomData, +} + +impl + SymMoveConjTable +{ + /// Generate the table + pub fn generate() -> Self { + use std::array::from_fn; + + let table = from_fn(|s| { + from_fn(|m| { + let s = S::from_repr(s); + let m = M::MOVE_LIST[m]; + + let c = s.conjugate_inverse(CubieCube::SOLVED.make_move(m.into_move())); + + M::MOVE_LIST + .iter() + .filter_map(|&m2| { + (c.clone().make_move(m2.into_move()) == CubieCube::SOLVED) + .then_some(m2.inverse()) + }) + .next() + .expect("Moves should have conjugates in each symmetry") + }) + }); + + SymMoveConjTable { + table, + _phantom: PhantomData, + } + } + + /// Determine the move corresponding to S^-1 M S given M and S. + pub fn conjugate(&self, m: M, s: S) -> M { + self.table[s.repr()][m.index()] + } +} + +/// An element of the set of symmetries of a cube that preserve domino reduction. This is generated +/// by: +/// - A 180 degree rotation around the F/B axis (aka F2) +/// - A 90 degree rotation around the U/D axis (aka U4) +/// - A reflection around the R-L slice (aka RL2) +// We represent a symmetry as a product of these generators in a specific order: +// - the 0th bit determines R-L reflection +// - the 1st bit determines F/B 180 rotation +// - the 2nd and 3rd bits determines U/D rotation +// We multiply from top to bottom of this list. It doesn't really make sense to expose this +// information publicly, since we could have multiplied different generators in a different order. +// We can just think of this 4 bit number as an identifier for each symmetry. +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct DrSymmetry(u8); + +/// A cube that, when multiplied, applies the F2 symmetry. +#[rustfmt::skip] +const SYM_F2: CubieCube = CubieCube { + co: [CT::Oriented; 8], + cp: [C::DFL, C::DFR, C::DBR, C::DBL, C::UFL, C::UFR, C::UBR, C::UBL], + eo: [EF::Oriented; 12], + ep: [E::DF, E::DR, E::DB, E::DL, E::UF, E::UR, E::UB, E::UL, E::FL, E::FR, E::BR, E::BL], +}; + +/// A cube that, when multiplied, applies the U4 symmetry. +#[rustfmt::skip] +const SYM_U4: CubieCube = CubieCube { + co: [CT::Oriented; 8], + cp: [C::UBR, C::UFR, C::UFL, C::UBL, C::DBR, C::DFR, C::DFL, C::DBL], + eo: [EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Oriented, EF::Flipped, EF::Flipped, EF::Flipped, EF::Flipped], + ep: [E::UR, E::UF, E::UL, E::UB, E::DR, E::DF, E::DL, E::DB, E::BR, E::FR, E::FL, E::BL], +}; + +/// A cube that, when multiplied, almost applies the RL2 symmetry, but additionally the corner +/// orientations must be inverted (clockwise and anticlockwise swapped). +#[rustfmt::skip] +const SYM_RL2: CubieCube = CubieCube { + co: [CT::Oriented; 8], + cp: [C::UBR, C::UBL, C::UFL, C::UFR, C::DBR, C::DBL, C::DFL, C::DFR], + eo: [EF::Oriented; 12], + ep: [E::UB, E::UL, E::UF, E::UR, E::DB, E::DL, E::DF, E::DR, E::BR, E::BL, E::FL, E::FR], +}; + +impl DrSymmetry { + /// Returns the power of RL2 in the standard product notation of this symmetry. + fn rl2_count(self) -> usize { + (self.0 & 1) as usize + } + + /// Returns the power of F2 in the standard product notation of this symmetry. + fn f2_count(self) -> usize { + (self.0 >> 1 & 1) as usize + } + + /// Returns the power of U4 in the standard product notation of this symmetry. + fn u4_count(self) -> usize { + (self.0 >> 2 & 3) as usize + } + + // lol + /// An array of each symmetry + #[rustfmt::skip] + pub const ARRAY: [DrSymmetry; 16] = [DrSymmetry(0), DrSymmetry(1), DrSymmetry(2), DrSymmetry(3), DrSymmetry(4), DrSymmetry(5), DrSymmetry(6), DrSymmetry(7), DrSymmetry(8), DrSymmetry(9), DrSymmetry(10), DrSymmetry(11), DrSymmetry(12), DrSymmetry(13), DrSymmetry(14), DrSymmetry(15)]; +} + +impl Symmetry for DrSymmetry { + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + DrSymmetry(n as u8) + } + + fn get_all() -> impl Iterator { + Self::ARRAY.into_iter() + } + + fn apply(&self, mut cube: CubieCube) -> CubieCube { + for _ in 0..self.rl2_count() { + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); + } + + for _ in 0..self.f2_count() { + cube = cube.multiply_cube(SYM_F2); + } + + for _ in 0..self.u4_count() { + cube = cube.multiply_cube(SYM_U4); + } + + cube + } + + fn apply_inverse(&self, mut cube: CubieCube) -> CubieCube { + for _ in 0..((4 - self.u4_count()) % 4) { + cube = cube.multiply_cube(SYM_U4); + } + + for _ in 0..self.f2_count() { + cube = cube.multiply_cube(SYM_F2); + } + + for _ in 0..self.rl2_count() { + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); + } + + cube + } +} + +/// An element of the set of symmetries of a cube that preserve domino reduction. This is generated +/// by: +/// - A 180 degree rotation around the F/B axis (aka F2) +/// - A 180 degree rotation around the U/D axis (aka U2) +/// - A reflection around the R-L slice (aka RL2) +// 0s bit is R-L +// 1s bit F/B +// 2s bit U/D +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct HalfSymmetry(u8); + +impl HalfSymmetry { + /// Returns the power of RL2 in the standard product notation of this symmetry. + fn rl2_count(self) -> usize { + (self.0 & 1) as usize + } + + /// Returns the power of F2 in the standard product notation of this symmetry. + fn f2_count(self) -> usize { + (self.0 >> 1 & 1) as usize + } + + /// Returns the power of U2 in the standard product notation of this symmetry. + fn u2_count(self) -> usize { + (self.0 >> 2 & 1) as usize + } + + // lol + /// An array of each symmetry + #[rustfmt::skip] + pub const ARRAY: [HalfSymmetry; 8] = [HalfSymmetry(0), HalfSymmetry(1), HalfSymmetry(2), HalfSymmetry(3), HalfSymmetry(4), HalfSymmetry(5), HalfSymmetry(6), HalfSymmetry(7)]; +} + +impl Symmetry for HalfSymmetry { + fn repr(self) -> usize { + self.0 as usize + } + + fn from_repr(n: usize) -> Self { + HalfSymmetry(n as u8) + } + + fn get_all() -> impl Iterator { + Self::ARRAY.into_iter() + } + + fn apply(&self, mut cube: CubieCube) -> CubieCube { + for _ in 0..self.rl2_count() { + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); + } + + for _ in 0..self.f2_count() { + cube = cube.multiply_cube(SYM_F2); + } + + for _ in 0..self.u2_count() { + cube = cube.multiply_cube(SYM_U4.multiply_cube(SYM_U4)); + } + + cube + } + + fn apply_inverse(&self, mut cube: CubieCube) -> CubieCube { + for _ in 0..self.u2_count() { + cube = cube.multiply_cube(SYM_U4.multiply_cube(SYM_U4)); + } + + for _ in 0..self.f2_count() { + cube = cube.multiply_cube(SYM_F2); + } + + for _ in 0..self.rl2_count() { + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); + } + + cube + } +} + +#[cfg(test)] +mod test { + use super::*; + + use crate::cube333::moves::Move333; + use crate::moves::MoveSequence; + + use proptest::collection::vec; + use proptest::prelude::*; + + fn check_multiplication_correct() { + let table = SymMultTable::::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + let cube = CubieCube::SOLVED.make_moves(mvs); + for sym1 in S::get_all() { + for sym2 in S::get_all() { + assert_eq!( + table.multiply(sym1, sym2).apply(cube.clone()), + sym2.apply(sym1.apply(cube.clone())), + "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2), + ); + assert_eq!( + table.multiply(sym1, sym2).apply_inverse(cube.clone()), + sym1.apply_inverse(sym2.apply_inverse(cube.clone())), + "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2), + ); + assert_eq!( + table.multiply(sym1, sym2).conjugate(cube.clone()), + sym1.conjugate(sym2.conjugate(cube.clone())), + "{sym1:?} {sym2:?} {:?}", table.multiply(sym1, sym2), + ); + } + } + }); + } + + #[test] + fn multiplication_correct() { + check_multiplication_correct::(); + check_multiplication_correct::(); + } +} diff --git a/src/error.rs b/src/error.rs index 9933011..4346631 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,4 +9,3 @@ pub enum TryFromIntToEnumError { #[error("attempted to convert integer into enum value, but integer was out of bounds")] OutOfBounds, } - diff --git a/src/lib.rs b/src/lib.rs index 265d196..26ebbec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![deny(missing_docs)] +pub mod coord; pub mod cube333; pub mod error; pub mod moves; diff --git a/src/moves/mod.rs b/src/moves/mod.rs index 60a99c6..5a26ab3 100644 --- a/src/moves/mod.rs +++ b/src/moves/mod.rs @@ -26,12 +26,10 @@ pub enum Cancellation { /// are encoded in the `commutes_with` method and order relations are encoded in the `cancel` /// method. These relations are all that are assumed for the general `MoveSequence::cancel`, so any /// additional relations will not be used for optimal cancellation. -pub trait Move: Eq + Clone { +pub trait Move: Eq + Clone + Sized { /// Take the inverse of a move. These inverses must satisfy the invertibility conditions of /// a group, i.e. that `X X^{-1} = X^{-1} X = e` where `e` is the empty sequence. - fn inverse(self) -> Self - where - Self: Sized; + fn inverse(self) -> Self; /// Returns whether the two moves commute, i.e. can be swapped when adjacent. It is required /// that this property is transitive. @@ -57,9 +55,7 @@ pub trait Move: Eq + Clone { /// assert!(mv!(R, 1).cancel(mv!(R, 3)) == Cancellation::NoMove); /// # } /// ``` - fn cancel(self, b: Self) -> Cancellation - where - Self: Sized; + fn cancel(self, b: Self) -> Cancellation; } /// A sequence of moves (also known as an algorithm) for some specific type of move.