A Functional Pearl: A minimalistic Rubik's Cube solver in under 400 lines of Python, powered by linear algebra.
https://github.com/user-attachments/assets/17c609ec-3adf-4eeb-b4e9-7d90617dfbfb
Most Rubik's cube solvers rely on complex combinatorial bookkeeping: tracking 54 color stickers mapped across flat arrays, maintaining lookup tables for permutations, or precomputing massive 100MB pattern databases.
Eigencube takes a different path. By framing the puzzle in discrete 3-dimensional Euclidean space using linear algebra, the entire physics, state, and solution of the Rubik's Cube reduce to vectors, rotation matrices, and dot products.
The payoff of this linear algebra formulation is radical simplicity: the entire puzzle model, transformations, and multi-phase solver in eigencube.py fit in under 400 lines of readable Python—with zero external puzzle libraries, zero lookup tables, and zero precomputed pattern databases.
[!NOTE] Shoutout: The core insight for Eigencube was directly sparked by Grant Sanderson’s (3Blue1Brown) masterclass YouTube series, Essence of Linear Algebra. The series’ emphasis on geometric intuition—treating matrices as transformations of coordinate space and tracking where standard basis vectors land—inspired ditching messy combinatorial sticker permutations in favor of discrete 3D rotation matrices, vectors, and inner products. Massive praise and props to Grant for making linear algebra so intuitive, visual, and delightful!
| Concept | In linear algebra |
|---|---|
| Cubelets | vectors $c \in \lbrace -1, 0, 1 \rbrace^3$: each cubelet's home address, from the fixed core |
| Cubelet type | $\lVert c \rVert_1 = \lvert x \rvert + \lvert y \rvert + \lvert z \rvert$ = number of stickers (core 0, center 1, edge 2, corner 3) |
| Colors | $\pm\mathbf{e}_x, \pm\mathbf{e}_y, \pm\mathbf{e}_z$: the never-moving centers, each an eigenvector of the turns around it |
| Sticker colors | the non-zero columns of $\mathrm{diag}(c)$ |
| State | one configuration $(c, R)$ per cubelet, with $R$ a rotation matrix |
| Position | $p = R,c$: where the cubelet is now |
| Move | the cubelets with $\mathbf{v} \cdot (R,c) > 0$ turn ($\mathbf{v}$: the face's axis): $R \leftarrow M R$ ($M$: the quarter-turn matrix) |
| Solved | $R \cdot \mathrm{diag}(c) = \mathrm{diag}(c)$ for every cubelet |
Finding the optimal solution to an arbitrary Rubik's cube is NP-hard, and God's Number (20 moves) requires massive precomputed pattern databases.
eigencube.py implements a hierarchical multi-phase A* search that mimics human layer-by-layer reduction, staying strictly under 400 lines without precomputed databases:
[Scrambled Cube]
|
v
Phase 1: Top Layer & Centers (17 cubelets)
|
v
Phase 2: Middle Layer Edges (4 cubelets)
|
v
Phase 3: Bottom Cross (4 edge orientations & positions)
|
v
Phase 4: Bottom Corners (4 corner placements & twists)
|
v
Phase 5: Endgame Permutation Alignment
|
v
[Solved Cube]
A* needs a sense of direction so it doesn't search aimlessly. Rather than storing gigabytes of precomputed lookup tables, Eigencube calculates a quick distance estimate on the fly:
min_moves_to_solved): Calculates how many 90° turns an isolated cubelet needs to reach its solved coordinate and orientation if no other cubelets were in the way.@functools.cache).RANDOMIZE_SEARCH = True) bounded by deterministic move budgets (max_moves=100_000 with gentle $1.5\times$ expansion). Instead of wall-clock timers and OS signals, searches cut losses after exploring ~100k moves (~2s) and retry with fresh randomized weights without discarding prior layer progress. Typical scrambles solve in 5–25 seconds.venv)# Clone the repository
git clone https://github.com/smolkaj/eigencube.git
cd eigencube
# Create and activate a virtual environment
python3 -m venv eigencube_env
source eigencube_env/bin/activate
# Install dependencies (numpy, pygame, opencv-python, Pillow)
pip install -r requirements.txt
Launch the 2D cube net visualizer:
python eigencube_gui.py
Right Arrow: Step forward through the solution moves (hold to fast-forward at 15×).Left Arrow: Step backward through the solution moves (rewind).Solve a scrambled cube directly from the terminal:
# Solve a 100,000-move scramble with default seed (42)
python eigencube.py
# Solve with a specific random seed
python eigencube.py 123
# Run benchmark across 100 random scrambles
python eigencube.py --benchmark
Use eigencube as a lightweight puzzle simulation and solving library:
from eigencube import solved_cube, shuffle, solve, describe_move, is_cube_solved, apply_move_to_cube
# Create a scrambled cube
scrambled = shuffle(solved_cube, iterations=1000, seed=42)
print("Is solved?", is_cube_solved(scrambled)) # False
# Compute solution moves
solution = solve(scrambled)
print(f"Solved in {len(solution)} moves:")
for move in solution:
print(" -", describe_move(move))
# Apply solution and verify final state
final_cube = scrambled
for move in solution:
final_cube = apply_move_to_cube(move, final_cube)
print("Is solved?", is_cube_solved(final_cube)) # True
Run the test suite:
python3 -m unittest discover tests
The test suite covers:
eigencube/
├── eigencube.py # Core solver & linear algebra model (< 400 lines)
├── eigencube_gui.py # Pygame GUI with animated moves & step playback
├── eigencube_scanner.py # Computer vision scanner for physical cubes (OpenCV)
├── explainer/ # Animated video explainer of the encoding (Manim)
├── scripts/
│ └── generate_logo.py # Logo, icons & GitHub social preview generator
├── tests/
│ ├── test_eigencube.py # Unit test suite
│ └── test_explainer_kit.py # The explainer film's geometry helpers
├── img/ # Logo, icons & the GUI's snapshot
│ ├── gui-preview.png # Written by eigencube_gui.render_frame_to_image
│ ├── logo.svg # Logo; generate_logo.py also writes the icons below
│ ├── apple-touch-icon.png
│ ├── favicon.ico
│ ├── icon.png # GUI window icon
│ └── social-preview.png
├── fonts/ # Roboto, for the GUI and the logo generator
├── requirements.txt # numpy, pygame, opencv-python, Pillow
└── README.md
left, top, bottom rather than L, U, D) so anyone can understand the logic without an external decoder ring.eigencube.py strictly stays below 400 lines of clean, readable Python.eigencube_gui works seamlessly in headless CI/CD environments.
A Functional Pearl: A minimalistic Rubik's Cube solver in under 400 lines of Python, powered by linear algebra.
https://github.com/user-attachments/assets/17c609ec-3adf-4eeb-b4e9-7d90617dfbfb
Most Rubik's cube solvers rely on complex combinatorial bookkeeping: tracking 54 color stickers mapped across flat arrays, maintaining lookup tables for permutations, or precomputing massive 100MB pattern databases.
Eigencube takes a different path. By framing the puzzle in discrete 3-dimensional Euclidean space using linear algebra, the entire physics, state, and solution of the Rubik's Cube reduce to vectors, rotation matrices, and dot products.
The payoff of this linear algebra formulation is radical simplicity: the entire puzzle model, transformations, and multi-phase solver in eigencube.py fit in under 400 lines of readable Python—with zero external puzzle libraries, zero lookup tables, and zero precomputed pattern databases.
[!NOTE] Shoutout: The core insight for Eigencube was directly sparked by Grant Sanderson’s (3Blue1Brown) masterclass YouTube series, Essence of Linear Algebra. The series’ emphasis on geometric intuition—treating matrices as transformations of coordinate space and tracking where standard basis vectors land—inspired ditching messy combinatorial sticker permutations in favor of discrete 3D rotation matrices, vectors, and inner products. Massive praise and props to Grant for making linear algebra so intuitive, visual, and delightful!
| Concept | In linear algebra |
|---|---|
| Cubelets | vectors $c \in \lbrace -1, 0, 1 \rbrace^3$: each cubelet's home address, from the fixed core |
| Cubelet type | $\lVert c \rVert_1 = \lvert x \rvert + \lvert y \rvert + \lvert z \rvert$ = number of stickers (core 0, center 1, edge 2, corner 3) |
| Colors | $\pm\mathbf{e}_x, \pm\mathbf{e}_y, \pm\mathbf{e}_z$: the never-moving centers, each an eigenvector of the turns around it |
| Sticker colors | the non-zero columns of $\mathrm{diag}(c)$ |
| State | one configuration $(c, R)$ per cubelet, with $R$ a rotation matrix |
| Position | $p = R,c$: where the cubelet is now |
| Move | the cubelets with $\mathbf{v} \cdot (R,c) > 0$ turn ($\mathbf{v}$: the face's axis): $R \leftarrow M R$ ($M$: the quarter-turn matrix) |
| Solved | $R \cdot \mathrm{diag}(c) = \mathrm{diag}(c)$ for every cubelet |
Finding the optimal solution to an arbitrary Rubik's cube is NP-hard, and God's Number (20 moves) requires massive precomputed pattern databases.
eigencube.py implements a hierarchical multi-phase A* search that mimics human layer-by-layer reduction, staying strictly under 400 lines without precomputed databases:
[Scrambled Cube]
|
v
Phase 1: Top Layer & Centers (17 cubelets)
|
v
Phase 2: Middle Layer Edges (4 cubelets)
|
v
Phase 3: Bottom Cross (4 edge orientations & positions)
|
v
Phase 4: Bottom Corners (4 corner placements & twists)
|
v
Phase 5: Endgame Permutation Alignment
|
v
[Solved Cube]
A* needs a sense of direction so it doesn't search aimlessly. Rather than storing gigabytes of precomputed lookup tables, Eigencube calculates a quick distance estimate on the fly:
min_moves_to_solved): Calculates how many 90° turns an isolated cubelet needs to reach its solved coordinate and orientation if no other cubelets were in the way.@functools.cache).RANDOMIZE_SEARCH = True) bounded by deterministic move budgets (max_moves=100_000 with gentle $1.5\times$ expansion). Instead of wall-clock timers and OS signals, searches cut losses after exploring ~100k moves (~2s) and retry with fresh randomized weights without discarding prior layer progress. Typical scrambles solve in 5–25 seconds.venv)# Clone the repository
git clone https://github.com/smolkaj/eigencube.git
cd eigencube
# Create and activate a virtual environment
python3 -m venv eigencube_env
source eigencube_env/bin/activate
# Install dependencies (numpy, pygame, opencv-python, Pillow)
pip install -r requirements.txt
Launch the 2D cube net visualizer:
python eigencube_gui.py
Right Arrow: Step forward through the solution moves (hold to fast-forward at 15×).Left Arrow: Step backward through the solution moves (rewind).Solve a scrambled cube directly from the terminal:
# Solve a 100,000-move scramble with default seed (42)
python eigencube.py
# Solve with a specific random seed
python eigencube.py 123
# Run benchmark across 100 random scrambles
python eigencube.py --benchmark
Use eigencube as a lightweight puzzle simulation and solving library:
from eigencube import solved_cube, shuffle, solve, describe_move, is_cube_solved, apply_move_to_cube
# Create a scrambled cube
scrambled = shuffle(solved_cube, iterations=1000, seed=42)
print("Is solved?", is_cube_solved(scrambled)) # False
# Compute solution moves
solution = solve(scrambled)
print(f"Solved in {len(solution)} moves:")
for move in solution:
print(" -", describe_move(move))
# Apply solution and verify final state
final_cube = scrambled
for move in solution:
final_cube = apply_move_to_cube(move, final_cube)
print("Is solved?", is_cube_solved(final_cube)) # True
Run the test suite:
python3 -m unittest discover tests
The test suite covers:
eigencube/
├── eigencube.py # Core solver & linear algebra model (< 400 lines)
├── eigencube_gui.py # Pygame GUI with animated moves & step playback
├── eigencube_scanner.py # Computer vision scanner for physical cubes (OpenCV)
├── explainer/ # Animated video explainer of the encoding (Manim)
├── scripts/
│ └── generate_logo.py # Logo, icons & GitHub social preview generator
├── tests/
│ ├── test_eigencube.py # Unit test suite
│ └── test_explainer_kit.py # The explainer film's geometry helpers
├── img/ # Logo, icons & the GUI's snapshot
│ ├── gui-preview.png # Written by eigencube_gui.render_frame_to_image
│ ├── logo.svg # Logo; generate_logo.py also writes the icons below
│ ├── apple-touch-icon.png
│ ├── favicon.ico
│ ├── icon.png # GUI window icon
│ └── social-preview.png
├── fonts/ # Roboto, for the GUI and the logo generator
├── requirements.txt # numpy, pygame, opencv-python, Pillow
└── README.md
left, top, bottom rather than L, U, D) so anyone can understand the logic without an external decoder ring.eigencube.py strictly stays below 400 lines of clean, readable Python.eigencube_gui works seamlessly in headless CI/CD environments.