From 07f7399c4b3d47d72108d1f8e549e414efda358c Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Fri, 18 Jul 2025 04:44:33 +1000 Subject: [PATCH 01/45] Phase one coordinates --- Cargo.toml | 3 +- src/coord.rs | 14 +++ src/cube333/edge.rs | 15 +++ src/cube333/mod.rs | 2 + src/cube333/two_phase_solver/coords.rs | 143 +++++++++++++++++++++++++ src/cube333/two_phase_solver/mod.rs | 3 + src/lib.rs | 1 + 7 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/coord.rs create mode 100644 src/cube333/two_phase_solver/coords.rs create mode 100644 src/cube333/two_phase_solver/mod.rs diff --git a/Cargo.toml b/Cargo.toml index d028b19..52efcfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [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 [dependencies] +itertools = "0.14.0" thiserror = "1.0.50" [dev-dependencies] diff --git a/src/coord.rs b/src/coord.rs new file mode 100644 index 0000000..afdee17 --- /dev/null +++ b/src/coord.rs @@ -0,0 +1,14 @@ +//! 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 { + /// Obtain the coordinate that corresponds to the given puzzle. + fn from_puzzle(puzzle: &P) -> Self; + + /// 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; +} diff --git a/src/cube333/edge.rs b/src/cube333/edge.rs index 585e5c5..2f4743f 100644 --- a/src/cube333/edge.rs +++ b/src/cube333/edge.rs @@ -55,6 +55,21 @@ 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 { diff --git a/src/cube333/mod.rs b/src/cube333/mod.rs index 29a86cb..f7afb89 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}; diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs new file mode 100644 index 0000000..75a3e02 --- /dev/null +++ b/src/cube333/two_phase_solver/coords.rs @@ -0,0 +1,143 @@ +//! This module contains the coordinate representations of cube states relevant to the two phases +//! of these solver. + +use crate::coord::Coordinate; +use crate::cube333::CubieCube; + +// TODO Copy pasted from the old coordcube file that i'll probably delete, should clean up +fn to_o_coord(arr: &[u8; COUNT]) -> u16 { + arr.iter() + .skip(1) + .fold(0, |acc, &i| (acc * STATES) + i as u16) +} + +/* +// TODO this is kinda unreadable lol +fn to_p_coord(arr: &[u8; COUNT]) -> u32 { + (1..COUNT).rev().fold(0, |acc, idx| { + (acc * (idx + 1) as u32) + arr[0..idx].iter().filter(|&&x| x > arr[idx]).count() as u32 + }) +} +*/ + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] +struct COCoord(u16); + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] +struct EOCoord(u16); + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] +struct ESliceEdgeCoord(u16); + +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 + } +} + +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 + } +} + +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 = 0; + + 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 = n + 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) % (k - 1) == 0); + 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) % (n - k + 1) == 0); + c = c * (n + 1) / (n - k + 1); + } + } + + ESliceEdgeCoord(r) + } + + fn count() -> usize { + 495 + } + + fn repr(self) -> usize { + self.0 as usize + } +} + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +// temp +#[allow(dead_code)] +struct Phase1Cube { + co: COCoord, + eo: EOCoord, + e_slice: ESliceEdgeCoord, +} + +#[cfg(test)] +mod test { + use std::collections::HashSet; + + use itertools::Itertools; + + use super::ESliceEdgeCoord; + use crate::{coord::Coordinate, cube333::CubieCube}; + + #[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()); + } +} diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs new file mode 100644 index 0000000..ebdcb97 --- /dev/null +++ b/src/cube333/two_phase_solver/mod.rs @@ -0,0 +1,3 @@ +//! An implementation of the two phase solver described [here](https://kociemba.org/cube.htm). + +mod coords; 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; From 29436f4fa4d73e2402d0be661f0fced2a2d61c69 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 19 Jul 2025 06:57:04 +1000 Subject: [PATCH 02/45] Use old coordinates for the phase 1 cube --- src/cube333/coordcube.rs | 82 +++++++++++++++++++++++--- src/cube333/mod.rs | 2 +- src/cube333/two_phase_solver/coords.rs | 74 ++++++----------------- src/error.rs | 1 - 4 files changed, 92 insertions(+), 67 deletions(-) diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs index 2e5e431..991aff3 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -1,13 +1,77 @@ use super::{Corner, CornerTwist, CubieCube, Edge, EdgeFlip}; +use crate::coord::Coordinate; +/// A coordinate representation of the corner orientation of a cube with respect to the U/F faces. #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -struct COCoord(u16); +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, Copy, Clone)] -struct CPCoord(u16); +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, Copy, Clone)] -struct EOCoord(u16); +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, Copy, Clone)] -struct EPCoord(u32); +pub struct EPCoord(u32); + +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 + } +} + +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 + } +} + +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 + } +} + +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 + } +} /// Implementation of a coord cube, representing pieces using coordinates, which are values which /// are isomorphic to arrays represented in a cubie cube. @@ -146,10 +210,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 }) } @@ -158,9 +222,9 @@ impl CubieCube { #[cfg(test)] mod tests { use crate::cube333::{ + Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube, coordcube::{CoordCube, CubieToCoordError}, moves::{Move333, Move333Type}, - Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube, }; use crate::mv; diff --git a/src/cube333/mod.rs b/src/cube333/mod.rs index f7afb89..8217729 100644 --- a/src/cube333/mod.rs +++ b/src/cube333/mod.rs @@ -419,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/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 75a3e02..6ea2bf6 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -2,63 +2,15 @@ //! of these solver. use crate::coord::Coordinate; -use crate::cube333::CubieCube; - -// TODO Copy pasted from the old coordcube file that i'll probably delete, should clean up -fn to_o_coord(arr: &[u8; COUNT]) -> u16 { - arr.iter() - .skip(1) - .fold(0, |acc, &i| (acc * STATES) + i as u16) -} - -/* -// TODO this is kinda unreadable lol -fn to_p_coord(arr: &[u8; COUNT]) -> u32 { - (1..COUNT).rev().fold(0, |acc, idx| { - (acc * (idx + 1) as u32) + arr[0..idx].iter().filter(|&&x| x > arr[idx]).count() as u32 - }) -} -*/ - -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] -struct COCoord(u16); - -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] -struct EOCoord(u16); +use crate::cube333::{ + CubieCube, + coordcube::{COCoord, EOCoord}, +}; +/// Coordinate for positions of E slice edges (ignoring what the edges actually arge) #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] struct ESliceEdgeCoord(u16); -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 - } -} - -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 - } -} - impl Coordinate for ESliceEdgeCoord { fn from_puzzle(puzzle: &CubieCube) -> Self { // https://kociemba.org/math/UDSliceCoord.htm @@ -107,14 +59,24 @@ impl Coordinate for ESliceEdgeCoord { } #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -// temp -#[allow(dead_code)] -struct Phase1Cube { +pub struct Phase1Cube { co: COCoord, eo: EOCoord, e_slice: ESliceEdgeCoord, } +impl Phase1Cube { + /// Convert a cubie cube into a phase 1 state cube. This will never fail as every cube can be + /// put into the phase 1 solver. + pub fn from_puzzle(puzzle: &CubieCube) -> Self { + Phase1Cube { + co: COCoord::from_puzzle(puzzle), + eo: EOCoord::from_puzzle(puzzle), + e_slice: ESliceEdgeCoord::from_puzzle(puzzle), + } + } +} + #[cfg(test)] mod test { use std::collections::HashSet; 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, } - From 3f7eed2de7e7e437e0fb79e972fd2fcfa524d91b Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sun, 20 Jul 2025 07:17:32 +1000 Subject: [PATCH 03/45] Phase 2 coordinates --- src/cube333/coordcube.rs | 11 ++-- src/cube333/two_phase_solver/coords.rs | 73 +++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs index 991aff3..91d63d7 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -2,16 +2,19 @@ use super::{Corner, CornerTwist, CubieCube, Edge, EdgeFlip}; use crate::coord::Coordinate; /// A coordinate representation of the corner orientation of a cube with respect to the U/F faces. -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, Default, PartialEq, Eq, 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, Copy, Clone)] +#[derive(Debug, Default, PartialEq, Eq, 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, Copy, Clone)] +#[derive(Debug, Default, PartialEq, Eq, 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, Copy, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] pub struct EPCoord(u32); impl Coordinate for COCoord { diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 6ea2bf6..9c31c44 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -4,13 +4,33 @@ use crate::coord::Coordinate; use crate::cube333::{ CubieCube, - coordcube::{COCoord, EOCoord}, + 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 { + (LOWER..UPPER).rev().fold(0, |acc, idx| { + (acc * (idx + 1) as u16) + arr[0..idx].iter().filter(|&&x| x > arr[idx]).count() as u16 + }) +} + /// Coordinate for positions of E slice edges (ignoring what the edges actually arge) #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] 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, Copy, Clone, Hash)] +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, Copy, Clone, Hash)] +struct DominoESliceCoord(u16); + impl Coordinate for ESliceEdgeCoord { fn from_puzzle(puzzle: &CubieCube) -> Self { // https://kociemba.org/math/UDSliceCoord.htm @@ -58,6 +78,34 @@ impl Coordinate for ESliceEdgeCoord { } } +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 + } +} + +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 + } +} + #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub struct Phase1Cube { co: COCoord, @@ -77,6 +125,29 @@ impl Phase1Cube { } } +pub struct Phase2Cube { + cp: CPCoord, + ep: DominoEPCoord, + e_slice: DominoESliceCoord, +} + +fn cubie_in_domino(puzzle: &CubieCube) -> bool { + let p1 = Phase1Cube::from_puzzle(puzzle); + p1.co.repr() == 0 && p1.eo.repr() == 0 && p1.e_slice.repr() == 0 +} + +impl Phase2Cube { + /// Attempt to convert a cubie cube into a Phase2Cube. This will fail if the cube is not in U/D + /// domino reduction. + pub fn from_puzzle(puzzle: &CubieCube) -> Option { + cubie_in_domino(puzzle).then_some(Phase2Cube { + cp: CPCoord::from_puzzle(puzzle), + ep: DominoEPCoord::from_puzzle(puzzle), + e_slice: DominoESliceCoord::from_puzzle(puzzle), + }) + } +} + #[cfg(test)] mod test { use std::collections::HashSet; From 3c0dd383ca43860aa0e147fcf7ee48ecb0c12ef3 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Mon, 21 Jul 2025 17:49:04 +1000 Subject: [PATCH 04/45] Create move tables --- src/coord.rs | 2 +- src/cube333/moves.rs | 4 +- src/cube333/two_phase_solver/coords.rs | 6 +- src/cube333/two_phase_solver/mod.rs | 1 + src/cube333/two_phase_solver/move_tables.rs | 194 ++++++++++++++++++++ src/moves/mod.rs | 10 +- 6 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/cube333/two_phase_solver/move_tables.rs diff --git a/src/coord.rs b/src/coord.rs index afdee17..a787771 100644 --- a/src/coord.rs +++ b/src/coord.rs @@ -2,7 +2,7 @@ //! of a puzzle. /// A coordinate type, encoding cosets of the puzzle P. -pub trait Coordinate

: Copy { +pub trait Coordinate

: Copy + Default { /// Obtain the coordinate that corresponds to the given puzzle. fn from_puzzle(puzzle: &P) -> Self; diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 6fd99a3..a006925 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -51,8 +51,6 @@ impl crate::moves::Move for Move333 { } fn cancel(self, b: Self) -> Cancellation - where - Self: Sized, { if self.ty == b.ty { let count = (self.count + b.count) % 4; @@ -101,7 +99,7 @@ impl From for usize { macro_rules! mv { ($ty:ident, $count: expr) => { Move333 { - ty: Move333Type::$ty, + ty: crate::cube333::moves::Move333Type::$ty, count: $count, } }; diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 9c31c44..c613738 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -19,17 +19,17 @@ fn to_p_coord( /// Coordinate for positions of E slice edges (ignoring what the edges actually arge) #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] -struct ESliceEdgeCoord(u16); +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, Copy, Clone, Hash)] -struct DominoEPCoord(u16); +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, Copy, Clone, Hash)] -struct DominoESliceCoord(u16); +pub struct DominoESliceCoord(u16); impl Coordinate for ESliceEdgeCoord { fn from_puzzle(puzzle: &CubieCube) -> Self { diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index ebdcb97..3e3a2c2 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -1,3 +1,4 @@ //! An implementation of the two phase solver described [here](https://kociemba.org/cube.htm). mod coords; +mod move_tables; 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..6118778 --- /dev/null +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -0,0 +1,194 @@ +//! Move tables for each coordinate type + +use crate::coord::Coordinate; +use crate::cube333::{CubieCube, moves::Move333}; +use crate::moves::{Cancellation, Move}; + +use super::coords::{DominoEPCoord, DominoESliceCoord, ESliceEdgeCoord}; +use crate::cube333::coordcube::{COCoord, CPCoord, EOCoord}; + +use std::marker::PhantomData; + +// 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 { + /// Interpret a move as a normal move to be applied to a `CubieCube`. + fn into_move(self) -> Move333; + + /// The number of moves that exist + fn count() -> usize; + + /// The list of all moves that this type encodes. The length of the returned vector should be + /// `count()`. + fn moves() -> Vec; + + /// 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(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MoveTable> { + table: Vec>, + _phantom: PhantomData, +} + +impl> MoveTable { + /// Generate a move table. This is slightly expensive, so making move tables repeatedly should + /// be avoided, since the resulting move table generated will be identical anyways. + pub fn generate() -> Self { + let mut visited = vec![false; C::count()]; + let mut stack = vec![CubieCube::SOLVED]; + + let mut table: Vec> = (0..M::count()) + .map(|_| vec![C::default(); C::count()]) + .collect(); + + while let Some(cur_cube) = stack.pop() { + let c = C::from_puzzle(&cur_cube); + for mv in M::moves() { + let next = cur_cube.make_move(mv.clone().into_move()); + let c2 = C::from_puzzle(&next); + + table[mv.index()][c.repr()] = c2; + + if !visited[c.repr()] { + visited[c.repr()] = true; + stack.push(next); + } + } + } + + Self { + table, + _phantom: PhantomData, + } + } + + /// Determine what coordinate comes from applying a move. + fn apply(&self, coord: C, mv: M) -> C { + self.table[mv.index()][coord.repr()] + } +} + +impl SubMove for Move333 { + fn into_move(self) -> Move333 { + self + } + + fn count() -> usize { + 18 + } + + fn moves() -> Vec { + use crate::cube333::moves::MoveGenerator; + crate::cube333::moves::Htm::MOVE_LIST.to_vec() + } + + fn index(self) -> usize { + self.into() + } +} + +// TODO proptest DrMoves preserve phase 2 + +/// A move in domino reduction (phase 2). +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum DrMove { + R2, + L2, + F2, + B2, + U(u8), + 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) % 4 == 0 => Cancellation::NoMove, + (M::D(n), M::D(m)) if (n + m) % 4 == 0 => 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 count() -> usize { + 10 + } + + fn moves() -> Vec { + use DrMove as M; + vec![ + M::R2, + M::L2, + M::F2, + M::B2, + M::U(1), + M::U(2), + M::U(3), + M::D(1), + M::D(2), + M::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) => 4 + (n % 4) as usize, + DrMove::D(n) => 7 + (n % 4) as usize, + } + } +} + +type COMoveTable = MoveTable; +type EOMoveTable = MoveTable; +type ESliceEdgeMoveTable = MoveTable; +type DominoCPMoveTable = MoveTable; +type DominoEPTable = MoveTable; +type DominoESliceMoveTable = MoveTable; 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. From 189d496f5b7a23e18f7712d7549ff95740648d08 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Mon, 21 Jul 2025 19:01:14 +1000 Subject: [PATCH 05/45] Test move tables --- src/coord.rs | 2 +- src/cube333/moves.rs | 2 +- src/cube333/two_phase_solver/coords.rs | 17 +-- src/cube333/two_phase_solver/move_tables.rs | 111 ++++++++++++++++++-- 4 files changed, 114 insertions(+), 18 deletions(-) diff --git a/src/coord.rs b/src/coord.rs index a787771..15a9d67 100644 --- a/src/coord.rs +++ b/src/coord.rs @@ -2,7 +2,7 @@ //! of a puzzle. /// A coordinate type, encoding cosets of the puzzle P. -pub trait Coordinate

: Copy + Default { +pub trait Coordinate

: Copy + Default + Eq { /// Obtain the coordinate that corresponds to the given puzzle. fn from_puzzle(puzzle: &P) -> Self; diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index a006925..c85063f 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -99,7 +99,7 @@ impl From for usize { macro_rules! mv { ($ty:ident, $count: expr) => { Move333 { - ty: crate::cube333::moves::Move333Type::$ty, + ty: Move333Type::$ty, count: $count, } }; diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index c613738..a0db836 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -12,8 +12,12 @@ use crate::cube333::{ fn to_p_coord( arr: &[u8; COUNT], ) -> u16 { - (LOWER..UPPER).rev().fold(0, |acc, idx| { - (acc * (idx + 1) as u16) + arr[0..idx].iter().filter(|&&x| x > arr[idx]).count() as 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 }) } @@ -47,13 +51,13 @@ impl Coordinate for ESliceEdgeCoord { k += 1; if k == 1 { // if k was previously zero, c was 0, so just set c to be the next n value - c = n + 1; + 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) % (k - 1) == 0); + debug_assert!((c * (n+1)) % (k - 1) == 0); c = c * (n + 1) / (k - 1); } } else if k > 0 { @@ -61,8 +65,8 @@ impl Coordinate for ESliceEdgeCoord { // 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) % (n - k + 1) == 0); - c = c * (n + 1) / (n - k + 1); + debug_assert!((c * (n+1)) % (n+1 - k+1) == 0); + c = c * (n + 1) / (n+1 - k+1); } } @@ -172,5 +176,6 @@ mod test { coords.insert(coord); } assert!(coords.len() == ESliceEdgeCoord::count()); + assert!(coords.iter().all(|c| c.repr() < ESliceEdgeCoord::count())); } } diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 6118778..c56a162 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -1,14 +1,20 @@ //! Move tables for each coordinate type use crate::coord::Coordinate; -use crate::cube333::{CubieCube, moves::Move333}; -use crate::moves::{Cancellation, Move}; +use crate::cube333::CubieCube; +use crate::cube333::moves::{Move333, Move333Type}; +use crate::moves::{Cancellation, Move, MoveSequence}; use super::coords::{DominoEPCoord, DominoESliceCoord, ESliceEdgeCoord}; use crate::cube333::coordcube::{COCoord, CPCoord, EOCoord}; 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. @@ -29,7 +35,7 @@ pub trait SubMove: Move { /// A move table, which stores mappings of coordinate + move pairs to the coordinate that results /// from applying the move. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MoveTable> { table: Vec>, _phantom: PhantomData, @@ -41,6 +47,7 @@ impl> MoveTable { pub fn generate() -> Self { let mut visited = vec![false; C::count()]; let mut stack = vec![CubieCube::SOLVED]; + visited[0] = true; let mut table: Vec> = (0..M::count()) .map(|_| vec![C::default(); C::count()]) @@ -54,13 +61,15 @@ impl> MoveTable { table[mv.index()][c.repr()] = c2; - if !visited[c.repr()] { - visited[c.repr()] = true; + if !visited[c2.repr()] { + visited[c2.repr()] = true; stack.push(next); } } } + debug_assert!(visited.into_iter().all(|b| b)); + Self { table, _phantom: PhantomData, @@ -68,9 +77,14 @@ impl> MoveTable { } /// Determine what coordinate comes from applying a move. - fn apply(&self, coord: C, mv: M) -> C { + pub fn apply(&self, coord: C, mv: M) -> C { self.table[mv.index()][coord.repr()] } + + /// Determine what coordinate comes from applying a sequence of moves. + fn apply_sequence(&self, coord: C, alg: MoveSequence) -> C { + alg.0.into_iter().fold(coord, |c, m| self.apply(c, m)) + } } impl SubMove for Move333 { @@ -95,13 +109,22 @@ impl SubMove for Move333 { // TODO proptest DrMoves preserve phase 2 /// A move in domino reduction (phase 2). -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[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(|n| DrMove::U(n))", weight = 3) + )] U(u8), + #[cfg_attr( + test, + proptest(strategy = "(1..=3u8).prop_map(|n| DrMove::D(n))", weight = 3) + )] D(u8), } @@ -180,8 +203,8 @@ impl SubMove for DrMove { 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) => 4 + (n % 4) as usize, - DrMove::D(n) => 7 + (n % 4) as usize, + DrMove::U(n) => 3 + (n % 4) as usize, + DrMove::D(n) => 6 + (n % 4) as usize, } } } @@ -190,5 +213,73 @@ type COMoveTable = MoveTable; type EOMoveTable = MoveTable; type ESliceEdgeMoveTable = MoveTable; type DominoCPMoveTable = MoveTable; -type DominoEPTable = MoveTable; +type DominoEPMoveTable = MoveTable; type DominoESliceMoveTable = MoveTable; + +#[cfg(test)] +mod test { + use super::*; + + use proptest::collection::vec; + use proptest::prelude::*; + + #[test] + fn generates() { + COMoveTable::generate(); + EOMoveTable::generate(); + ESliceEdgeMoveTable::generate(); + DominoCPMoveTable::generate(); + DominoEPMoveTable::generate(); + DominoESliceMoveTable::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 + std::fmt::Debug>( + table: &MoveTable, + p: CubieCube, + mvs: MoveSequence, + ) { + let l = table.apply_sequence(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); + } + + #[test] + fn commutes_normal() { + let co_table = COMoveTable::generate(); + let eo_table = EOMoveTable::generate(); + let eslice_table = ESliceEdgeMoveTable::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v)))| { + diagram_commutes(&co_table, CubieCube::SOLVED, mvs.clone()); + diagram_commutes(&eo_table, CubieCube::SOLVED, mvs.clone()); + diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); + }); + } + + #[test] + fn commutes_domino() { + let cp_table = DominoCPMoveTable::generate(); + let ep_table = DominoEPMoveTable::generate(); + let eslice_table = DominoESliceMoveTable::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v)))| { + diagram_commutes(&cp_table, CubieCube::SOLVED, mvs.clone()); + diagram_commutes(&ep_table, CubieCube::SOLVED, mvs.clone()); + diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); + }); + } +} From 2394a702c2d9cd992b5dac0d4d5ca1c03eeb13b5 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Mon, 21 Jul 2025 22:55:47 +1000 Subject: [PATCH 06/45] Attempt to improve dev mode perf, but there's only so much that's possible... --- src/cube333/corner.rs | 41 +++++++---------- src/cube333/edge.rs | 50 ++++++++------------- src/cube333/moves.rs | 34 +++++--------- src/cube333/two_phase_solver/coords.rs | 6 +-- src/cube333/two_phase_solver/move_tables.rs | 44 +++++++++--------- 5 files changed, 69 insertions(+), 106 deletions(-) diff --git a/src/cube333/corner.rs b/src/cube333/corner.rs index c2dc4b4..5e7e567 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 } } diff --git a/src/cube333/edge.rs b/src/cube333/edge.rs index 2f4743f..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; @@ -74,20 +75,7 @@ impl Edge { 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 } } @@ -116,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/moves.rs b/src/cube333/moves.rs index c85063f..7ef83cf 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -50,8 +50,7 @@ impl crate::moves::Move for Move333 { } } - fn cancel(self, b: Self) -> Cancellation - { + fn cancel(self, b: Self) -> Cancellation { if self.ty == b.ty { let count = (self.count + b.count) % 4; if count == 0 { @@ -153,7 +152,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], @@ -169,7 +168,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], @@ -179,27 +178,18 @@ 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 + 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 + 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 { + 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]; @@ -216,13 +206,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()); diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index a0db836..e1fa059 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -57,7 +57,7 @@ impl Coordinate for ESliceEdgeCoord { // 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)) % (k - 1) == 0); + debug_assert!((c * (n + 1)) % (k - 1) == 0); c = c * (n + 1) / (k - 1); } } else if k > 0 { @@ -65,8 +65,8 @@ impl Coordinate for ESliceEdgeCoord { // 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)) % (n+1 - k+1) == 0); - c = c * (n + 1) / (n+1 - k+1); + debug_assert!((c * (n + 1)) % (n + 1 - k + 1) == 0); + c = c * (n + 1) / (n + 1 - k + 1); } } diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index c56a162..c6af1a6 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -18,7 +18,10 @@ 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 { +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; @@ -27,7 +30,7 @@ pub trait SubMove: Move { /// The list of all moves that this type encodes. The length of the returned vector should be /// `count()`. - fn moves() -> Vec; + const MOVE_LIST: &'static [Self]; /// Get the index of this move in the move list. fn index(self) -> usize; @@ -55,8 +58,8 @@ impl> MoveTable { while let Some(cur_cube) = stack.pop() { let c = C::from_puzzle(&cur_cube); - for mv in M::moves() { - let next = cur_cube.make_move(mv.clone().into_move()); + for mv in M::MOVE_LIST { + let next = cur_cube.clone().make_move(mv.into_move()); let c2 = C::from_puzzle(&next); table[mv.index()][c.repr()] = c2; @@ -87,6 +90,7 @@ impl> MoveTable { } } +use crate::cube333::moves::MoveGenerator; impl SubMove for Move333 { fn into_move(self) -> Move333 { self @@ -96,10 +100,7 @@ impl SubMove for Move333 { 18 } - fn moves() -> Vec { - use crate::cube333::moves::MoveGenerator; - crate::cube333::moves::Htm::MOVE_LIST.to_vec() - } + const MOVE_LIST: &'static [Move333] = crate::cube333::moves::Htm::MOVE_LIST; fn index(self) -> usize { self.into() @@ -179,21 +180,18 @@ impl SubMove for DrMove { 10 } - fn moves() -> Vec { - use DrMove as M; - vec![ - M::R2, - M::L2, - M::F2, - M::B2, - M::U(1), - M::U(2), - M::U(3), - M::D(1), - M::D(2), - M::D(3), - ] - } + 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 { From d28e08010b65f28774f31208cf6494735db47782 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Thu, 24 Jul 2025 14:28:49 +1000 Subject: [PATCH 07/45] consistent naming --- src/cube333/two_phase_solver/move_tables.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index c6af1a6..14df51b 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -80,13 +80,13 @@ impl> MoveTable { } /// Determine what coordinate comes from applying a move. - pub fn apply(&self, coord: C, mv: M) -> C { + pub fn make_move(&self, coord: C, mv: M) -> C { self.table[mv.index()][coord.repr()] } /// Determine what coordinate comes from applying a sequence of moves. - fn apply_sequence(&self, coord: C, alg: MoveSequence) -> C { - alg.0.into_iter().fold(coord, |c, m| self.apply(c, m)) + fn make_moves(&self, coord: C, alg: MoveSequence) -> C { + alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) } } @@ -250,7 +250,7 @@ mod test { p: CubieCube, mvs: MoveSequence, ) { - let l = table.apply_sequence(C::from_puzzle(&p), mvs.clone()); + 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(), ))); From e1b54b111234989a546c3bbbd879be1d2f354614 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Thu, 24 Jul 2025 15:46:35 +1000 Subject: [PATCH 08/45] Add option for submoves to generate successor cube states more efficiently --- src/cube333/moves.rs | 7 ++++--- src/cube333/two_phase_solver/move_tables.rs | 11 +++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 7ef83cf..f5ac571 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -178,18 +178,19 @@ const EP_OFFSETS: [[u8; 12]; 6] = [ ]; impl CubieCube { - /// Copy the cube and apply an algorithm to it. + /// 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. + /// 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 { + /// 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]; diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 14df51b..bfd6501 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -32,6 +32,14 @@ where /// `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; } @@ -58,8 +66,7 @@ impl> MoveTable { while let Some(cur_cube) = stack.pop() { let c = C::from_puzzle(&cur_cube); - for mv in M::MOVE_LIST { - let next = cur_cube.clone().make_move(mv.into_move()); + for (mv, next) in M::successor_states(cur_cube) { let c2 = C::from_puzzle(&next); table[mv.index()][c.repr()] = c2; From b0ec9255c95a4684b2ed61f2b3804f9422ea8892 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 26 Jul 2025 00:53:45 +1000 Subject: [PATCH 09/45] Use boxed array to get contiguous memory over moves --- src/cube333/two_phase_solver/move_tables.rs | 35 +++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index bfd6501..0b2825a 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -47,12 +47,12 @@ where /// 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> { - table: Vec>, +pub struct MoveTable, const MOVES: usize> { + table: Box<[[C; MOVES]]>, _phantom: PhantomData, } -impl> MoveTable { +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 be identical anyways. pub fn generate() -> Self { @@ -60,16 +60,15 @@ impl> MoveTable { let mut stack = vec![CubieCube::SOLVED]; visited[0] = true; - let mut table: Vec> = (0..M::count()) - .map(|_| vec![C::default(); C::count()]) - .collect(); + 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[mv.index()][c.repr()] = c2; + table[c.repr()][mv.index()] = c2; if !visited[c2.repr()] { visited[c2.repr()] = true; @@ -88,7 +87,7 @@ impl> MoveTable { /// Determine what coordinate comes from applying a move. pub fn make_move(&self, coord: C, mv: M) -> C { - self.table[mv.index()][coord.repr()] + self.table[coord.repr()][mv.index()] } /// Determine what coordinate comes from applying a sequence of moves. @@ -214,12 +213,12 @@ impl SubMove for DrMove { } } -type COMoveTable = MoveTable; -type EOMoveTable = MoveTable; -type ESliceEdgeMoveTable = MoveTable; -type DominoCPMoveTable = MoveTable; -type DominoEPMoveTable = MoveTable; -type DominoESliceMoveTable = MoveTable; +type COMoveTable = MoveTable; +type EOMoveTable = MoveTable; +type ESliceEdgeMoveTable = MoveTable; +type DominoCPMoveTable = MoveTable; +type DominoEPMoveTable = MoveTable; +type DominoESliceMoveTable = MoveTable; #[cfg(test)] mod test { @@ -252,8 +251,12 @@ mod test { * Move application should be compatable with coordinate translation. */ - fn diagram_commutes + std::fmt::Debug>( - table: &MoveTable, + fn diagram_commutes< + M: SubMove, + C: Coordinate + std::fmt::Debug, + const MOVES: usize, + >( + table: &MoveTable, p: CubieCube, mvs: MoveSequence, ) { From d88e4c4592296fba04ed380375dd79cb83485eea Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 26 Jul 2025 04:18:55 +1000 Subject: [PATCH 10/45] Apply and invert operations on the cubiecube group --- src/cube333/corner.rs | 9 ++++++++ src/cube333/moves.rs | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/cube333/corner.rs b/src/cube333/corner.rs index 5e7e567..1721117 100644 --- a/src/cube333/corner.rs +++ b/src/cube333/corner.rs @@ -128,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/moves.rs b/src/cube333/moves.rs index f5ac571..5364591 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -223,6 +223,47 @@ 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 { + // this is kinda confusing but like try it on a cube + 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)] @@ -264,5 +305,12 @@ mod tests { let cancelled = mvs.clone().cancel(); assert_eq!(cancelled.clone().cancel(), cancelled); } + + #[test] + fn inverse_apply(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v))) { + // 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())); + } } } From a527147492ddcac826ddb0acb6d3999af79c62d1 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 26 Jul 2025 16:57:35 +1000 Subject: [PATCH 11/45] DR Symmetries of a cubiecube --- src/cube333/moves.rs | 1 + src/cube333/two_phase_solver/mod.rs | 1 + src/cube333/two_phase_solver/symmetry.rs | 95 ++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 src/cube333/two_phase_solver/symmetry.rs diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 5364591..d88ecdf 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -311,6 +311,7 @@ mod tests { // 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/mod.rs b/src/cube333/two_phase_solver/mod.rs index 3e3a2c2..fea93f5 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -2,3 +2,4 @@ mod coords; mod move_tables; +mod symmetry; diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs new file mode 100644 index 0000000..8ce303b --- /dev/null +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -0,0 +1,95 @@ +//! Implements symmetries for the `CubieCube` and symmetry tables for use in move table generation. + +use crate::cube333::{Corner as C, CornerTwist as CT, CubieCube, Edge as E, EdgeFlip as EF}; + +/// 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, 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, applies the RL2 symmetry. +#[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 + } + + /// Obtain a CubieCube that applies this symmetry when multiplied. + fn to_cubie(self) -> CubieCube { + let mut res = CubieCube::SOLVED; + + for _ in 0..self.rl2_count() { + res = res.multiply_cube(SYM_RL2); + } + + for _ in 0..self.f2_count() { + res = res.multiply_cube(SYM_F2); + } + + for _ in 0..self.u4_count() { + res = res.multiply_cube(SYM_U4); + } + + res + } +} + +impl CubieCube { + /// Obtain the cube given by applying some symmetry + fn apply_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { + self.multiply_cube(sym.to_cubie()) + } + + /// Obtain the cube given by conjugating by some symmetry. We conjugate in the order S C S^-1 + fn conjuate_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { + sym.to_cubie() + .multiply_cube(self) + .multiply_cube(sym.to_cubie().inverse()) + } +} From 42b76e00a9cbd080c4c22c0c2195b8edfca7a4cb Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 26 Jul 2025 23:39:37 +1000 Subject: [PATCH 12/45] DrSymmetry multiplication table --- src/cube333/moves.rs | 8 ++-- src/cube333/two_phase_solver/move_tables.rs | 8 ++-- src/cube333/two_phase_solver/symmetry.rs | 45 +++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index d88ecdf..009b383 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -287,27 +287,27 @@ 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(|v| MoveSequence(v))) { + 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())); diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 0b2825a..1778d93 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -125,12 +125,12 @@ pub enum DrMove { B2, #[cfg_attr( test, - proptest(strategy = "(1..=3u8).prop_map(|n| DrMove::U(n))", weight = 3) + proptest(strategy = "(1..=3u8).prop_map(DrMove::U)", weight = 3) )] U(u8), #[cfg_attr( test, - proptest(strategy = "(1..=3u8).prop_map(|n| DrMove::D(n))", weight = 3) + proptest(strategy = "(1..=3u8).prop_map(DrMove::D)", weight = 3) )] D(u8), } @@ -272,7 +272,7 @@ mod test { let co_table = COMoveTable::generate(); let eo_table = EOMoveTable::generate(); let eslice_table = ESliceEdgeMoveTable::generate(); - proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v)))| { + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { diagram_commutes(&co_table, CubieCube::SOLVED, mvs.clone()); diagram_commutes(&eo_table, CubieCube::SOLVED, mvs.clone()); diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); @@ -284,7 +284,7 @@ mod test { let cp_table = DominoCPMoveTable::generate(); let ep_table = DominoEPMoveTable::generate(); let eslice_table = DominoESliceMoveTable::generate(); - proptest!(|(mvs in vec(any::(), 0..20).prop_map(|v| MoveSequence(v)))| { + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { diagram_commutes(&cp_table, CubieCube::SOLVED, mvs.clone()); diagram_commutes(&ep_table, CubieCube::SOLVED, mvs.clone()); diagram_commutes(&eslice_table, CubieCube::SOLVED, mvs.clone()); diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 8ce303b..127d29d 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -93,3 +93,48 @@ impl CubieCube { .multiply_cube(sym.to_cubie().inverse()) } } + +/// Multiplication table for the `DrSymmetry` group. +struct DrSymTable { + table: [[DrSymmetry; 16]; 16], +} + +impl DrSymTable { + /// Generate the table + fn generate() -> Self { + use std::array::from_fn; + + let cubie_syms: [_; 16] = from_fn(|n| DrSymmetry(n as u8).to_cubie()); + + let table = from_fn(|a| { + from_fn(|b| { + let a = DrSymmetry(a as u8); + let b = DrSymmetry(b as u8); + let c = a.to_cubie().multiply_cube(b.to_cubie()); + DrSymmetry(cubie_syms.iter().position(|d| &c == d).unwrap() as u8) + }) + }); + + DrSymTable { table } + } + + /// Multiply two symmetries + fn multiply(&self, a: DrSymmetry, b: DrSymmetry) -> DrSymmetry { + self.table[a.0 as usize][b.0 as usize] + } +} + +#[cfg(test)] +mod test { + use super::*; + + use proptest::prelude::*; + + #[test] + fn multiplication_correct() { + let table = DrSymTable::generate(); + proptest!(|(sym1 in (0..16u8).prop_map(DrSymmetry), sym2 in (0..16u8).prop_map(DrSymmetry))| { + assert_eq!(table.multiply(sym1, sym2).to_cubie(), sym1.to_cubie().multiply_cube(sym2.to_cubie())); + }) + } +} From 99fd60ec4b98b2435d766f4185c4639896d227e2 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sun, 27 Jul 2025 03:41:20 +1000 Subject: [PATCH 13/45] sym and raw coordinate conversion tables --- src/coord.rs | 16 ++++ src/cube333/coordcube.rs | 16 ++++ src/cube333/two_phase_solver/coords.rs | 82 ++++++++++++++++++++- src/cube333/two_phase_solver/move_tables.rs | 10 +-- src/cube333/two_phase_solver/symmetry.rs | 15 ++-- 5 files changed, 125 insertions(+), 14 deletions(-) diff --git a/src/coord.rs b/src/coord.rs index 15a9d67..5284fa6 100644 --- a/src/coord.rs +++ b/src/coord.rs @@ -3,6 +3,8 @@ /// 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; @@ -11,4 +13,18 @@ pub trait Coordinate

: Copy + Default + Eq { /// 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 91d63d7..bfa446d 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -30,6 +30,10 @@ impl Coordinate for COCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + COCoord(n as u16) + } } impl Coordinate for CPCoord { @@ -44,6 +48,10 @@ impl Coordinate for CPCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + CPCoord(n as u16) + } } impl Coordinate for EOCoord { @@ -59,6 +67,10 @@ impl Coordinate for EOCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + EOCoord(n as u16) + } } impl Coordinate for EPCoord { @@ -74,6 +86,10 @@ impl Coordinate for EPCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + EPCoord(n as u32) + } } /// Implementation of a coord cube, representing pieces using coordinates, which are values which diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index e1fa059..ad4cb9c 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -1,11 +1,12 @@ //! This module contains the coordinate representations of cube states relevant to the two phases //! of these solver. -use crate::coord::Coordinate; +use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{ CubieCube, coordcube::{COCoord, CPCoord, EOCoord}, }; +use super::symmetry::DrSymmetry; // TODO this is kinda unreadable lol // this is copied from coordcube.rs then modified hmmm maybe copy pasting isn't ideal @@ -80,6 +81,10 @@ impl Coordinate for ESliceEdgeCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + ESliceEdgeCoord(n as u16) + } } impl Coordinate for DominoEPCoord { @@ -94,6 +99,10 @@ impl Coordinate for DominoEPCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + DominoEPCoord(n as u16) + } } impl Coordinate for DominoESliceCoord { @@ -108,6 +117,77 @@ impl Coordinate for DominoESliceCoord { fn repr(self) -> usize { self.0 as usize } + + fn from_repr(n: usize) -> Self { + DominoESliceCoord(n as u16) + } +} + +/// A symmetry coordinate over `DrSymmetry`s. A SymCoordinate should include both what equivalence +/// class the coordinate is in, along with the symmetry it has from the representant. +pub trait DrSymCoordinate: Copy + Default + Eq { + type Raw: Coordinate; + + /// The number of possible coordinate states. + fn count() -> usize; + + /// The number of equivalence classes this coordinate encodes modulo symmetry. + fn classes() -> usize; + + /// A representation of this coordinate as a usize, for use, in table lookups. + fn repr(self) -> (usize, DrSymmetry); + + /// 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: DrSymmetry) -> Self; +} + +pub struct RawDrSymTable +where + CubieCube: FromCoordinate, +{ + raw_to_sym: Box<[S]>, + sym_to_raw: Box<[S::Raw]>, +} + +impl RawDrSymTable +where + CubieCube: FromCoordinate, +{ + pub fn generate() -> Self { + let mut raw_to_sym = vec![S::default(); S::Raw::count()].into_boxed_slice(); + let mut sym_to_raw = vec![S::Raw::default(); S::count()].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 sym_to_raw[raw.repr()] != S::Raw::default() { + continue; + } + + let mut c = CubieCube::SOLVED; + c.set_coord(raw); + + // Then we go over every symmetry of this coordinate, and update the tables based on + // them. + for sym in DrSymmetry::ARRAY { + let d = c.clone().conjugate_dr_symmetry(sym); + let raw2 = S::Raw::from_puzzle(&d); + raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); + } + + sym_to_raw[sym_idx] = raw; + sym_idx += 1; + } + + RawDrSymTable { + raw_to_sym, + sym_to_raw, + } + } } #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 1778d93..d1d2693 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -123,15 +123,9 @@ pub enum DrMove { L2, F2, B2, - #[cfg_attr( - test, - proptest(strategy = "(1..=3u8).prop_map(DrMove::U)", weight = 3) - )] + #[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) - )] + #[cfg_attr(test, proptest(strategy = "(1..=3u8).prop_map(DrMove::D)", weight = 3))] D(u8), } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 127d29d..4e125e2 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -78,16 +78,21 @@ impl DrSymmetry { res } + + // lol + /// An array of each symmetry + #[rustfmt::ignore] + 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 CubieCube { /// Obtain the cube given by applying some symmetry - fn apply_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { + pub(super) fn apply_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { self.multiply_cube(sym.to_cubie()) } /// Obtain the cube given by conjugating by some symmetry. We conjugate in the order S C S^-1 - fn conjuate_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { + pub(super) fn conjugate_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { sym.to_cubie() .multiply_cube(self) .multiply_cube(sym.to_cubie().inverse()) @@ -95,13 +100,13 @@ impl CubieCube { } /// Multiplication table for the `DrSymmetry` group. -struct DrSymTable { +pub struct DrSymTable { table: [[DrSymmetry; 16]; 16], } impl DrSymTable { /// Generate the table - fn generate() -> Self { + pub fn generate() -> Self { use std::array::from_fn; let cubie_syms: [_; 16] = from_fn(|n| DrSymmetry(n as u8).to_cubie()); @@ -119,7 +124,7 @@ impl DrSymTable { } /// Multiply two symmetries - fn multiply(&self, a: DrSymmetry, b: DrSymmetry) -> DrSymmetry { + pub fn multiply(&self, a: DrSymmetry, b: DrSymmetry) -> DrSymmetry { self.table[a.0 as usize][b.0 as usize] } } From fee29422e7e0bd761471d012f2221590af6105cb Mon Sep 17 00:00:00 2001 From: bpaul Date: Mon, 28 Jul 2025 17:35:57 +1000 Subject: [PATCH 14/45] Move symmetries into a trait --- src/coord.rs | 2 +- src/cube333/two_phase_solver/coords.rs | 19 ++-- src/cube333/two_phase_solver/symmetry.rs | 118 ++++++++++++++--------- 3 files changed, 85 insertions(+), 54 deletions(-) diff --git a/src/coord.rs b/src/coord.rs index 5284fa6..cb52139 100644 --- a/src/coord.rs +++ b/src/coord.rs @@ -11,7 +11,7 @@ pub trait Coordinate

: Copy + Default + Eq { /// The number of possible coordinate states. fn count() -> usize; - /// A representation of this coordinate as a usize, for use, in table lookups. + /// 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 diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index ad4cb9c..d7ca6fd 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -1,12 +1,12 @@ //! This module contains the coordinate representations of cube states relevant to the two phases //! of these solver. +use super::symmetry::Symmetry; use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{ CubieCube, coordcube::{COCoord, CPCoord, EOCoord}, }; -use super::symmetry::DrSymmetry; // TODO this is kinda unreadable lol // this is copied from coordcube.rs then modified hmmm maybe copy pasting isn't ideal @@ -123,9 +123,10 @@ impl Coordinate for DominoESliceCoord { } } -/// A symmetry coordinate over `DrSymmetry`s. A SymCoordinate should include both what equivalence +/// 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 DrSymCoordinate: Copy + Default + Eq { +pub trait SymCoordinate: Copy + Default + Eq { + type Sym: Symmetry; type Raw: Coordinate; /// The number of possible coordinate states. @@ -135,14 +136,14 @@ pub trait DrSymCoordinate: Copy + Default + Eq { fn classes() -> usize; /// A representation of this coordinate as a usize, for use, in table lookups. - fn repr(self) -> (usize, DrSymmetry); + fn repr(self) -> (usize, 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: DrSymmetry) -> Self; + fn from_repr(idx: usize, sym: Self::Sym) -> Self; } -pub struct RawDrSymTable +pub struct RawDrSymTable where CubieCube: FromCoordinate, { @@ -150,7 +151,7 @@ where sym_to_raw: Box<[S::Raw]>, } -impl RawDrSymTable +impl RawDrSymTable where CubieCube: FromCoordinate, { @@ -173,8 +174,8 @@ where // Then we go over every symmetry of this coordinate, and update the tables based on // them. - for sym in DrSymmetry::ARRAY { - let d = c.clone().conjugate_dr_symmetry(sym); + 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); } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 4e125e2..5b2c9ac 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -2,6 +2,51 @@ use crate::cube333::{Corner as C, CornerTwist as CT, CubieCube, Edge as E, EdgeFlip as EF}; +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; + + /// Obtain a CubieCube that applies this symmetry when multiplied. + fn to_puzzle(self) -> CubieCube; +} + +/// 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).to_puzzle()); + + let table = from_fn(|a| { + from_fn(|b| { + let a = S::from_repr(a); + let b = S::from_repr(b); + let c = a.to_puzzle().multiply_cube(b.to_puzzle()); + 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()] + } +} + /// 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) @@ -17,6 +62,8 @@ use crate::cube333::{Corner as C, CornerTwist as CT, CubieCube, Edge as E, EdgeF #[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] pub struct DrSymmetry(u8); +pub type DrSymMultTable = SymMultTable; + /// A cube that, when multiplied, applies the F2 symmetry. #[rustfmt::skip] const SYM_F2: CubieCube = CubieCube { @@ -60,8 +107,26 @@ impl DrSymmetry { (self.0 >> 2 & 3) as usize } - /// Obtain a CubieCube that applies this symmetry when multiplied. - fn to_cubie(self) -> CubieCube { + // 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 to_puzzle(self) -> CubieCube { let mut res = CubieCube::SOLVED; for _ in 0..self.rl2_count() { @@ -78,54 +143,19 @@ impl DrSymmetry { res } - - // lol - /// An array of each symmetry - #[rustfmt::ignore] - 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 CubieCube { /// Obtain the cube given by applying some symmetry - pub(super) fn apply_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { - self.multiply_cube(sym.to_cubie()) + pub(super) fn apply_symmetry(self, sym: S) -> CubieCube { + self.multiply_cube(sym.to_puzzle()) } /// Obtain the cube given by conjugating by some symmetry. We conjugate in the order S C S^-1 - pub(super) fn conjugate_dr_symmetry(self, sym: DrSymmetry) -> CubieCube { - sym.to_cubie() + pub(super) fn conjugate_symmetry(self, sym: S) -> CubieCube { + sym.to_puzzle() .multiply_cube(self) - .multiply_cube(sym.to_cubie().inverse()) - } -} - -/// Multiplication table for the `DrSymmetry` group. -pub struct DrSymTable { - table: [[DrSymmetry; 16]; 16], -} - -impl DrSymTable { - /// Generate the table - pub fn generate() -> Self { - use std::array::from_fn; - - let cubie_syms: [_; 16] = from_fn(|n| DrSymmetry(n as u8).to_cubie()); - - let table = from_fn(|a| { - from_fn(|b| { - let a = DrSymmetry(a as u8); - let b = DrSymmetry(b as u8); - let c = a.to_cubie().multiply_cube(b.to_cubie()); - DrSymmetry(cubie_syms.iter().position(|d| &c == d).unwrap() as u8) - }) - }); - - DrSymTable { table } - } - - /// Multiply two symmetries - pub fn multiply(&self, a: DrSymmetry, b: DrSymmetry) -> DrSymmetry { - self.table[a.0 as usize][b.0 as usize] + .multiply_cube(sym.to_puzzle().inverse()) } } @@ -137,9 +167,9 @@ mod test { #[test] fn multiplication_correct() { - let table = DrSymTable::generate(); + let table = DrSymMultTable::generate(); proptest!(|(sym1 in (0..16u8).prop_map(DrSymmetry), sym2 in (0..16u8).prop_map(DrSymmetry))| { - assert_eq!(table.multiply(sym1, sym2).to_cubie(), sym1.to_cubie().multiply_cube(sym2.to_cubie())); + assert_eq!(table.multiply(sym1, sym2).to_puzzle(), sym1.to_puzzle().multiply_cube(sym2.to_puzzle())); }) } } From f0ab1dee3b757e0dff9d885319a1d4a711ddae89 Mon Sep 17 00:00:00 2001 From: bpaul Date: Tue, 29 Jul 2025 16:37:01 +1000 Subject: [PATCH 15/45] Half symmetry type --- src/cube333/two_phase_solver/symmetry.rs | 85 +++++++++++++++++++++--- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 5b2c9ac..908248b 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -17,6 +17,20 @@ pub trait Symmetry: Copy + Default + Eq { fn to_puzzle(self) -> CubieCube; } +impl CubieCube { + /// Obtain the cube given by applying some symmetry + pub(super) fn apply_symmetry(self, sym: S) -> CubieCube { + self.multiply_cube(sym.to_puzzle()) + } + + /// 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.to_puzzle() + .multiply_cube(self) + .multiply_cube(sym.to_puzzle().inverse()) + } +} + /// Multiplication table for a `Symmetry` group. pub struct SymMultTable { table: [[S; COUNT]; COUNT], @@ -59,7 +73,7 @@ impl SymMultTable { // 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, Copy, Clone, Hash)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] pub struct DrSymmetry(u8); pub type DrSymMultTable = SymMultTable; @@ -145,17 +159,68 @@ impl Symmetry for DrSymmetry { } } -impl CubieCube { - /// Obtain the cube given by applying some symmetry - pub(super) fn apply_symmetry(self, sym: S) -> CubieCube { - self.multiply_cube(sym.to_puzzle()) +/// 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 } - /// 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.to_puzzle() - .multiply_cube(self) - .multiply_cube(sym.to_puzzle().inverse()) + /// 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 u2_count(self) -> usize { + (self.0 >> 2 & 3) 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 to_puzzle(self) -> CubieCube { + let mut res = CubieCube::SOLVED; + + for _ in 0..self.rl2_count() { + res = res.multiply_cube(SYM_RL2); + } + + for _ in 0..self.f2_count() { + res = res.multiply_cube(SYM_F2); + } + + for _ in 0..self.u2_count() { + res = res.multiply_cube(SYM_U4.multiply_cube(SYM_U4)); + } + + res } } From 3ff0de51b42a3b11f7af56e68a09943676e52cdb Mon Sep 17 00:00:00 2001 From: bpaul Date: Tue, 29 Jul 2025 17:43:45 +1000 Subject: [PATCH 16/45] Make orientation sym coords and find bug! --- src/cube333/coordcube.rs | 78 ++++++++++++++++++-- src/cube333/two_phase_solver/coords.rs | 91 ++++++++++++++++++++---- src/cube333/two_phase_solver/symmetry.rs | 6 +- 3 files changed, 155 insertions(+), 20 deletions(-) diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs index bfa446d..4638911 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -1,20 +1,20 @@ use super::{Corner, CornerTwist, CubieCube, Edge, EdgeFlip}; -use crate::coord::Coordinate; +use crate::coord::{Coordinate, FromCoordinate}; /// A coordinate representation of the corner orientation of a cube with respect to the U/F faces. -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] +#[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, Copy, Clone, Hash)] +#[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, Copy, Clone, Hash)] +#[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, Copy, Clone, Hash)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] pub struct EPCoord(u32); impl Coordinate for COCoord { @@ -36,6 +36,31 @@ impl Coordinate for COCoord { } } +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) @@ -73,6 +98,27 @@ impl Coordinate for EOCoord { } } +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()))) @@ -240,6 +286,7 @@ impl CubieCube { #[cfg(test)] mod tests { + use super::*; use crate::cube333::{ Corner, CornerTwist, CubieCube, Edge, EdgeFlip, StickerCube, coordcube::{CoordCube, CubieToCoordError}, @@ -329,4 +376,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/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index d7ca6fd..4ea6fec 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -1,7 +1,7 @@ //! This module contains the coordinate representations of cube states relevant to the two phases //! of these solver. -use super::symmetry::Symmetry; +use super::symmetry::{DrSymmetry, HalfSymmetry, Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{ CubieCube, @@ -23,17 +23,17 @@ fn to_p_coord( } /// Coordinate for positions of E slice edges (ignoring what the edges actually arge) -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone, Hash)] +#[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, Copy, Clone, Hash)] +#[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, Copy, Clone, Hash)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] pub struct DominoESliceCoord(u16); impl Coordinate for ESliceEdgeCoord { @@ -143,21 +143,21 @@ pub trait SymCoordinate: Copy + Default + Eq { fn from_repr(idx: usize, sym: Self::Sym) -> Self; } -pub struct RawDrSymTable +pub struct RawSymTable where CubieCube: FromCoordinate, { raw_to_sym: Box<[S]>, - sym_to_raw: Box<[S::Raw]>, + class_to_repr: Box<[S::Raw]>, } -impl RawDrSymTable +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 sym_to_raw = vec![S::Raw::default(); S::count()].into_boxed_slice(); + let mut class_to_repr = vec![S::Raw::default(); S::classes()].into_boxed_slice(); let mut sym_idx = 0; @@ -165,7 +165,7 @@ where // 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 sym_to_raw[raw.repr()] != S::Raw::default() { + if raw_to_sym[raw.repr()] != S::default() { continue; } @@ -180,17 +180,75 @@ where raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); } - sym_to_raw[sym_idx] = raw; + class_to_repr[sym_idx] = raw; sym_idx += 1; } - RawDrSymTable { + println!("{sym_idx}"); + + RawSymTable { raw_to_sym, - sym_to_raw, + class_to_repr, } } } +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] +pub struct COSymCoord(u16); + +impl SymCoordinate for COSymCoord { + type Sym = DrSymmetry; + + type Raw = COCoord; + + fn count() -> usize { + Self::classes() * 16 + } + + fn classes() -> usize { + 1000 + } + + fn repr(self) -> (usize, Self::Sym) { + ( + (self.0 >> 4) as usize, + DrSymmetry::from_repr((self.0 & 15) as usize), + ) + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + COSymCoord((idx as u16) << 4 | (sym.repr() as u16)) + } +} + +#[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 count() -> usize { + Self::classes() * 8 + } + + fn classes() -> usize { + 336 + } + + fn repr(self) -> (usize, Self::Sym) { + ( + (self.0 >> 3) as usize, + HalfSymmetry::from_repr((self.0 & 7) as usize), + ) + } + + fn from_repr(idx: usize, sym: Self::Sym) -> Self { + EOSymCoord((idx as u16) << 3 | (sym.repr() as u16)) + } +} + #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub struct Phase1Cube { co: COCoord, @@ -239,7 +297,7 @@ mod test { use itertools::Itertools; - use super::ESliceEdgeCoord; + use super::*; use crate::{coord::Coordinate, cube333::CubieCube}; #[test] @@ -259,4 +317,11 @@ mod test { assert!(coords.len() == ESliceEdgeCoord::count()); assert!(coords.iter().all(|c| c.repr() < ESliceEdgeCoord::count())); } + + #[test] + fn sym_table_generates() { + RawSymTable::::generate(); + RawSymTable::::generate(); + panic!(); + } } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 908248b..0b0e176 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -96,6 +96,8 @@ const SYM_U4: CubieCube = CubieCube { 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], }; +// TODO BUG!!! reflection isn't an element of the cube!!! applying this symmetry is meant to flip +// clockwise and anticlockwise, but here it doesn't! /// A cube that, when multiplied, applies the RL2 symmetry. #[rustfmt::skip] const SYM_RL2: CubieCube = CubieCube { @@ -181,9 +183,9 @@ impl HalfSymmetry { (self.0 >> 1 & 1) as usize } - /// Returns the power of U4 in the standard product notation of this symmetry. + /// Returns the power of U2 in the standard product notation of this symmetry. fn u2_count(self) -> usize { - (self.0 >> 2 & 3) as usize + (self.0 >> 2 & 1) as usize } // lol From 3def01062f07cbb02db2d4333c27be1e0478a1a5 Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sat, 2 Aug 2025 00:00:34 +1000 Subject: [PATCH 17/45] Correct symmetries to allow being outside of the cube group --- src/cube333/moves.rs | 2 + src/cube333/two_phase_solver/coords.rs | 17 ++--- src/cube333/two_phase_solver/symmetry.rs | 96 +++++++++++++++++------- 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 009b383..e566741 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -189,6 +189,8 @@ impl CubieCube { (0..mv.count).fold(self, |c, _| c.make_move_type(mv.ty)) } + // 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]; diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 4ea6fec..568fd7e 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -172,8 +172,8 @@ where let mut c = CubieCube::SOLVED; c.set_coord(raw); - // Then we go over every symmetry of this coordinate, and update the tables based on - // them. + // 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); @@ -184,8 +184,6 @@ where sym_idx += 1; } - println!("{sym_idx}"); - RawSymTable { raw_to_sym, class_to_repr, @@ -197,7 +195,7 @@ where pub struct COSymCoord(u16); impl SymCoordinate for COSymCoord { - type Sym = DrSymmetry; + type Sym = HalfSymmetry; type Raw = COCoord; @@ -206,18 +204,18 @@ impl SymCoordinate for COSymCoord { } fn classes() -> usize { - 1000 + 324 } fn repr(self) -> (usize, Self::Sym) { ( - (self.0 >> 4) as usize, - DrSymmetry::from_repr((self.0 & 15) as usize), + (self.0 >> 3) as usize, + HalfSymmetry::from_repr((self.0 & 7) as usize), ) } fn from_repr(idx: usize, sym: Self::Sym) -> Self { - COSymCoord((idx as u16) << 4 | (sym.repr() as u16)) + COSymCoord((idx as u16) << 3 | (sym.repr() as u16)) } } @@ -322,6 +320,5 @@ mod test { fn sym_table_generates() { RawSymTable::::generate(); RawSymTable::::generate(); - panic!(); } } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 0b0e176..433e36d 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -13,21 +13,28 @@ pub trait Symmetry: Copy + Default + Eq { /// Iterator over every symmetry in order of representation fn get_all() -> impl Iterator; - /// Obtain a CubieCube that applies this symmetry when multiplied. - fn to_puzzle(self) -> CubieCube; + /// 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(CubieCube::SOLVED) + .multiply_cube(self.apply_inverse(cube)) + } } impl CubieCube { /// Obtain the cube given by applying some symmetry pub(super) fn apply_symmetry(self, sym: S) -> CubieCube { - self.multiply_cube(sym.to_puzzle()) + 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.to_puzzle() - .multiply_cube(self) - .multiply_cube(sym.to_puzzle().inverse()) + sym.conjugate(self) } } @@ -41,13 +48,15 @@ impl SymMultTable { pub fn generate() -> Self { use std::array::from_fn; - let cubie_syms: [_; COUNT] = from_fn(|n| S::from_repr(n).to_puzzle()); + 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.to_puzzle().multiply_cube(b.to_puzzle()); + let c = a + .apply(CubieCube::SOLVED) + .multiply_cube(b.apply(CubieCube::SOLVED)); S::from_repr(cubie_syms.iter().position(|d| &c == d).unwrap()) }) }); @@ -96,9 +105,8 @@ const SYM_U4: CubieCube = CubieCube { 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], }; -// TODO BUG!!! reflection isn't an element of the cube!!! applying this symmetry is meant to flip -// clockwise and anticlockwise, but here it doesn't! -/// A cube that, when multiplied, applies the RL2 symmetry. +/// 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], @@ -142,22 +150,38 @@ impl Symmetry for DrSymmetry { Self::ARRAY.into_iter() } - fn to_puzzle(self) -> CubieCube { - let mut res = CubieCube::SOLVED; - + fn apply(&self, mut cube: CubieCube) -> CubieCube { for _ in 0..self.rl2_count() { - res = res.multiply_cube(SYM_RL2); + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); } for _ in 0..self.f2_count() { - res = res.multiply_cube(SYM_F2); + cube = cube.multiply_cube(SYM_F2); } for _ in 0..self.u4_count() { - res = res.multiply_cube(SYM_U4); + 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()); } - res + cube } } @@ -207,22 +231,38 @@ impl Symmetry for HalfSymmetry { Self::ARRAY.into_iter() } - fn to_puzzle(self) -> CubieCube { - let mut res = CubieCube::SOLVED; - + fn apply(&self, mut cube: CubieCube) -> CubieCube { for _ in 0..self.rl2_count() { - res = res.multiply_cube(SYM_RL2); + cube = cube.multiply_cube(SYM_RL2); + cube.co = cube.co.map(|c| c.inverse()); } for _ in 0..self.f2_count() { - res = res.multiply_cube(SYM_F2); + cube = cube.multiply_cube(SYM_F2); } for _ in 0..self.u2_count() { - res = res.multiply_cube(SYM_U4.multiply_cube(SYM_U4)); + 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()); } - res + cube } } @@ -236,7 +276,11 @@ mod test { fn multiplication_correct() { let table = DrSymMultTable::generate(); proptest!(|(sym1 in (0..16u8).prop_map(DrSymmetry), sym2 in (0..16u8).prop_map(DrSymmetry))| { - assert_eq!(table.multiply(sym1, sym2).to_puzzle(), sym1.to_puzzle().multiply_cube(sym2.to_puzzle())); + let cube = CubieCube::SOLVED; + assert_eq!( + table.multiply(sym1, sym2).apply(cube.clone()), + sym1.apply(cube.clone()).multiply_cube(sym2.apply(cube.clone())) + ); }) } } From c79281846be41cdfdfb705e3e6f68fa1c31e19cb Mon Sep 17 00:00:00 2001 From: Benjamin Paul Date: Sun, 3 Aug 2025 19:23:20 +1000 Subject: [PATCH 18/45] Incorrect sym move table implementation (tests fail) --- src/cube333/two_phase_solver/coords.rs | 12 +++ src/cube333/two_phase_solver/move_tables.rs | 92 ++++++++++++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 568fd7e..863b10d 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -189,6 +189,18 @@ where 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)) + } } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index d1d2693..e70cdb9 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -1,12 +1,16 @@ //! Move tables for each coordinate type -use crate::coord::Coordinate; +use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::CubieCube; +use crate::cube333::coordcube::{COCoord, CPCoord, EOCoord}; use crate::cube333::moves::{Move333, Move333Type}; use crate::moves::{Cancellation, Move, MoveSequence}; -use super::coords::{DominoEPCoord, DominoESliceCoord, ESliceEdgeCoord}; -use crate::cube333::coordcube::{COCoord, CPCoord, EOCoord}; +use super::coords::{ + COSymCoord, DominoEPCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, + SymCoordinate, +}; +use super::symmetry::SymMultTable; use std::marker::PhantomData; @@ -96,6 +100,59 @@ impl, const MOVES: usize> MoveTable +where + CubieCube: FromCoordinate, +{ + table: Box<[[S; MOVES]]>, + sym_mul_table: SymMultTable, + _phantom: PhantomData, +} + +impl + SymMoveTable +where + CubieCube: FromCoordinate, +{ + 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_mul_table = SymMultTable::generate(); + + SymMoveTable { + table, + sym_mul_table, + _phantom: PhantomData, + } + } + + pub fn make_move(&self, coord: S, mv: M) -> S { + let (idx, sym1) = coord.repr(); + let (idx, sym2) = self.table[idx][mv.index()].repr(); + let sym = self.sym_mul_table.multiply(sym1, sym2); + + S::from_repr(idx, sym) + } + + 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 { @@ -209,6 +266,8 @@ impl SubMove for DrMove { type COMoveTable = MoveTable; type EOMoveTable = MoveTable; +type COSymMoveTable = SymMoveTable; +type EOSymMoveTable = SymMoveTable; type ESliceEdgeMoveTable = MoveTable; type DominoCPMoveTable = MoveTable; type DominoEPMoveTable = MoveTable; @@ -261,15 +320,42 @@ mod test { 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, + { + 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!(l, r); + } + #[test] fn commutes_normal() { let co_table = COMoveTable::generate(); let eo_table = EOMoveTable::generate(); 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(&co_table, CubieCube::SOLVED, mvs.clone()); diagram_commutes(&eo_table, CubieCube::SOLVED, mvs.clone()); 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()); }); } From d7c1c5bd50bb09cb7a99982c9d75a0b2652160a3 Mon Sep 17 00:00:00 2001 From: bpaul Date: Sun, 2 Nov 2025 16:45:11 +1000 Subject: [PATCH 19/45] Check raw sym tables generate correct representatives --- src/cube333/two_phase_solver/coords.rs | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 863b10d..85ab6d0 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -141,6 +141,11 @@ pub trait SymCoordinate: Copy + Default + Eq { /// 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 { + self.repr().0 + } } pub struct RawSymTable @@ -308,7 +313,14 @@ mod test { use itertools::Itertools; use super::*; - use crate::{coord::Coordinate, cube333::CubieCube}; + use crate::{ + coord::Coordinate, + cube333::{CubieCube, moves::Move333}, + moves::MoveSequence, + }; + + use proptest::collection::vec; + use proptest::prelude::*; #[test] fn e_slice_edge_uniqueness() { @@ -333,4 +345,17 @@ mod test { 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()); + }) + } } From c3ee97e33aa07d08728003342c081daf1d7a8450 Mon Sep 17 00:00:00 2001 From: bpaul Date: Sun, 2 Nov 2025 16:49:24 +1000 Subject: [PATCH 20/45] Is multiple of --- src/cube333/two_phase_solver/coords.rs | 6 +++--- src/cube333/two_phase_solver/move_tables.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 85ab6d0..176abf5 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -43,7 +43,7 @@ impl Coordinate for ESliceEdgeCoord { // c is meant to be n choose (k-1) if k >= 1 let mut k = 0; - let mut c = 0; + let mut c = 0u16; for n in 0..12 { if puzzle.ep[n as usize].e_slice() { @@ -58,7 +58,7 @@ impl Coordinate for ESliceEdgeCoord { // 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)) % (k - 1) == 0); + debug_assert!((c * (n + 1)).is_multiple_of(k - 1)); c = c * (n + 1) / (k - 1); } } else if k > 0 { @@ -66,7 +66,7 @@ impl Coordinate for ESliceEdgeCoord { // 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)) % (n + 1 - k + 1) == 0); + debug_assert!((c * (n + 1)).is_multiple_of(n + 1 - k + 1)); c = c * (n + 1) / (n + 1 - k + 1); } } diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index e70cdb9..7066e7f 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -210,8 +210,8 @@ impl Move for DrMove { (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) % 4 == 0 => Cancellation::NoMove, - (M::D(n), M::D(m)) if (n + m) % 4 == 0 => 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, From d1a4bfcbdfa55125961d79f68ddd2c37b1f40cd9 Mon Sep 17 00:00:00 2001 From: bpaul Date: Sun, 2 Nov 2025 17:01:27 +1000 Subject: [PATCH 21/45] Change sym coord api to make more sense --- src/cube333/two_phase_solver/coords.rs | 41 ++++++++++++--------- src/cube333/two_phase_solver/move_tables.rs | 6 +-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 176abf5..7688fd8 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -136,16 +136,19 @@ pub trait SymCoordinate: Copy + Default + Eq { fn classes() -> usize; /// A representation of this coordinate as a usize, for use, in table lookups. - fn repr(self) -> (usize, Self::Sym); + 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 { - self.repr().0 - } + fn class(self) -> usize; + + /// Obtain the underlying symmetry of this ooordinate. + fn sym(self) -> Self::Sym; } pub struct RawSymTable @@ -224,16 +227,17 @@ impl SymCoordinate for COSymCoord { 324 } - fn repr(self) -> (usize, Self::Sym) { - ( - (self.0 >> 3) as usize, - HalfSymmetry::from_repr((self.0 & 7) as usize), - ) - } - 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)] @@ -252,16 +256,17 @@ impl SymCoordinate for EOSymCoord { 336 } - fn repr(self) -> (usize, Self::Sym) { - ( - (self.0 >> 3) as usize, - HalfSymmetry::from_repr((self.0 & 7) as usize), - ) - } - 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, Copy, Clone)] diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 7066e7f..bea4230 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -105,7 +105,7 @@ where CubieCube: FromCoordinate, { table: Box<[[S; MOVES]]>, - sym_mul_table: SymMultTable, + sym_mult_table: SymMultTable, _phantom: PhantomData, } @@ -135,7 +135,7 @@ where SymMoveTable { table, - sym_mul_table, + sym_mult_table: sym_mul_table, _phantom: PhantomData, } } @@ -143,7 +143,7 @@ where pub fn make_move(&self, coord: S, mv: M) -> S { let (idx, sym1) = coord.repr(); let (idx, sym2) = self.table[idx][mv.index()].repr(); - let sym = self.sym_mul_table.multiply(sym1, sym2); + let sym = self.sym_mult_table.multiply(sym1, sym2); S::from_repr(idx, sym) } From 387a82ae477f6bdbb10fd5d536dfdbbfd7833050 Mon Sep 17 00:00:00 2001 From: bpaul Date: Sun, 2 Nov 2025 22:35:52 +1000 Subject: [PATCH 22/45] Test half symmetry correctness --- src/cube333/two_phase_solver/symmetry.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 433e36d..086bfba 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -270,17 +270,23 @@ impl Symmetry for HalfSymmetry { mod test { use super::*; - use proptest::prelude::*; + fn check_multiplication_correct() { + let table = SymMultTable::::generate(); + for sym1 in S::get_all() { + for sym2 in S::get_all() { + let cube = CubieCube::SOLVED; + assert_eq!( + table.multiply(sym1, sym2).apply(cube.clone()), + sym1.apply(cube.clone()) + .multiply_cube(sym2.apply(cube.clone())) + ); + } + } + } #[test] fn multiplication_correct() { - let table = DrSymMultTable::generate(); - proptest!(|(sym1 in (0..16u8).prop_map(DrSymmetry), sym2 in (0..16u8).prop_map(DrSymmetry))| { - let cube = CubieCube::SOLVED; - assert_eq!( - table.multiply(sym1, sym2).apply(cube.clone()), - sym1.apply(cube.clone()).multiply_cube(sym2.apply(cube.clone())) - ); - }) + check_multiplication_correct::(); + check_multiplication_correct::(); } } From 82f22b01c28f00b7396fe6a3ad57f56e1b98d862 Mon Sep 17 00:00:00 2001 From: bpaul Date: Thu, 27 Nov 2025 19:12:18 +1000 Subject: [PATCH 23/45] Test raw_to_sym is a right inverse --- src/cube333/two_phase_solver/coords.rs | 32 ++++++++++++++++++++- src/cube333/two_phase_solver/move_tables.rs | 3 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 7688fd8..9a01753 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -185,7 +185,12 @@ where 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); + // We do not want to overwrite already initialised entries in the raw_to_sym table, + // or the default entry. This is to keep duplicate symmetries (which are + // "equivalent") into a canonical form based on being first in the symmetry list. + if raw2.repr() != 0 && raw_to_sym[raw2.repr()] == S::default() { + raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); + } } class_to_repr[sym_idx] = raw; @@ -209,6 +214,14 @@ where pub fn puzzle_to_sym(&self, p: &CubieCube) -> S { self.raw_to_sym(S::Raw::from_puzzle(p)) } + + 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)] @@ -363,4 +376,21 @@ mod test { 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()); }) } + + 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::(); + } } diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index bea4230..1ee2f3d 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -332,12 +332,13 @@ mod test { 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!(l, r); + assert_eq!(sym_table.sym_to_raw(l), sym_table.sym_to_raw(r)); } #[test] From 8a8d10e0aae0dee5b29d8f887eb267161ef4c12d Mon Sep 17 00:00:00 2001 From: bpaul Date: Thu, 27 Nov 2025 22:05:37 +1000 Subject: [PATCH 24/45] Make a move conjugate table (fixes the sym move table) --- src/cube333/moves.rs | 1 - src/cube333/two_phase_solver/coords.rs | 7 +- src/cube333/two_phase_solver/move_tables.rs | 10 ++- src/cube333/two_phase_solver/symmetry.rs | 82 ++++++++++++++++++--- 4 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index e566741..7ec1605 100644 --- a/src/cube333/moves.rs +++ b/src/cube333/moves.rs @@ -231,7 +231,6 @@ impl CubieCube { let mut result = CubieCube::SOLVED; for i in 0..8 { - // this is kinda confusing but like try it on a cube let oa = self.co[other.cp[i] as usize]; let ob = other.co[i]; let o = oa.twist_by(ob); diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 9a01753..f2efe61 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -185,12 +185,7 @@ where for sym in S::Sym::get_all() { let d = c.clone().conjugate_symmetry(sym); let raw2 = S::Raw::from_puzzle(&d); - // We do not want to overwrite already initialised entries in the raw_to_sym table, - // or the default entry. This is to keep duplicate symmetries (which are - // "equivalent") into a canonical form based on being first in the symmetry list. - if raw2.repr() != 0 && raw_to_sym[raw2.repr()] == S::default() { - raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); - } + raw_to_sym[raw2.repr()] = S::from_repr(sym_idx, sym); } class_to_repr[sym_idx] = raw; diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 1ee2f3d..719393b 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -10,7 +10,7 @@ use super::coords::{ COSymCoord, DominoEPCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, SymCoordinate, }; -use super::symmetry::SymMultTable; +use super::symmetry::{SymMultTable, SymMoveConjTable}; use std::marker::PhantomData; @@ -106,6 +106,7 @@ where { table: Box<[[S; MOVES]]>, sym_mult_table: SymMultTable, + sym_move_conj_table: SymMoveConjTable, _phantom: PhantomData, } @@ -131,17 +132,20 @@ where }) .collect::>() .into_boxed_slice(); - let sym_mul_table = SymMultTable::generate(); + let sym_mult_table = SymMultTable::generate(); + let sym_move_conj_table = SymMoveConjTable::generate(); SymMoveTable { table, - sym_mult_table: sym_mul_table, + sym_mult_table, + sym_move_conj_table, _phantom: PhantomData, } } 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); diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 086bfba..9abd96e 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -1,7 +1,10 @@ //! 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; @@ -21,8 +24,7 @@ pub trait Symmetry: Copy + Default + Eq { /// Conjugate the given puzzle by this symmetry, written S P S^-1 fn conjugate(&self, cube: CubieCube) -> CubieCube { - self.apply(CubieCube::SOLVED) - .multiply_cube(self.apply_inverse(cube)) + self.apply_inverse(self.apply(CubieCube::SOLVED).multiply_cube(cube)) } } @@ -70,6 +72,44 @@ impl SymMultTable { } } +/// A table of conjugates of moves by symmetries +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(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 } + } + + 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) @@ -270,18 +310,36 @@ impl Symmetry for HalfSymmetry { mod test { use super::*; - fn check_multiplication_correct() { + use crate::cube333::moves::Move333; + use crate::moves::MoveSequence; + + use proptest::collection::vec; + use proptest::prelude::*; + + fn check_multiplication_correct() { let table = SymMultTable::::generate(); - for sym1 in S::get_all() { - for sym2 in S::get_all() { - let cube = CubieCube::SOLVED; - assert_eq!( - table.multiply(sym1, sym2).apply(cube.clone()), - sym1.apply(cube.clone()) - .multiply_cube(sym2.apply(cube.clone())) - ); + 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] From e7581484700a9ac60c00480fd8db4475b1daa98a Mon Sep 17 00:00:00 2001 From: bpaul Date: Tue, 2 Dec 2025 01:51:33 +1000 Subject: [PATCH 25/45] Document symmetry move table stuff --- src/cube333/two_phase_solver/move_tables.rs | 20 +++++++++++++++++--- src/cube333/two_phase_solver/symmetry.rs | 5 ++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 719393b..b08310a 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -10,7 +10,7 @@ use super::coords::{ COSymCoord, DominoEPCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, SymCoordinate, }; -use super::symmetry::{SymMultTable, SymMoveConjTable}; +use super::symmetry::{SymMoveConjTable, SymMultTable}; use std::marker::PhantomData; @@ -57,8 +57,8 @@ pub struct MoveTable, const MOVES: usize> { } 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 be identical anyways. + /// 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]; @@ -100,6 +100,16 @@ impl, const MOVES: usize> MoveTable where CubieCube: FromCoordinate, @@ -115,6 +125,8 @@ impl 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| { @@ -143,6 +155,7 @@ where } } + /// 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); @@ -152,6 +165,7 @@ where S::from_repr(idx, sym) } + /// Determine what coordinate comes from applying a sequence of moves. fn make_moves(&self, coord: S, alg: MoveSequence) -> S { alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 9abd96e..0c413a7 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -72,7 +72,9 @@ impl SymMultTable { } } -/// A table of conjugates of moves by symmetries +/// 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, @@ -105,6 +107,7 @@ impl 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()] } From eeeb2ae4dbffb402f1170cef95031c62fb92ab9c Mon Sep 17 00:00:00 2001 From: bpaul Date: Thu, 4 Dec 2025 17:30:06 +1000 Subject: [PATCH 26/45] Realise we need conjugate tables --- src/cube333/two_phase_solver/mod.rs | 1 + src/cube333/two_phase_solver/prune.rs | 38 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/cube333/two_phase_solver/prune.rs diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index fea93f5..3eca12c 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -2,4 +2,5 @@ mod coords; mod move_tables; +mod prune; mod symmetry; diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs new file mode 100644 index 0000000..14c9dd9 --- /dev/null +++ b/src/cube333/two_phase_solver/prune.rs @@ -0,0 +1,38 @@ +//! Pruning tables for the two phase solver. +//! +//! Choices of pruning tables are from cs0x7f's min2phase. + +use crate::coord::Coordinate; +use crate::cube333::CubieCube; +use super::coords::SymCoordinate; + +use std::marker::PhantomData; + +// TODO future stuff: +// look into alternative pruning table choices +// look into alternative information to store in pruning tables +// look into alternative compression schemes + +/// 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, const COUNT: usize> { + table: [u8; COUNT], + _phantom1: PhantomData, + _phantom2: PhantomData, +} + +impl, const COUNT: usize> SymRawPruningTable { + pub fn generate() -> Self { + todo!() + } + + pub fn query(&self, s: S, r: R) -> u8 { + // WE NEED CONJUGATE TABLES!!!!!!!!!!!! + todo!() + } +} From fb5d820bfc3920f35d0762d8e18b26b7a39e37a6 Mon Sep 17 00:00:00 2001 From: bpaul Date: Mon, 15 Dec 2025 12:32:34 +1000 Subject: [PATCH 27/45] Conjugate tables --- src/cube333/two_phase_solver/prune.rs | 54 +++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index 14c9dd9..eaa916a 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -2,9 +2,10 @@ //! //! Choices of pruning tables are from cs0x7f's min2phase. -use crate::coord::Coordinate; -use crate::cube333::CubieCube; use super::coords::SymCoordinate; +use super::symmetry::Symmetry; +use crate::coord::{Coordinate, FromCoordinate}; +use crate::cube333::CubieCube; use std::marker::PhantomData; @@ -13,6 +14,50 @@ use std::marker::PhantomData; // look into alternative information to store in pruning tables // look into alternative compression schemes +/// A table storing results of conjugating raw coordinates by symmetries. +pub struct SymConjTable< + S: Symmetry, + R: Coordinate, + const SYMS: usize, +> { + 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(); + + 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; + } + } + + Self { + table, + _phantom1: PhantomData, + _phantom2: PhantomData, + } + } + + /// Conjugate the given raw coordinate by the given symmetry + pub fn conjugate(&self, r: R, s: S) -> R { + self.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 @@ -26,13 +71,14 @@ pub struct SymRawPruningTable, const _phantom2: PhantomData, } -impl, const COUNT: usize> SymRawPruningTable { +impl, const COUNT: usize> + SymRawPruningTable +{ pub fn generate() -> Self { todo!() } pub fn query(&self, s: S, r: R) -> u8 { - // WE NEED CONJUGATE TABLES!!!!!!!!!!!! todo!() } } From 43a2fd10e6f9bd72ec90aba66fb094275312abb0 Mon Sep 17 00:00:00 2001 From: bpaul Date: Mon, 15 Dec 2025 12:49:57 +1000 Subject: [PATCH 28/45] Conj table tests (need e slice fromcoord) --- src/cube333/two_phase_solver/prune.rs | 61 ++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index eaa916a..7034cd5 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -2,10 +2,10 @@ //! //! Choices of pruning tables are from cs0x7f's min2phase. -use super::coords::SymCoordinate; -use super::symmetry::Symmetry; +use super::coords::{ESliceEdgeCoord, SymCoordinate}; +use super::symmetry::{HalfSymmetry, Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; -use crate::cube333::CubieCube; +use crate::cube333::{CubieCube, coordcube::EOCoord}; use std::marker::PhantomData; @@ -15,18 +15,13 @@ use std::marker::PhantomData; // look into alternative compression schemes /// A table storing results of conjugating raw coordinates by symmetries. -pub struct SymConjTable< - S: Symmetry, - R: Coordinate, - const SYMS: usize, -> { +pub struct SymConjTable, const SYMS: usize> { table: Box<[[R; SYMS]]>, _phantom1: PhantomData, _phantom2: PhantomData, } -impl, const SYMS: usize> - SymConjTable +impl, const SYMS: usize> SymConjTable where CubieCube: FromCoordinate, { @@ -34,7 +29,7 @@ where pub fn generate() -> Self { let mut 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); @@ -58,6 +53,9 @@ where } } +type SliceConjTable = SymConjTable; +type EoConjTable = SymConjTable; + /// 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 @@ -82,3 +80,44 @@ impl, const COUNT: usize> todo!() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::cube333::moves::Move333; + use crate::moves::MoveSequence; + + use proptest::collection::vec; + use proptest::prelude::*; + + #[test] + fn conj_generates() { + //SliceConjTable::generate(); + EoConjTable::generate(); + } + + 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_symmetry(s)); + assert_eq!(a, b); + } + } + + #[test] + fn conj_commutes() { + let eo_conj_table = EoConjTable::generate(); + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + diagram_commutes(&eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone())); + }); + } +} From f9ae93bad400b37c8da04d1af63b9e8ccab5a6ef Mon Sep 17 00:00:00 2001 From: bpaul Date: Mon, 15 Dec 2025 13:16:27 +1000 Subject: [PATCH 29/45] E slice from coord --- src/cube333/two_phase_solver/coords.rs | 43 ++++++++++++++++++++++++++ src/cube333/two_phase_solver/prune.rs | 4 ++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index f2efe61..c8b3355 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -87,6 +87,39 @@ impl Coordinate for ESliceEdgeCoord { } } +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()))) @@ -335,6 +368,16 @@ mod test { use proptest::collection::vec; use proptest::prelude::*; + #[test] + fn from_coord() { + proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { + let coord = ESliceEdgeCoord::from_puzzle(&CubieCube::SOLVED.make_moves(mvs)); + let mut d = CubieCube::SOLVED; + d.set_coord(coord); + assert_eq!(ESliceEdgeCoord::from_puzzle(&d), coord); + }); + } + #[test] fn e_slice_edge_uniqueness() { let mut coords = HashSet::new(); diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index 7034cd5..7fa9d68 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -92,7 +92,7 @@ mod test { #[test] fn conj_generates() { - //SliceConjTable::generate(); + SliceConjTable::generate(); EoConjTable::generate(); } @@ -115,8 +115,10 @@ mod test { #[test] fn conj_commutes() { + let slice_conj_table = SliceConjTable::generate(); let eo_conj_table = EoConjTable::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())); }); } From 7528e71b0f2223ecde307f22041a889037ba3796 Mon Sep 17 00:00:00 2001 From: bpaul Date: Mon, 15 Dec 2025 18:27:57 +1000 Subject: [PATCH 30/45] dr e slice from coord --- src/cube333/two_phase_solver/coords.rs | 34 +++++++++++++++++++++++++- src/cube333/two_phase_solver/prune.rs | 8 +++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index c8b3355..114e262 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -88,7 +88,7 @@ impl Coordinate for ESliceEdgeCoord { } fn binom(n: usize, k: usize) -> usize { - (n-k+1..=n).product::() / (1..=k).product::() + (n - k + 1..=n).product::() / (1..=k).product::() } impl FromCoordinate for CubieCube { @@ -156,6 +156,27 @@ impl Coordinate for DominoESliceCoord { } } +impl FromCoordinate for CubieCube { + fn set_coord(&mut self, coord: DominoESliceCoord) { + self.ep = CubieCube::SOLVED.ep; + let mut c = coord.repr(); + + let mut n = [0; 4]; + for (i, n) in n.iter_mut().enumerate() { + *n = c % (i + 1); + c /= i + 1; + } + + use crate::cube333::edge::Edge as E; + let mut bag = vec![E::BR, E::BL, E::FL, E::FR]; + for i in (8..=11).rev() { + let index = n[i - 8]; + self.ep[i] = bag[index]; + bag.remove(index); + } + } +} + /// 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 { @@ -358,6 +379,7 @@ mod test { use itertools::Itertools; + use super::super::move_tables::{DrMove, SubMove}; use super::*; use crate::{ coord::Coordinate, @@ -378,6 +400,16 @@ mod test { }); } + #[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))| { + let coord = DominoESliceCoord::from_puzzle(&CubieCube::SOLVED.make_moves(mvs)); + let mut d = CubieCube::SOLVED; + d.set_coord(coord); + assert_eq!(DominoESliceCoord::from_puzzle(&d), coord); + }); + } + #[test] fn e_slice_edge_uniqueness() { let mut coords = HashSet::new(); diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index 7fa9d68..eb726fe 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -2,7 +2,7 @@ //! //! Choices of pruning tables are from cs0x7f's min2phase. -use super::coords::{ESliceEdgeCoord, SymCoordinate}; +use super::coords::{DominoESliceCoord, ESliceEdgeCoord, SymCoordinate}; use super::symmetry::{HalfSymmetry, Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{CubieCube, coordcube::EOCoord}; @@ -55,6 +55,7 @@ where type SliceConjTable = SymConjTable; type EoConjTable = SymConjTable; +type DominoESliceConjTable = SymConjTable; /// A pruning table indexed by the class of a symmetry coordinate and a raw coordinate. /// @@ -84,6 +85,7 @@ impl, const COUNT: usize> #[cfg(test)] mod test { use super::*; + use super::super::move_tables::{DrMove, SubMove}; use crate::cube333::moves::Move333; use crate::moves::MoveSequence; @@ -117,9 +119,13 @@ mod 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())); + }); } } From 9fd860c4ef0929df472f600c00fd255295df0f35 Mon Sep 17 00:00:00 2001 From: bpaul Date: Wed, 17 Dec 2025 12:09:39 +1000 Subject: [PATCH 31/45] Correctly take inverse conjugates in conjugate tables --- src/cube333/two_phase_solver/prune.rs | 15 +++++---------- src/cube333/two_phase_solver/symmetry.rs | 11 +++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index eb726fe..5f6f03f 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -14,7 +14,8 @@ use std::marker::PhantomData; // look into alternative information to store in pruning tables // look into alternative compression schemes -/// A table storing results of conjugating raw coordinates by symmetries. +/// A table storing results of conjugating raw coordinates by inverses of symmetries (i.e. we +/// compute S^-1 R S given R and S). pub struct SymConjTable, const SYMS: usize> { table: Box<[[R; SYMS]]>, _phantom1: PhantomData, @@ -34,7 +35,7 @@ where let mut c = CubieCube::SOLVED; c.set_coord(r1); for s in S::get_all() { - let d = c.clone().conjugate_symmetry(s); + let d = c.clone().conjugate_inverse_symmetry(s); let r2 = R::from_puzzle(&d); table[r1.repr()][s.repr()] = r2; } @@ -47,7 +48,7 @@ where } } - /// Conjugate the given raw coordinate by the given symmetry + /// Conjugate the given raw coordinate by the given symmetry's inverse (S^-1 R S). pub fn conjugate(&self, r: R, s: S) -> R { self.table[r.repr()][s.repr()] } @@ -92,12 +93,6 @@ mod test { use proptest::collection::vec; use proptest::prelude::*; - #[test] - fn conj_generates() { - SliceConjTable::generate(); - EoConjTable::generate(); - } - fn diagram_commutes< S: Symmetry, R: Coordinate + std::fmt::Debug, @@ -110,7 +105,7 @@ mod test { { for s in S::get_all() { let a = table.conjugate(R::from_puzzle(&cube), s); - let b = R::from_puzzle(&cube.clone().conjugate_symmetry(s)); + let b = R::from_puzzle(&cube.clone().conjugate_inverse_symmetry(s)); assert_eq!(a, b); } } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 0c413a7..906de43 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -26,6 +26,11 @@ pub trait Symmetry: Copy + Default + Eq { 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 { @@ -38,6 +43,12 @@ impl CubieCube { 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. From a88e6e0bd2372509216c9fda968af55706839dc3 Mon Sep 17 00:00:00 2001 From: bpaul Date: Wed, 17 Dec 2025 15:08:47 +1000 Subject: [PATCH 32/45] Incomplete pruning table generation --- src/cube333/two_phase_solver/coords.rs | 2 +- src/cube333/two_phase_solver/prune.rs | 119 ++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 114e262..68c8326 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -22,7 +22,7 @@ fn to_p_coord( }) } -/// Coordinate for positions of E slice edges (ignoring what the edges actually arge) +/// 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); diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index 5f6f03f..ce01234 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -2,10 +2,13 @@ //! //! Choices of pruning tables are from cs0x7f's min2phase. -use super::coords::{DominoESliceCoord, ESliceEdgeCoord, SymCoordinate}; +use super::coords::{ + COSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, SymCoordinate, +}; +use super::move_tables::{MoveTable, SubMove, SymMoveTable}; use super::symmetry::{HalfSymmetry, Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; -use crate::cube333::{CubieCube, coordcube::EOCoord}; +use crate::cube333::{CubieCube, coordcube::EOCoord, moves::Move333}; use std::marker::PhantomData; @@ -65,28 +68,112 @@ type DominoESliceConjTable = SymConjTable; /// 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, const COUNT: usize> { - table: [u8; COUNT], +pub struct SymRawPruningTable< + S: SymCoordinate, + R: Coordinate, + M: SubMove, + const SYMS: usize, + const MOVES: usize, +> { + table: Box<[u8]>, + conj_table: SymConjTable, _phantom1: PhantomData, _phantom2: PhantomData, + _phantom3: PhantomData, } -impl, const COUNT: usize> - SymRawPruningTable +impl, M: SubMove, const SYMS: usize, const MOVES: usize> + SymRawPruningTable +where + CubieCube: FromCoordinate, + CubieCube: FromCoordinate, { - pub fn generate() -> Self { - todo!() + pub fn generate( + sym_table: &RawSymTable, + sym_move_table: &SymMoveTable, + raw_move_table: &MoveTable, + ) -> 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, + _phantom1: PhantomData, + _phantom2: PhantomData, + _phantom3: PhantomData, + }; + + let s = sym_table.puzzle_to_sym(&CubieCube::SOLVED); + let r = R::from_puzzle(&CubieCube::SOLVED); + table.set(s, r, 0, sym_table); + 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 = sym_move_table.make_move(s, m); + let r2 = raw_move_table.make_move(r, m); + if table.query(s2, r2) == 3 { + next.push((s2, r2)); + table.set(s2, r2, depth % 3, sym_table); + } + } + } + + stack = next; + next = vec![]; + depth += 1; + } + + table + } + + fn index(&self, s: S, r: R) -> (usize, usize) { + let r2 = self.conj_table.conjugate(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, sym_table: &RawSymTable) { + 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. + + /* i'll do it later + for sym in S::Sym::get_all() { + todo!() + } + */ + + let (index, shift) = self.index(s, r); + + self.table[index] &= !(3 << shift); + self.table[index] |= val << shift; } pub fn query(&self, s: S, r: R) -> u8 { - todo!() + let (index, shift) = self.index(s, r); + + (self.table[index] >> shift) & 3 } } +type ESliceTwistPruneTable = SymRawPruningTable; +type ESliceFlipPruneTable = SymRawPruningTable; + #[cfg(test)] mod test { - use super::*; use super::super::move_tables::{DrMove, SubMove}; + use super::*; use crate::cube333::moves::Move333; use crate::moves::MoveSequence; @@ -123,4 +210,16 @@ mod test { diagram_commutes(&domino_eo_conj_table, CubieCube::SOLVED.make_moves(mvs.clone())); }); } + + #[test] + fn prune_generates() { + let co_sym_table = RawSymTable::generate(); + let co_sym_move_table = SymMoveTable::generate(&co_sym_table); + let eo_sym_table = RawSymTable::generate(); + let eo_sym_move_table = SymMoveTable::generate(&eo_sym_table); + let e_slice_move_table = MoveTable::generate(); + + ESliceTwistPruneTable::generate(&co_sym_table, &co_sym_move_table, &e_slice_move_table); + ESliceFlipPruneTable::generate(&eo_sym_table, &eo_sym_move_table, &e_slice_move_table); + } } From 82e38efbbcecf96c648c716fb8c30dac64001109 Mon Sep 17 00:00:00 2001 From: bpaul Date: Wed, 17 Dec 2025 17:45:09 +1000 Subject: [PATCH 33/45] Admissable pruning table tests --- src/cube333/two_phase_solver/move_tables.rs | 4 +- src/cube333/two_phase_solver/prune.rs | 137 ++++++++++++++++---- 2 files changed, 111 insertions(+), 30 deletions(-) diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index b08310a..d3f0a89 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -95,7 +95,7 @@ impl, const MOVES: usize> MoveTable) -> C { + pub fn make_moves(&self, coord: C, alg: MoveSequence) -> C { alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) } } @@ -166,7 +166,7 @@ where } /// Determine what coordinate comes from applying a sequence of moves. - fn make_moves(&self, coord: S, alg: MoveSequence) -> S { + pub fn make_moves(&self, coord: S, alg: MoveSequence) -> S { alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) } } diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index ce01234..afc49c0 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -69,43 +69,53 @@ type DominoESliceConjTable = SymConjTable; /// 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< + 'a, S: SymCoordinate, R: Coordinate, M: SubMove, const SYMS: usize, const MOVES: usize, -> { +> where + CubieCube: FromCoordinate, + CubieCube: FromCoordinate, +{ table: Box<[u8]>, conj_table: SymConjTable, - _phantom1: PhantomData, - _phantom2: PhantomData, - _phantom3: PhantomData, + sym_table: &'a RawSymTable, + sym_move_table: &'a SymMoveTable, + raw_move_table: &'a MoveTable, } -impl, M: SubMove, const SYMS: usize, const MOVES: usize> - SymRawPruningTable +impl< + 'a, + S: SymCoordinate, + R: Coordinate, + M: SubMove, + const SYMS: usize, + const MOVES: usize, +> SymRawPruningTable<'a, S, R, M, SYMS, MOVES> where CubieCube: FromCoordinate, CubieCube: FromCoordinate, { pub fn generate( - sym_table: &RawSymTable, - sym_move_table: &SymMoveTable, - raw_move_table: &MoveTable, + sym_table: &'a RawSymTable, + sym_move_table: &'a SymMoveTable, + raw_move_table: &'a MoveTable, ) -> 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, - _phantom1: PhantomData, - _phantom2: PhantomData, - _phantom3: PhantomData, + sym_table, + sym_move_table, + raw_move_table, }; - let s = sym_table.puzzle_to_sym(&CubieCube::SOLVED); + let s = table.sym_table.puzzle_to_sym(&CubieCube::SOLVED); let r = R::from_puzzle(&CubieCube::SOLVED); - table.set(s, r, 0, sym_table); + table.set(s, r, 0); let mut stack = vec![(s, r)]; let mut next = vec![]; let mut depth = 1; @@ -113,11 +123,11 @@ where while !stack.is_empty() { while let Some((s, r)) = stack.pop() { for &m in M::MOVE_LIST { - let s2 = sym_move_table.make_move(s, m); - let r2 = raw_move_table.make_move(r, m); + 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, sym_table); + table.set(s2, r2, depth % 3); } } } @@ -137,7 +147,7 @@ where } /// Set the depth in the search tree of this coordinate pair modulo 3. - fn set(&mut self, s: S, r: R, val: u8, sym_table: &RawSymTable) { + fn set(&mut self, s: S, r: R, val: u8) { assert!(val & !3 == 0); // Some S::Raw coordinates can be represented in multiple ways by S (there can be multiple @@ -148,16 +158,22 @@ where // the symmetry) S^-1 r S, and so we must iterate over all symmetries and find the // duplicates we need to update. - /* i'll do it later + // this is extremely inefficient! It would be very beneficial to create some sort of + // temporary table which stored masks of equivalent symmetries (from min2phase)! + let class_repr = S::from_repr(s.class(), S::Sym::from_repr(0)); + let mut base_cube = CubieCube::SOLVED; + base_cube.set_coord(self.sym_table.sym_to_raw(class_repr)); + let mut s_cube = CubieCube::SOLVED; + s_cube.set_coord(self.sym_table.sym_to_raw(s)); for sym in S::Sym::get_all() { - todo!() - } - */ - - let (index, shift) = self.index(s, r); + if base_cube.clone().conjugate_inverse_symmetry(sym) == s_cube { + 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; + self.table[index] &= !(3 << shift); + self.table[index] |= val << shift; + } + } } pub fn query(&self, s: S, r: R) -> u8 { @@ -165,10 +181,37 @@ where (self.table[index] >> shift) & 3 } + + 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 + } } -type ESliceTwistPruneTable = SymRawPruningTable; -type ESliceFlipPruneTable = SymRawPruningTable; +type ESliceTwistPruneTable<'a> = + SymRawPruningTable<'a, COSymCoord, ESliceEdgeCoord, Move333, 8, 18>; +type ESliceFlipPruneTable<'a> = SymRawPruningTable<'a, EOSymCoord, ESliceEdgeCoord, Move333, 8, 18>; #[cfg(test)] mod test { @@ -222,4 +265,42 @@ mod test { ESliceTwistPruneTable::generate(&co_sym_table, &co_sym_move_table, &e_slice_move_table); ESliceFlipPruneTable::generate(&eo_sym_table, &eo_sym_move_table, &e_slice_move_table); } + + fn admissable< + 'a, + S: SymCoordinate, + R: Coordinate, + M: SubMove, + const SYMS: usize, + const MOVES: usize, + >( + prune_table: &SymRawPruningTable<'a, S, R, M, SYMS, MOVES>, + 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()); + assert!(prune_table.bound(s, r) <= mvs.len()); + } + + #[test] + fn check_admissable() { + let co_sym_table = RawSymTable::generate(); + let co_sym_move_table = SymMoveTable::generate(&co_sym_table); + let eo_sym_table = RawSymTable::generate(); + let eo_sym_move_table = SymMoveTable::generate(&eo_sym_table); + let e_slice_move_table = MoveTable::generate(); + let c_prune = + ESliceTwistPruneTable::generate(&co_sym_table, &co_sym_move_table, &e_slice_move_table); + 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(&c_prune, mvs.clone()); + admissable(&e_prune, mvs.clone()); + }); + } } From ec237340c4f760f88f844a1f34512b2b33f0e4a1 Mon Sep 17 00:00:00 2001 From: bpaul Date: Wed, 17 Dec 2025 18:48:27 +1000 Subject: [PATCH 34/45] Improve prune table gen speed --- src/cube333/two_phase_solver/prune.rs | 33 ++++++++------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index afc49c0..eb58c8a 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -113,9 +113,11 @@ where 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); + table.set(s, r, 0, &conj_table_s); let mut stack = vec![(s, r)]; let mut next = vec![]; let mut depth = 1; @@ -127,7 +129,7 @@ where 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); + table.set(s2, r2, depth % 3, &conj_table_s); } } } @@ -147,7 +149,7 @@ where } /// Set the depth in the search tree of this coordinate pair modulo 3. - fn set(&mut self, s: S, r: R, val: u8) { + 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 @@ -158,15 +160,12 @@ where // the symmetry) S^-1 r S, and so we must iterate over all symmetries and find the // duplicates we need to update. - // this is extremely inefficient! It would be very beneficial to create some sort of - // temporary table which stored masks of equivalent symmetries (from min2phase)! - let class_repr = S::from_repr(s.class(), S::Sym::from_repr(0)); - let mut base_cube = CubieCube::SOLVED; - base_cube.set_coord(self.sym_table.sym_to_raw(class_repr)); - let mut s_cube = CubieCube::SOLVED; - s_cube.set_coord(self.sym_table.sym_to_raw(s)); + let repr_raw = self.sym_table.index_to_repr(s.class()); + // this is technically S r S^-1 and not S^-1 r S, but it should be fine i think! Symmetries + // that agree on this inverse should agree on the normal case + let sinv_raw = conj_table_s.conjugate(repr_raw, s.sym()); for sym in S::Sym::get_all() { - if base_cube.clone().conjugate_inverse_symmetry(sym) == s_cube { + 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); @@ -254,18 +253,6 @@ mod test { }); } - #[test] - fn prune_generates() { - let co_sym_table = RawSymTable::generate(); - let co_sym_move_table = SymMoveTable::generate(&co_sym_table); - let eo_sym_table = RawSymTable::generate(); - let eo_sym_move_table = SymMoveTable::generate(&eo_sym_table); - let e_slice_move_table = MoveTable::generate(); - - ESliceTwistPruneTable::generate(&co_sym_table, &co_sym_move_table, &e_slice_move_table); - ESliceFlipPruneTable::generate(&eo_sym_table, &eo_sym_move_table, &e_slice_move_table); - } - fn admissable< 'a, S: SymCoordinate, From b2abdac00e694ed326b9c5b1b98b144654f025df Mon Sep 17 00:00:00 2001 From: bpaul Date: Thu, 18 Dec 2025 21:01:03 +1000 Subject: [PATCH 35/45] prune table docs --- src/cube333/two_phase_solver/prune.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index eb58c8a..aab6750 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -98,6 +98,7 @@ where CubieCube: FromCoordinate, CubieCube: FromCoordinate, { + /// Generate the table pub fn generate( sym_table: &'a RawSymTable, sym_move_table: &'a SymMoveTable, @@ -142,6 +143,7 @@ where 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(r, s.sym()); let i = r2.repr() * S::classes() + s.class(); @@ -175,12 +177,14 @@ where } } + /// Determine the bound of a coordinate pair modulo 3 with a lookup (fast) pub fn query(&self, s: S, r: R) -> u8 { let (index, shift) = self.index(s, r); (self.table[index] >> shift) & 3 } + /// 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 = ( From bdf8fdbabc0257e2b85d051935ef15d118b1229d Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 19 Dec 2025 23:55:44 +1000 Subject: [PATCH 36/45] Support dr permutation coordinates (it all works!) --- src/cube333/coordcube.rs | 40 ++++++ src/cube333/two_phase_solver/coords.rs | 142 ++++++++++++++++---- src/cube333/two_phase_solver/move_tables.rs | 34 +++-- src/cube333/two_phase_solver/prune.rs | 51 +++++-- src/cube333/two_phase_solver/symmetry.rs | 10 +- 5 files changed, 223 insertions(+), 54 deletions(-) diff --git a/src/cube333/coordcube.rs b/src/cube333/coordcube.rs index 4638911..b45e53a 100644 --- a/src/cube333/coordcube.rs +++ b/src/cube333/coordcube.rs @@ -17,6 +17,26 @@ pub struct EOCoord(u16); #[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()))) @@ -79,6 +99,16 @@ impl Coordinate for CPCoord { } } +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()))) @@ -138,6 +168,16 @@ impl Coordinate for EPCoord { } } +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. #[derive(Debug, PartialEq, Eq)] diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index 68c8326..f16694f 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -22,6 +22,24 @@ fn to_p_coord( }) } +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); @@ -138,6 +156,18 @@ impl Coordinate for DominoEPCoord { } } +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()))) @@ -158,22 +188,12 @@ impl Coordinate for DominoESliceCoord { impl FromCoordinate for CubieCube { fn set_coord(&mut self, coord: DominoESliceCoord) { - self.ep = CubieCube::SOLVED.ep; - let mut c = coord.repr(); - - let mut n = [0; 4]; - for (i, n) in n.iter_mut().enumerate() { - *n = c % (i + 1); - c /= i + 1; - } + //self.ep = CubieCube::SOLVED.ep; use crate::cube333::edge::Edge as E; - let mut bag = vec![E::BR, E::BL, E::FL, E::FR]; - for i in (8..=11).rev() { - let index = n[i - 8]; - self.ep[i] = bag[index]; - bag.remove(index); - } + let bag = vec![E::BR, E::BL, E::FL, E::FR]; + + set_p_coord::<12, 8, 11, E>(coord.repr(), &mut self.ep, bag); } } @@ -331,6 +351,64 @@ impl SymCoordinate for EOSymCoord { } } +#[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 count() -> usize { + Self::classes() * 16 + } + + 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 count() -> usize { + Self::classes() * 16 + } + + 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) + } +} + #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub struct Phase1Cube { co: COCoord, @@ -390,23 +468,28 @@ mod test { 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))| { - let coord = ESliceEdgeCoord::from_puzzle(&CubieCube::SOLVED.make_moves(mvs)); - let mut d = CubieCube::SOLVED; - d.set_coord(coord); - assert_eq!(ESliceEdgeCoord::from_puzzle(&d), coord); + 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))| { - let coord = DominoESliceCoord::from_puzzle(&CubieCube::SOLVED.make_moves(mvs)); - let mut d = CubieCube::SOLVED; - d.set_coord(coord); - assert_eq!(DominoESliceCoord::from_puzzle(&d), coord); + sets_coord::(mvs.clone()); + sets_coord::(mvs); }); } @@ -432,6 +515,8 @@ mod test { fn sym_table_generates() { RawSymTable::::generate(); RawSymTable::::generate(); + RawSymTable::::generate(); + RawSymTable::::generate(); } #[test] @@ -444,7 +529,16 @@ mod test { 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() @@ -462,5 +556,7 @@ mod 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/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index d3f0a89..c6be713 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -2,13 +2,13 @@ use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::CubieCube; -use crate::cube333::coordcube::{COCoord, CPCoord, EOCoord}; +use crate::cube333::coordcube::{COCoord, EOCoord}; use crate::cube333::moves::{Move333, Move333Type}; use crate::moves::{Cancellation, Move, MoveSequence}; use super::coords::{ - COSymCoord, DominoEPCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, - SymCoordinate, + COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, + RawSymTable, SymCoordinate, }; use super::symmetry::{SymMoveConjTable, SymMultTable}; @@ -282,13 +282,11 @@ impl SubMove for DrMove { } } -type COMoveTable = MoveTable; -type EOMoveTable = MoveTable; type COSymMoveTable = SymMoveTable; type EOSymMoveTable = SymMoveTable; type ESliceEdgeMoveTable = MoveTable; -type DominoCPMoveTable = MoveTable; -type DominoEPMoveTable = MoveTable; +type DominoCPSymMoveTable = SymMoveTable; +type DominoEPSymMoveTable = SymMoveTable; type DominoESliceMoveTable = MoveTable; #[cfg(test)] @@ -300,12 +298,12 @@ mod test { #[test] fn generates() { - COMoveTable::generate(); - EOMoveTable::generate(); ESliceEdgeMoveTable::generate(); - DominoCPMoveTable::generate(); - DominoEPMoveTable::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 @@ -361,8 +359,6 @@ mod test { #[test] fn commutes_normal() { - let co_table = COMoveTable::generate(); - let eo_table = EOMoveTable::generate(); let eslice_table = ESliceEdgeMoveTable::generate(); let co_sym = RawSymTable::generate(); @@ -370,8 +366,6 @@ mod test { 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(&co_table, CubieCube::SOLVED, mvs.clone()); - diagram_commutes(&eo_table, CubieCube::SOLVED, mvs.clone()); 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()); @@ -380,12 +374,14 @@ mod test { #[test] fn commutes_domino() { - let cp_table = DominoCPMoveTable::generate(); - let ep_table = DominoEPMoveTable::generate(); + 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))| { - diagram_commutes(&cp_table, CubieCube::SOLVED, mvs.clone()); - diagram_commutes(&ep_table, CubieCube::SOLVED, mvs.clone()); + 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 index aab6750..b86f1f5 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -3,9 +3,10 @@ //! Choices of pruning tables are from cs0x7f's min2phase. use super::coords::{ - COSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, RawSymTable, SymCoordinate, + COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, + RawSymTable, SymCoordinate, }; -use super::move_tables::{MoveTable, SubMove, SymMoveTable}; +use super::move_tables::{DrMove, MoveTable, SubMove, SymMoveTable}; use super::symmetry::{HalfSymmetry, Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{CubieCube, coordcube::EOCoord, moves::Move333}; @@ -17,10 +18,10 @@ use std::marker::PhantomData; // look into alternative information to store in pruning tables // look into alternative compression schemes -/// A table storing results of conjugating raw coordinates by inverses of symmetries (i.e. we -/// compute S^-1 R S given R and S). +/// 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, } @@ -33,28 +34,39 @@ where 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_inverse_symmetry(s); + 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 inverse (S^-1 R S). + /// 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()] + } } type SliceConjTable = SymConjTable; @@ -145,7 +157,7 @@ where /// 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(r, s.sym()); + let r2 = self.conj_table.conjugate_inverse(r, s.sym()); let i = r2.repr() * S::classes() + s.class(); (i >> 2, (i & 3) * 2) } @@ -163,8 +175,6 @@ where // duplicates we need to update. let repr_raw = self.sym_table.index_to_repr(s.class()); - // this is technically S r S^-1 and not S^-1 r S, but it should be fine i think! Symmetries - // that agree on this inverse should agree on the normal case 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 { @@ -215,6 +225,10 @@ where type ESliceTwistPruneTable<'a> = SymRawPruningTable<'a, COSymCoord, ESliceEdgeCoord, Move333, 8, 18>; type ESliceFlipPruneTable<'a> = SymRawPruningTable<'a, EOSymCoord, ESliceEdgeCoord, Move333, 8, 18>; +type DominoSliceCPPruneTable<'a> = + SymRawPruningTable<'a, CPSymCoord, DominoESliceCoord, DrMove, 16, 10>; +type DominoSliceEPPruneTable<'a> = + SymRawPruningTable<'a, DominoEPSymCoord, DominoESliceCoord, DrMove, 16, 10>; #[cfg(test)] mod test { @@ -293,5 +307,24 @@ mod test { admissable(&c_prune, mvs.clone()); admissable(&e_prune, mvs.clone()); }); + let cp_sym_table = RawSymTable::generate(); + let cp_sym_move_table = SymMoveTable::generate(&cp_sym_table); + let ep_sym_table = RawSymTable::generate(); + let ep_sym_move_table = SymMoveTable::generate(&ep_sym_table); + let d_e_slice_move_table = MoveTable::generate(); + let d_c_prune = DominoSliceCPPruneTable::generate( + &cp_sym_table, + &cp_sym_move_table, + &d_e_slice_move_table, + ); + 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(&d_c_prune, mvs.clone()); + admissable(&d_e_prune, mvs.clone()); + }); } } diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index 906de43..f94c93b 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -103,19 +103,23 @@ impl let s = S::from_repr(s); let m = M::MOVE_LIST[m]; - let c = s.conjugate(CubieCube::SOLVED.make_move(m.into_move())); + 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()) + (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 } + SymMoveConjTable { + table, + _phantom: PhantomData, + } } /// Determine the move corresponding to S^-1 M S given M and S. From 19593258cbae8e48d0919834667b7380936a8ea7 Mon Sep 17 00:00:00 2001 From: bpaul Date: Tue, 23 Dec 2025 02:42:23 +1000 Subject: [PATCH 37/45] Make itertools a dev dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 52efcfa..9704df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -itertools = "0.14.0" thiserror = "1.0.50" [dev-dependencies] +itertools = "0.14.0" proptest = "1.7.0" proptest-derive = "0.6.0" From 67453cd00dae57fd2f35abe4b9d7b134974bf9b4 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 01:17:21 +1000 Subject: [PATCH 38/45] it solves cubes???? --- src/cube333/two_phase_solver/coords.rs | 42 --- src/cube333/two_phase_solver/mod.rs | 357 ++++++++++++++++++++ src/cube333/two_phase_solver/move_tables.rs | 12 +- src/cube333/two_phase_solver/prune.rs | 120 ++++--- 4 files changed, 431 insertions(+), 100 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index f16694f..ebd4e6c 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -409,48 +409,6 @@ impl SymCoordinate for DominoEPSymCoord { } } -#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] -pub struct Phase1Cube { - co: COCoord, - eo: EOCoord, - e_slice: ESliceEdgeCoord, -} - -impl Phase1Cube { - /// Convert a cubie cube into a phase 1 state cube. This will never fail as every cube can be - /// put into the phase 1 solver. - pub fn from_puzzle(puzzle: &CubieCube) -> Self { - Phase1Cube { - co: COCoord::from_puzzle(puzzle), - eo: EOCoord::from_puzzle(puzzle), - e_slice: ESliceEdgeCoord::from_puzzle(puzzle), - } - } -} - -pub struct Phase2Cube { - cp: CPCoord, - ep: DominoEPCoord, - e_slice: DominoESliceCoord, -} - -fn cubie_in_domino(puzzle: &CubieCube) -> bool { - let p1 = Phase1Cube::from_puzzle(puzzle); - p1.co.repr() == 0 && p1.eo.repr() == 0 && p1.e_slice.repr() == 0 -} - -impl Phase2Cube { - /// Attempt to convert a cubie cube into a Phase2Cube. This will fail if the cube is not in U/D - /// domino reduction. - pub fn from_puzzle(puzzle: &CubieCube) -> Option { - cubie_in_domino(puzzle).then_some(Phase2Cube { - cp: CPCoord::from_puzzle(puzzle), - ep: DominoEPCoord::from_puzzle(puzzle), - e_slice: DominoESliceCoord::from_puzzle(puzzle), - }) - } -} - #[cfg(test)] mod test { use std::collections::HashSet; diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index 3eca12c..53d15ec 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -4,3 +4,360 @@ 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 Mover { + p1_co: Rc, + p1_eo: Rc, + p1_slice: Rc, + p2_cp: Rc, + p2_ep: Rc, + p2_slice: Rc, +} + +impl Mover { + fn make_p1_move(&self, cube: P1Cube, m: Move333) -> P1Cube { + let co = self.p1_co.make_move(cube.co, m); + let eo = self.p1_eo.make_move(cube.eo, m); + let slice = self.p1_slice.make_move(cube.slice, m); + P1Cube { co, eo, slice } + } + + fn make_p2_move(&self, cube: P2Cube, m: DrMove) -> P2Cube { + let cp = self.p2_cp.make_move(cube.cp, m); + let ep = self.p2_ep.make_move(cube.ep, m); + let slice = self.p2_slice.make_move(cube.slice, m); + P2Cube { cp, ep, slice } + } +} + +struct Pruner { + p1_co: prune::ESliceTwistPruneTable, + p1_eo: prune::ESliceFlipPruneTable, + p2_cp: prune::DominoSliceCPPruneTable, + p2_ep: prune::DominoSliceEPPruneTable, +} + +impl Pruner { + fn init_prune_p1(&self, c: P1Cube) -> P1PruneState { + P1PruneState { + co_slice: self.p1_co.bound(c.co, c.slice), + eo_slice: self.p1_eo.bound(c.eo, c.slice), + } + } + + fn update_prune_p1(&self, p: P1PruneState, c: P1Cube) -> P1PruneState { + let co_slice = self.p1_co.update(p.co_slice, c.co, c.slice); + let eo_slice = self.p1_eo.update(p.eo_slice, c.eo, c.slice); + P1PruneState { co_slice, eo_slice } + } + + fn init_prune_p2(&self, c: P2Cube) -> P2PruneState { + P2PruneState { + cp_slice: self.p2_cp.bound(c.cp, c.slice), + ep_slice: self.p2_ep.bound(c.ep, c.slice), + } + } + + fn update_prune_p2(&self, p: P2PruneState, c: P2Cube) -> P2PruneState { + let cp_slice = self.p2_cp.update(p.cp_slice, c.cp, c.slice); + let ep_slice = self.p2_ep.update(p.ep_slice, c.ep, c.slice); + P2PruneState { cp_slice, ep_slice } + } +} + +struct SymTables { + co: Rc>, + eo: Rc>, + cp: Rc>, + ep: Rc>, +} + +impl SymTables { + fn get_p1_cube(&self, c: &CubieCube) -> P1Cube { + P1Cube { + co: self.co.puzzle_to_sym(c), + eo: self.eo.puzzle_to_sym(c), + slice: coords::ESliceEdgeCoord::from_puzzle(c), + } + } + + fn get_p2_cube(&self, c: &CubieCube) -> P2Cube { + P2Cube { + cp: self.cp.puzzle_to_sym(c), + ep: self.ep.puzzle_to_sym(c), + slice: coords::DominoESliceCoord::from_puzzle(c), + } + } +} + +#[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 P1PruneState { + fn val(self) -> usize { + self.co_slice.max(self.eo_slice) + } +} + +impl P2PruneState { + fn val(self) -> usize { + self.cp_slice.max(self.ep_slice) + } +} + +#[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 P1Cube { + fn is_solved(self) -> bool { + // TODO this sucks + self.co.class() == 0 && self.eo.class() == 0 && self.slice.repr() == 0 + } +} + +impl P2Cube { + fn is_solved(self) -> bool { + self.cp.class() == 0 && self.ep.class() == 0 && self.slice.repr() == 0 + } +} + +/// 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() + } +} + +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_p1(&cube); + let cube = cube.make_moves(MoveSequence(p1.clone())); + let mut p2 = self.solve_p2(&cube); + + let mut sol = p1; + sol.append(&mut p2); + MoveSequence(sol).cancel() + } + + fn solve_p1(&self, cube: &CubieCube) -> Vec { + let cube = self.sym_tables.get_p1_cube(cube); + let prune = self.pruner.init_prune_p1(cube); + let mut sol = Vec::new(); + + let mut depth = 0; + while depth < 20 { + depth = self.search_p1(cube, prune, &mut sol, 0, depth); + if depth == 50 { + return sol; + } + } + + panic!("No phase 1 solution found in 20 moves") + } + + fn search_p1( + &self, + cube: P1Cube, + prune: P1PruneState, + sol: &mut Vec, + cost: usize, + depth: usize, + ) -> usize { + if cube.is_solved() { + // TODO why 50... + return 50; + } + let estimate = cost + prune.val(); + if estimate > depth { + return estimate; + } + let mut min = usize::MAX; + for &m in Move333::MOVE_LIST { + // TODO eliminate redundant moves + let cube2 = self.mover.make_p1_move(cube, m); + let prune2 = self.pruner.update_prune_p1(prune, cube2); + sol.push(m); + + let depth = self.search_p1(cube2, prune2, sol, cost + 1, depth); + if depth == 50 { + return 50; + } + min = min.min(depth); + + sol.pop(); + } + + min + } + + // WHY IS IT COPY PASTED AAAAAAAAA + fn solve_p2(&self, cube: &CubieCube) -> Vec { + let cube = self.sym_tables.get_p2_cube(cube); + let prune = self.pruner.init_prune_p2(cube); + let mut sol = Vec::new(); + + let mut depth = 0; + while depth < 20 { + depth = self.search_p2(cube, prune, &mut sol, 0, depth); + if depth == 50 { + return sol.into_iter().map(|m| m.into_move()).collect(); + } + } + + panic!("No phase 2 solution found in 20 moves") + } + + fn search_p2( + &self, + cube: P2Cube, + prune: P2PruneState, + sol: &mut Vec, + cost: usize, + depth: usize, + ) -> usize { + if cube.is_solved() { + // TODO why 50... + return 50; + } + let estimate = cost + prune.val(); + if estimate > depth { + return estimate; + } + let mut min = usize::MAX; + for &m in DrMove::MOVE_LIST { + // TODO eliminate redundant moves + let cube2 = self.mover.make_p2_move(cube, m); + let prune2 = self.pruner.update_prune_p2(prune, cube2); + sol.push(m); + + let depth = self.search_p2(cube2, prune2, sol, cost + 1, depth); + if depth == 50 { + return 50; + } + min = min.min(depth); + + sol.pop(); + } + + 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 index c6be713..f7cc1fc 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -282,12 +282,12 @@ impl SubMove for DrMove { } } -type COSymMoveTable = SymMoveTable; -type EOSymMoveTable = SymMoveTable; -type ESliceEdgeMoveTable = MoveTable; -type DominoCPSymMoveTable = SymMoveTable; -type DominoEPSymMoveTable = SymMoveTable; -type DominoESliceMoveTable = MoveTable; +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 { diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index b86f1f5..f6598f7 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -12,6 +12,7 @@ use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{CubieCube, coordcube::EOCoord, moves::Move333}; use std::marker::PhantomData; +use std::rc::Rc; // TODO future stuff: // look into alternative pruning table choices @@ -81,7 +82,6 @@ type DominoESliceConjTable = SymConjTable; /// 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< - 'a, S: SymCoordinate, R: Coordinate, M: SubMove, @@ -93,28 +93,22 @@ pub struct SymRawPruningTable< { table: Box<[u8]>, conj_table: SymConjTable, - sym_table: &'a RawSymTable, - sym_move_table: &'a SymMoveTable, - raw_move_table: &'a MoveTable, + sym_table: Rc>, + sym_move_table: Rc>, + raw_move_table: Rc>, } -impl< - 'a, - S: SymCoordinate, - R: Coordinate, - M: SubMove, - const SYMS: usize, - const MOVES: usize, -> SymRawPruningTable<'a, S, R, M, SYMS, MOVES> +impl, M: SubMove, const SYMS: usize, const MOVES: usize> + SymRawPruningTable where CubieCube: FromCoordinate, CubieCube: FromCoordinate, { /// Generate the table pub fn generate( - sym_table: &'a RawSymTable, - sym_move_table: &'a SymMoveTable, - raw_move_table: &'a MoveTable, + 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(); @@ -187,13 +181,26 @@ where } } - /// Determine the bound of a coordinate pair modulo 3 with a lookup (fast) - pub fn query(&self, s: S, r: R) -> u8 { + /// 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; @@ -222,13 +229,12 @@ where } } -type ESliceTwistPruneTable<'a> = - SymRawPruningTable<'a, COSymCoord, ESliceEdgeCoord, Move333, 8, 18>; -type ESliceFlipPruneTable<'a> = SymRawPruningTable<'a, EOSymCoord, ESliceEdgeCoord, Move333, 8, 18>; -type DominoSliceCPPruneTable<'a> = - SymRawPruningTable<'a, CPSymCoord, DominoESliceCoord, DrMove, 16, 10>; -type DominoSliceEPPruneTable<'a> = - SymRawPruningTable<'a, DominoEPSymCoord, DominoESliceCoord, DrMove, 16, 10>; +pub type ESliceTwistPruneTable = SymRawPruningTable; +pub type ESliceFlipPruneTable = SymRawPruningTable; +pub type DominoSliceCPPruneTable = + SymRawPruningTable; +pub type DominoSliceEPPruneTable = + SymRawPruningTable; #[cfg(test)] mod test { @@ -271,15 +277,14 @@ mod test { }); } - fn admissable< - 'a, + fn admissable_and_update_correct< S: SymCoordinate, R: Coordinate, M: SubMove, const SYMS: usize, const MOVES: usize, >( - prune_table: &SymRawPruningTable<'a, S, R, M, SYMS, MOVES>, + prune_table: &SymRawPruningTable, mvs: MoveSequence, ) where CubieCube: FromCoordinate, @@ -289,42 +294,53 @@ mod test { 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()); - assert!(prune_table.bound(s, r) <= mvs.len()); + 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() { - let co_sym_table = RawSymTable::generate(); - let co_sym_move_table = SymMoveTable::generate(&co_sym_table); - let eo_sym_table = RawSymTable::generate(); - let eo_sym_move_table = SymMoveTable::generate(&eo_sym_table); - let e_slice_move_table = MoveTable::generate(); - let c_prune = - ESliceTwistPruneTable::generate(&co_sym_table, &co_sym_move_table, &e_slice_move_table); + 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); + ESliceFlipPruneTable::generate(eo_sym_table, eo_sym_move_table, e_slice_move_table); proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { - admissable(&c_prune, mvs.clone()); - admissable(&e_prune, mvs.clone()); + admissable_and_update_correct(&c_prune, mvs.clone()); + admissable_and_update_correct(&e_prune, mvs.clone()); }); - let cp_sym_table = RawSymTable::generate(); - let cp_sym_move_table = SymMoveTable::generate(&cp_sym_table); - let ep_sym_table = RawSymTable::generate(); - let ep_sym_move_table = SymMoveTable::generate(&ep_sym_table); - let d_e_slice_move_table = MoveTable::generate(); + 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, + 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, + ep_sym_table, + ep_sym_move_table, + d_e_slice_move_table, ); proptest!(|(mvs in vec(any::(), 0..20).prop_map(MoveSequence))| { - admissable(&d_c_prune, mvs.clone()); - admissable(&d_e_prune, mvs.clone()); + admissable_and_update_correct(&d_c_prune, mvs.clone()); + admissable_and_update_correct(&d_e_prune, mvs.clone()); }); } } From 12fd0da50d79074ac8db062b28ccee18e82807d9 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 08:13:11 +1000 Subject: [PATCH 39/45] Abstract phases to remove duplication --- src/cube333/two_phase_solver/mod.rs | 277 +++++++++++++--------------- 1 file changed, 132 insertions(+), 145 deletions(-) diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index 53d15ec..4c979fe 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -14,91 +14,130 @@ use crate::moves::MoveSequence; use std::rc::Rc; -struct Mover { - p1_co: Rc, - p1_eo: Rc, - p1_slice: Rc, - p2_cp: Rc, - p2_ep: Rc, - p2_slice: Rc, +struct Phase1; +struct Phase2; + +trait Phase { + type Cube: PhaseCube; + type Move: SubMove; + type Prune: PhasePrune; + + fn get_cube(sym_tables: &SymTables, cube: &CubieCube) -> Self::Cube; + + fn make_move(mover: &Mover, cube: Self::Cube, m: Self::Move) -> Self::Cube; + + fn init_prune(pruner: &Pruner, c: Self::Cube) -> Self::Prune; + + fn update_prune(pruner: &Pruner, p: Self::Prune, c: Self::Cube) -> Self::Prune; } -impl Mover { - fn make_p1_move(&self, cube: P1Cube, m: Move333) -> P1Cube { - let co = self.p1_co.make_move(cube.co, m); - let eo = self.p1_eo.make_move(cube.eo, m); - let slice = self.p1_slice.make_move(cube.slice, m); - P1Cube { co, eo, slice } +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_p2_move(&self, cube: P2Cube, m: DrMove) -> P2Cube { - let cp = self.p2_cp.make_move(cube.cp, m); - let ep = self.p2_ep.make_move(cube.ep, m); - let slice = self.p2_slice.make_move(cube.slice, m); - P2Cube { cp, ep, slice } + 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), + } } -} -struct Pruner { - p1_co: prune::ESliceTwistPruneTable, - p1_eo: prune::ESliceFlipPruneTable, - p2_cp: prune::DominoSliceCPPruneTable, - p2_ep: prune::DominoSliceEPPruneTable, -} + 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), + } + } -impl Pruner { - fn init_prune_p1(&self, c: P1Cube) -> P1PruneState { + fn update_prune(pruner: &Pruner, p: P1PruneState, c: P1Cube) -> P1PruneState { P1PruneState { - co_slice: self.p1_co.bound(c.co, c.slice), - eo_slice: self.p1_eo.bound(c.eo, c.slice), + 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 update_prune_p1(&self, p: P1PruneState, c: P1Cube) -> P1PruneState { - let co_slice = self.p1_co.update(p.co_slice, c.co, c.slice); - let eo_slice = self.p1_eo.update(p.eo_slice, c.eo, c.slice); - P1PruneState { co_slice, eo_slice } + 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_p2(&self, c: P2Cube) -> P2PruneState { + fn init_prune(pruner: &Pruner, c: P2Cube) -> P2PruneState { P2PruneState { - cp_slice: self.p2_cp.bound(c.cp, c.slice), - ep_slice: self.p2_ep.bound(c.ep, c.slice), + cp_slice: pruner.p2_cp.bound(c.cp, c.slice), + ep_slice: pruner.p2_ep.bound(c.ep, c.slice), } } - fn update_prune_p2(&self, p: P2PruneState, c: P2Cube) -> P2PruneState { - let cp_slice = self.p2_cp.update(p.cp_slice, c.cp, c.slice); - let ep_slice = self.p2_ep.update(p.ep_slice, c.ep, c.slice); - P2PruneState { cp_slice, ep_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), + } } } -struct SymTables { - co: Rc>, - eo: Rc>, - cp: Rc>, - ep: Rc>, +trait PhaseCube: Copy { + fn is_solved(self) -> bool; } -impl SymTables { - fn get_p1_cube(&self, c: &CubieCube) -> P1Cube { - P1Cube { - co: self.co.puzzle_to_sym(c), - eo: self.eo.puzzle_to_sym(c), - slice: coords::ESliceEdgeCoord::from_puzzle(c), - } +#[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 { + // TODO this sucks + self.co.class() == 0 && self.eo.class() == 0 && self.slice.repr() == 0 } +} - fn get_p2_cube(&self, c: &CubieCube) -> P2Cube { - P2Cube { - cp: self.cp.puzzle_to_sym(c), - ep: self.ep.puzzle_to_sym(c), - slice: coords::DominoESliceCoord::from_puzzle(c), - } +impl PhaseCube for P2Cube { + fn is_solved(self) -> bool { + self.cp.class() == 0 && self.ep.class() == 0 && self.slice.repr() == 0 } } +trait PhasePrune: Copy { + fn val(self) -> usize; +} + #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] struct P1PruneState { co_slice: usize, @@ -111,43 +150,43 @@ struct P2PruneState { ep_slice: usize, } -impl P1PruneState { +impl PhasePrune for P1PruneState { fn val(self) -> usize { self.co_slice.max(self.eo_slice) } } -impl P2PruneState { +impl PhasePrune for P2PruneState { fn val(self) -> usize { self.cp_slice.max(self.ep_slice) } } -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] -struct P1Cube { - co: coords::COSymCoord, - eo: coords::EOSymCoord, - slice: coords::ESliceEdgeCoord, +struct Mover { + p1_co: Rc, + p1_eo: Rc, + p1_slice: Rc, + p2_cp: Rc, + p2_ep: Rc, + p2_slice: Rc, } -#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] -struct P2Cube { - cp: coords::CPSymCoord, - ep: coords::DominoEPSymCoord, - slice: coords::DominoESliceCoord, +struct Pruner { + p1_co: prune::ESliceTwistPruneTable, + p1_eo: prune::ESliceFlipPruneTable, + p2_cp: prune::DominoSliceCPPruneTable, + p2_ep: prune::DominoSliceEPPruneTable, } -impl P1Cube { - fn is_solved(self) -> bool { - // TODO this sucks - self.co.class() == 0 && self.eo.class() == 0 && self.slice.repr() == 0 - } +struct SymTables { + co: Rc>, + eo: Rc>, + cp: Rc>, + ep: Rc>, } -impl P2Cube { - fn is_solved(self) -> bool { - self.cp.class() == 0 && self.ep.class() == 0 && self.slice.repr() == 0 - } +impl SymTables { + } /// A cube solver that uses Kociemba's two phase algorithm. @@ -231,75 +270,23 @@ impl Solver { /// 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_p1(&cube); + let p1 = self.solve_phase::(&cube); let cube = cube.make_moves(MoveSequence(p1.clone())); - let mut p2 = self.solve_p2(&cube); + let mut p2 = self.solve_phase::(&cube); let mut sol = p1; sol.append(&mut p2); MoveSequence(sol).cancel() } - fn solve_p1(&self, cube: &CubieCube) -> Vec { - let cube = self.sym_tables.get_p1_cube(cube); - let prune = self.pruner.init_prune_p1(cube); - let mut sol = Vec::new(); - - let mut depth = 0; - while depth < 20 { - depth = self.search_p1(cube, prune, &mut sol, 0, depth); - if depth == 50 { - return sol; - } - } - - panic!("No phase 1 solution found in 20 moves") - } - - fn search_p1( - &self, - cube: P1Cube, - prune: P1PruneState, - sol: &mut Vec, - cost: usize, - depth: usize, - ) -> usize { - if cube.is_solved() { - // TODO why 50... - return 50; - } - let estimate = cost + prune.val(); - if estimate > depth { - return estimate; - } - let mut min = usize::MAX; - for &m in Move333::MOVE_LIST { - // TODO eliminate redundant moves - let cube2 = self.mover.make_p1_move(cube, m); - let prune2 = self.pruner.update_prune_p1(prune, cube2); - sol.push(m); - - let depth = self.search_p1(cube2, prune2, sol, cost + 1, depth); - if depth == 50 { - return 50; - } - min = min.min(depth); - - sol.pop(); - } - - min - } - - // WHY IS IT COPY PASTED AAAAAAAAA - fn solve_p2(&self, cube: &CubieCube) -> Vec { - let cube = self.sym_tables.get_p2_cube(cube); - let prune = self.pruner.init_prune_p2(cube); + fn solve_phase(&self, cube: &CubieCube) -> Vec { + 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 { - depth = self.search_p2(cube, prune, &mut sol, 0, depth); + depth = self.search_phase::

(cube, prune, &mut sol, 0, depth); if depth == 50 { return sol.into_iter().map(|m| m.into_move()).collect(); } @@ -308,11 +295,11 @@ impl Solver { panic!("No phase 2 solution found in 20 moves") } - fn search_p2( + fn search_phase( &self, - cube: P2Cube, - prune: P2PruneState, - sol: &mut Vec, + cube: P::Cube, + prune: P::Prune, + sol: &mut Vec, cost: usize, depth: usize, ) -> usize { @@ -325,13 +312,13 @@ impl Solver { return estimate; } let mut min = usize::MAX; - for &m in DrMove::MOVE_LIST { + for &m in P::Move::MOVE_LIST { // TODO eliminate redundant moves - let cube2 = self.mover.make_p2_move(cube, m); - let prune2 = self.pruner.update_prune_p2(prune, cube2); + let cube2 = P::make_move(&self.mover, cube, m); + let prune2 = P::update_prune(&self.pruner, prune, cube2); sol.push(m); - let depth = self.search_p2(cube2, prune2, sol, cost + 1, depth); + let depth = self.search_phase::

(cube2, prune2, sol, cost + 1, depth); if depth == 50 { return 50; } From 0e9f68fef3ca767be6104adf8c499ae326c77f30 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 08:18:05 +1000 Subject: [PATCH 40/45] Search result enum --- src/cube333/two_phase_solver/mod.rs | 33 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index 4c979fe..d92de7b 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -185,10 +185,6 @@ struct SymTables { ep: Rc>, } -impl SymTables { - -} - /// A cube solver that uses Kociemba's two phase algorithm. pub struct Solver { mover: Mover, @@ -202,6 +198,11 @@ impl Default for Solver { } } +enum SearchResult { + Found, + Bound(usize), +} + impl Solver { /// Create a solver. pub fn new() -> Self { @@ -286,13 +287,13 @@ impl Solver { let mut depth = 0; while depth < 20 { - depth = self.search_phase::

(cube, prune, &mut sol, 0, depth); - if depth == 50 { - return sol.into_iter().map(|m| m.into_move()).collect(); + 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 2 solution found in 20 moves") + panic!("No phase solution found in 20 moves") } fn search_phase( @@ -302,14 +303,13 @@ impl Solver { sol: &mut Vec, cost: usize, depth: usize, - ) -> usize { + ) -> SearchResult { if cube.is_solved() { - // TODO why 50... - return 50; + return SearchResult::Found; } let estimate = cost + prune.val(); if estimate > depth { - return estimate; + return SearchResult::Bound(estimate); } let mut min = usize::MAX; for &m in P::Move::MOVE_LIST { @@ -318,16 +318,15 @@ impl Solver { let prune2 = P::update_prune(&self.pruner, prune, cube2); sol.push(m); - let depth = self.search_phase::

(cube2, prune2, sol, cost + 1, depth); - if depth == 50 { - return 50; + match self.search_phase::

(cube2, prune2, sol, cost + 1, depth) { + SearchResult::Found => return SearchResult::Found, + SearchResult::Bound(depth) => min = min.min(depth), } - min = min.min(depth); sol.pop(); } - min + SearchResult::Bound(min) } } From 26bd888fd38692f5510cb83297cff0c3b7c26062 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 08:23:20 +1000 Subject: [PATCH 41/45] Fix warnings --- src/cube333/two_phase_solver/coords.rs | 20 +------------------- src/cube333/two_phase_solver/move_tables.rs | 18 +++++------------- src/cube333/two_phase_solver/prune.rs | 17 ++++++++++------- src/cube333/two_phase_solver/symmetry.rs | 4 ++-- 4 files changed, 18 insertions(+), 41 deletions(-) diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index ebd4e6c..a2612d9 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -203,9 +203,6 @@ pub trait SymCoordinate: Copy + Default + Eq { type Sym: Symmetry; type Raw: Coordinate; - /// The number of possible coordinate states. - fn count() -> usize; - /// The number of equivalence classes this coordinate encodes modulo symmetry. fn classes() -> usize; @@ -284,6 +281,7 @@ where 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; @@ -301,10 +299,6 @@ impl SymCoordinate for COSymCoord { type Raw = COCoord; - fn count() -> usize { - Self::classes() * 16 - } - fn classes() -> usize { 324 } @@ -330,10 +324,6 @@ impl SymCoordinate for EOSymCoord { type Raw = EOCoord; - fn count() -> usize { - Self::classes() * 8 - } - fn classes() -> usize { 336 } @@ -359,10 +349,6 @@ impl SymCoordinate for CPSymCoord { type Raw = CPCoord; - fn count() -> usize { - Self::classes() * 16 - } - fn classes() -> usize { 2768 } @@ -388,10 +374,6 @@ impl SymCoordinate for DominoEPSymCoord { type Raw = DominoEPCoord; - fn count() -> usize { - Self::classes() * 16 - } - fn classes() -> usize { 2768 } diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index f7cc1fc..e0e2463 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -2,9 +2,10 @@ use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::CubieCube; -use crate::cube333::coordcube::{COCoord, EOCoord}; use crate::cube333::moves::{Move333, Move333Type}; -use crate::moves::{Cancellation, Move, MoveSequence}; +use crate::moves::{Cancellation, Move}; +#[cfg(test)] +use crate::moves::MoveSequence; use super::coords::{ COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, @@ -29,9 +30,6 @@ where /// Interpret a move as a normal move to be applied to a `CubieCube`. fn into_move(self) -> Move333; - /// The number of moves that exist - fn count() -> usize; - /// The list of all moves that this type encodes. The length of the returned vector should be /// `count()`. const MOVE_LIST: &'static [Self]; @@ -94,6 +92,7 @@ impl, const MOVES: usize> MoveTable) -> C { alg.0.into_iter().fold(coord, |c, m| self.make_move(c, m)) @@ -165,6 +164,7 @@ where 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)) @@ -177,10 +177,6 @@ impl SubMove for Move333 { self } - fn count() -> usize { - 18 - } - const MOVE_LIST: &'static [Move333] = crate::cube333::moves::Htm::MOVE_LIST; fn index(self) -> usize { @@ -251,10 +247,6 @@ impl SubMove for DrMove { } } - fn count() -> usize { - 10 - } - const MOVE_LIST: &'static [DrMove] = &[ DrMove::R2, DrMove::L2, diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index f6598f7..80a1bd1 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -7,9 +7,9 @@ use super::coords::{ RawSymTable, SymCoordinate, }; use super::move_tables::{DrMove, MoveTable, SubMove, SymMoveTable}; -use super::symmetry::{HalfSymmetry, Symmetry}; +use super::symmetry::{Symmetry}; use crate::coord::{Coordinate, FromCoordinate}; -use crate::cube333::{CubieCube, coordcube::EOCoord, moves::Move333}; +use crate::cube333::{CubieCube, moves::Move333}; use std::marker::PhantomData; use std::rc::Rc; @@ -70,10 +70,6 @@ where } } -type SliceConjTable = SymConjTable; -type EoConjTable = SymConjTable; -type DominoESliceConjTable = SymConjTable; - /// 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 @@ -197,7 +193,7 @@ where 0 => cur, 1 => cur + 1, 2 => cur - 1, - _ => unreachable!() + _ => unreachable!(), } } @@ -239,13 +235,20 @@ pub type DominoSliceEPPruneTable = #[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, diff --git a/src/cube333/two_phase_solver/symmetry.rs b/src/cube333/two_phase_solver/symmetry.rs index f94c93b..032c4ed 100644 --- a/src/cube333/two_phase_solver/symmetry.rs +++ b/src/cube333/two_phase_solver/symmetry.rs @@ -34,10 +34,12 @@ pub trait Symmetry: Copy + Default + Eq { } 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 { @@ -143,8 +145,6 @@ impl #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] pub struct DrSymmetry(u8); -pub type DrSymMultTable = SymMultTable; - /// A cube that, when multiplied, applies the F2 symmetry. #[rustfmt::skip] const SYM_F2: CubieCube = CubieCube { From d9b7e53d1934ca6db04c468033fc2e470a549a6a Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 08:40:03 +1000 Subject: [PATCH 42/45] Eliminate redundant moves --- src/cube333/moves.rs | 14 ++++++++++++++ src/cube333/two_phase_solver/mod.rs | 6 ++++-- src/cube333/two_phase_solver/move_tables.rs | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/cube333/moves.rs b/src/cube333/moves.rs index 7ec1605..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))] diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index d92de7b..d29be84 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -312,8 +312,10 @@ impl Solver { return SearchResult::Bound(estimate); } let mut min = usize::MAX; - for &m in P::Move::MOVE_LIST { - // TODO eliminate redundant moves + 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); diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index e0e2463..7a63a97 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -30,6 +30,10 @@ where /// 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]; @@ -247,6 +251,17 @@ impl SubMove for DrMove { } } + 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, From 170973f5d19fd6f5921cc29b0396eb99f554cc0b Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 09:57:47 +1000 Subject: [PATCH 43/45] Solved fn --- src/coord.rs | 6 ++++++ src/cube333/two_phase_solver/coords.rs | 5 +++++ src/cube333/two_phase_solver/mod.rs | 5 ++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/coord.rs b/src/coord.rs index cb52139..dc7309c 100644 --- a/src/coord.rs +++ b/src/coord.rs @@ -8,6 +8,12 @@ pub trait Coordinate

: Copy + Default + Eq { /// 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; diff --git a/src/cube333/two_phase_solver/coords.rs b/src/cube333/two_phase_solver/coords.rs index a2612d9..14d2f5e 100644 --- a/src/cube333/two_phase_solver/coords.rs +++ b/src/cube333/two_phase_solver/coords.rs @@ -206,6 +206,11 @@ pub trait SymCoordinate: Copy + Default + Eq { /// 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()) diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index d29be84..66b0797 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -123,14 +123,13 @@ struct P2Cube { impl PhaseCube for P1Cube { fn is_solved(self) -> bool { - // TODO this sucks - self.co.class() == 0 && self.eo.class() == 0 && self.slice.repr() == 0 + self.co.solved() && self.eo.solved() && self.slice.solved() } } impl PhaseCube for P2Cube { fn is_solved(self) -> bool { - self.cp.class() == 0 && self.ep.class() == 0 && self.slice.repr() == 0 + self.cp.solved() && self.ep.solved() && self.slice.solved() } } From 5da3eaed6cf09e6006b615772decd113616188b6 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 18:39:42 +1000 Subject: [PATCH 44/45] Comments --- src/cube333/two_phase_solver/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cube333/two_phase_solver/mod.rs b/src/cube333/two_phase_solver/mod.rs index 66b0797..bc0bf69 100644 --- a/src/cube333/two_phase_solver/mod.rs +++ b/src/cube333/two_phase_solver/mod.rs @@ -17,17 +17,22 @@ 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; } @@ -280,6 +285,7 @@ impl Solver { } 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(); From 203df6243a202f798f599049d8fe57bed2e847d7 Mon Sep 17 00:00:00 2001 From: bpaul Date: Fri, 26 Dec 2025 18:41:34 +1000 Subject: [PATCH 45/45] fmt --- src/cube333/two_phase_solver/move_tables.rs | 2 +- src/cube333/two_phase_solver/prune.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cube333/two_phase_solver/move_tables.rs b/src/cube333/two_phase_solver/move_tables.rs index 7a63a97..880000e 100644 --- a/src/cube333/two_phase_solver/move_tables.rs +++ b/src/cube333/two_phase_solver/move_tables.rs @@ -3,9 +3,9 @@ use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::CubieCube; use crate::cube333::moves::{Move333, Move333Type}; -use crate::moves::{Cancellation, Move}; #[cfg(test)] use crate::moves::MoveSequence; +use crate::moves::{Cancellation, Move}; use super::coords::{ COSymCoord, CPSymCoord, DominoEPSymCoord, DominoESliceCoord, EOSymCoord, ESliceEdgeCoord, diff --git a/src/cube333/two_phase_solver/prune.rs b/src/cube333/two_phase_solver/prune.rs index 80a1bd1..de38193 100644 --- a/src/cube333/two_phase_solver/prune.rs +++ b/src/cube333/two_phase_solver/prune.rs @@ -7,7 +7,7 @@ use super::coords::{ RawSymTable, SymCoordinate, }; use super::move_tables::{DrMove, MoveTable, SubMove, SymMoveTable}; -use super::symmetry::{Symmetry}; +use super::symmetry::Symmetry; use crate::coord::{Coordinate, FromCoordinate}; use crate::cube333::{CubieCube, moves::Move333};