alphacube 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alphacube-0.1.0/PKG-INFO +23 -0
- alphacube-0.1.0/README.md +168 -0
- alphacube-0.1.0/alphacube/__init__.py +161 -0
- alphacube-0.1.0/alphacube/_validator.py +149 -0
- alphacube-0.1.0/alphacube/env.py +261 -0
- alphacube-0.1.0/alphacube/model.py +124 -0
- alphacube-0.1.0/alphacube/search.py +285 -0
- alphacube-0.1.0/alphacube/solver.py +101 -0
- alphacube-0.1.0/alphacube.egg-info/PKG-INFO +23 -0
- alphacube-0.1.0/alphacube.egg-info/SOURCES.txt +14 -0
- alphacube-0.1.0/alphacube.egg-info/dependency_links.txt +1 -0
- alphacube-0.1.0/alphacube.egg-info/entry_points.txt +3 -0
- alphacube-0.1.0/alphacube.egg-info/requires.txt +5 -0
- alphacube-0.1.0/alphacube.egg-info/top_level.txt +1 -0
- alphacube-0.1.0/setup.cfg +4 -0
- alphacube-0.1.0/setup.py +41 -0
alphacube-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 1.2
|
|
2
|
+
Name: alphacube
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A powerful & flexible Rubik's Cube solver
|
|
5
|
+
Home-page: https://alphacube.dev/
|
|
6
|
+
Author: Kyo Takano
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Documentation, https://alphacube.dev/docs/index.html
|
|
9
|
+
Project-URL: Source, https://github.com/kyo-takano/alphacube
|
|
10
|
+
Description: UNKNOWN
|
|
11
|
+
Platform: UNKNOWN
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Requires-Python: >= 3.6
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# AlphaCube
|
|
2
|
+
|
|
3
|
+
AlphaCube is a powerful & flexible Rubik's Cube solver that extends [EfficientCube](https://github.com/kyo-takano/efficientcube).
|
|
4
|
+
|
|
5
|
+
## App
|
|
6
|
+
|
|
7
|
+
We offer an interactive demo application for you to try out AlphaCube:
|
|
8
|
+
[alphacube.dev](https://alphacube.dev)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Open a terminal and execute the following command:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install alphacube
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic
|
|
21
|
+
|
|
22
|
+
Using AlphaCube in Python is as simple as this:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import alphacube
|
|
26
|
+
|
|
27
|
+
# Load a trained DNN
|
|
28
|
+
alphacube.load(model_id="small") # the default model
|
|
29
|
+
|
|
30
|
+
# Solve the cube using a given scramble
|
|
31
|
+
result = alphacube.solve(
|
|
32
|
+
scramble="D U F2 L2 U' B2 F2 D L2 U R' F' D R' F' U L D' F' D R2",
|
|
33
|
+
beam_width=1024,
|
|
34
|
+
)
|
|
35
|
+
print(result)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **Output**
|
|
39
|
+
> ```python
|
|
40
|
+
> {
|
|
41
|
+
> 'solutions': [
|
|
42
|
+
> "D L D2 R' U2 D B' D' U2 B U2 B' U' B2 D B2 D' B2 F2 U2 F2"
|
|
43
|
+
> ],
|
|
44
|
+
> 'num_nodes': 19744,
|
|
45
|
+
> 'time':1.4068585219999659
|
|
46
|
+
> }
|
|
47
|
+
> ```
|
|
48
|
+
|
|
49
|
+
AlphaCube selects the smallest model by default and takes about a few seconds to find moderately good solution(s).
|
|
50
|
+
|
|
51
|
+
### Better Solutions
|
|
52
|
+
|
|
53
|
+
If you want even shorter solutions, simply increase the `beam_width` parameter:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
alphacube.load() # model_id="small"
|
|
57
|
+
result = alphacube.solve(
|
|
58
|
+
scramble="D U F2 L2 U' B2 F2 D L2 U R' F' D R' F' U L D' F' D R2",
|
|
59
|
+
beam_width=65536,
|
|
60
|
+
)
|
|
61
|
+
print(result)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> **Output**
|
|
65
|
+
> ```python
|
|
66
|
+
> {
|
|
67
|
+
> 'solutions': [
|
|
68
|
+
> "D' R' D2 F' L2 F' U B F D L D' L B D2 R2 F2 R2 F'",
|
|
69
|
+
> "D2 L2 R' D' B D2 B' D B2 R2 U2 L' U L' D' U2 R' F2 R'"
|
|
70
|
+
> ],
|
|
71
|
+
> 'num_nodes': 968984,
|
|
72
|
+
> 'time': 45.690575091997744
|
|
73
|
+
> }
|
|
74
|
+
> ```
|
|
75
|
+
|
|
76
|
+
You generally get more and better solutions, at the sacrifice of an increased wall-clock time and computational cost.
|
|
77
|
+
|
|
78
|
+
### Applying Ergonomic Bias
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
ergonomic_bias = {
|
|
82
|
+
"U": 0.9, "U'": 0.9, "U2": 0.8,
|
|
83
|
+
"R": 0.8, "R'": 0.8, "R2": 0.75,
|
|
84
|
+
"L": 0.55, "L'": 0.4, "L2": 0.3,
|
|
85
|
+
"F": 0.7, "F'": 0.6, "F2": 0.6,
|
|
86
|
+
"D": 0.3, "D'": 0.3, "D2": 0.2,
|
|
87
|
+
"B": 0.05, "B'": 0.05, "B2": 0.01,
|
|
88
|
+
"u": 0.45, "u'": 0.45, "u2": 0.4,
|
|
89
|
+
"r": 0.3, "r'": 0.3, "r2": 0.25,
|
|
90
|
+
"l": 0.2, "l'": 0.2, "l2": 0.15,
|
|
91
|
+
"f": 0.35, "f'": 0.3, "f2": 0.25,
|
|
92
|
+
"d": 0.15, "d'": 0.15, "d2": 0.1,
|
|
93
|
+
"b": 0.03, "b'": 0.03, "b2": 0.01
|
|
94
|
+
} # Each value represents the desirability score
|
|
95
|
+
|
|
96
|
+
result = alphacube.solve(
|
|
97
|
+
scramble="D U F2 L2 U' B2 F2 D L2 U R' F' D R' F' U L D' F' D R2",
|
|
98
|
+
beam_width=65536,
|
|
99
|
+
ergonomic_bias=ergonomic_bias
|
|
100
|
+
)
|
|
101
|
+
print(result)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> **Output**
|
|
105
|
+
> ```python
|
|
106
|
+
> {
|
|
107
|
+
> 'solutions': [
|
|
108
|
+
> "u' U' f' R2 U2 R' L' F' R D2 f2 R2 U2 R U L' U R L",
|
|
109
|
+
> "u' U' f' R2 U2 R' L' F' R D2 f2 R2 U2 R d F' U f F",
|
|
110
|
+
> "u' U' f' R2 U2 R' L' F' R u2 F2 R2 D2 R u f' l u U"
|
|
111
|
+
> ],
|
|
112
|
+
> 'num_nodes': 1078054,
|
|
113
|
+
> 'time': 56.13087955299852
|
|
114
|
+
> }
|
|
115
|
+
> ```
|
|
116
|
+
|
|
117
|
+
### GPU Acceleration
|
|
118
|
+
|
|
119
|
+
If you have a GPU, we highly recommend you select the largest model for the best possible performance:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
alphacube.load(model_id="large")
|
|
123
|
+
result = alphacube.solve(
|
|
124
|
+
scramble="D U F2 L2 U' B2 F2 D L2 U R' F' D R' F' U L D' F' D R2",
|
|
125
|
+
beam_width=65536,
|
|
126
|
+
)
|
|
127
|
+
print(result)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> **Output**
|
|
131
|
+
> ```python
|
|
132
|
+
> {
|
|
133
|
+
> 'solutions': ["D F L' F' U2 B2 U F' L R2 B2 U D' F2 U2 R D'"],
|
|
134
|
+
> 'num_nodes': 903448,
|
|
135
|
+
> 'time': 20.46845487099995
|
|
136
|
+
> }
|
|
137
|
+
> ```
|
|
138
|
+
|
|
139
|
+
You will get even better solutions in an order of magnitude smaller amount of time.
|
|
140
|
+
|
|
141
|
+
On the contrary, using a model larger than `"small"` (i.e., `"base"` or `"large"`) *on CPU* will likely result in inferior temporal performance.
|
|
142
|
+
|
|
143
|
+
### Verbose Mode
|
|
144
|
+
|
|
145
|
+
Additionally, you may call `alphacube.set_verbose()` to keep track of the progress. It will display the current depth of search on your terminal.
|
|
146
|
+
|
|
147
|
+
Please refer to our [documentation](https://alphacube.dev/docs) for more, especially ["Getting Started"](https://alphacube.dev/docs/getting-started/index.html)
|
|
148
|
+
|
|
149
|
+
## How It Works
|
|
150
|
+
|
|
151
|
+
At the heart of AlphaCube lies a methodology rooted in the paper titled ["Self-Supervision is All You Need for Solving Rubik's Cube" (TMLR'23)](https://openreview.net/forum?id=bnBeNFB27b), the official code of which is also available as a [GitHub repository](https://github.com/kyo-takano/efficientcube).
|
|
152
|
+
This project releases the three largest solvers trained in Half-Turn Metric (HTM) as described in **Section 7. Scaling Law**.
|
|
153
|
+
|
|
154
|
+
In EfficientCube, a Deep Neural Network (DNN) is trained to predict the **probability distribution of the next moves** that would bring a given state one step closer to the goal.
|
|
155
|
+
These predicted moves are then applied sequentially to solve a given scramble.
|
|
156
|
+
|
|
157
|
+
This project releases the three largest, compute-optimally trained solvers as described in **Section 7. Scaling Law**.
|
|
158
|
+
|
|
159
|
+
You may also read ["How It Works"](https://alphacube.dev/docs/how-it-works/index.html) in the documentation for illustrated descriptions.
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
You are more than welcome to collaborate on AlphaCube.
|
|
164
|
+
If you're interested, please read our [CONTRIBUTING](https://github.com/kyo-takano/alphacube/CONTRIBUTING.md) guide to get started.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
AlphaCube is open-sourced under the [MIT license](https://github.com/kyo-takano/alphacube/LICENSE), granting you the freedom to use and modify it.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package provides functions to load a deep neural Rubik's Cube solver (DNN) and to solve a scrambled Rubik's Cube with it.
|
|
3
|
+
It also offers a command-line utility for solving a Rubik's Cube using AlphaCube.
|
|
4
|
+
|
|
5
|
+
Functions:
|
|
6
|
+
|
|
7
|
+
- ``load(*args, **kwargs)``: Load a trained DNN.
|
|
8
|
+
- ``solve(*args, **kwargs)``: Solve a Rubik's Cube using the loaded solver.
|
|
9
|
+
- ``cli()``: Command-line utility for solving a Rubik's Cube using AlphaCube.
|
|
10
|
+
- ``set_verbose(loglevel=logging.INFO)``: Set the verbosity level of the logger.
|
|
11
|
+
|
|
12
|
+
Example::
|
|
13
|
+
|
|
14
|
+
import alphacube
|
|
15
|
+
alphacube.load(model_id="base")
|
|
16
|
+
solution = alphacube.solve(format='moves', scramble="R U R' U'", beam_width=1024)
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
import logging
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.logging import RichHandler
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("rich")
|
|
24
|
+
logger.propagate = False
|
|
25
|
+
logger.addHandler(RichHandler(console=Console(stderr=True)))
|
|
26
|
+
logger.setLevel(logging.WARNING)
|
|
27
|
+
logargs = dict(extra={"markup": True})
|
|
28
|
+
|
|
29
|
+
# Set up logging level
|
|
30
|
+
def set_verbose(loglevel=logging.INFO):
|
|
31
|
+
"""
|
|
32
|
+
Set the verbosity level of the logger.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
loglevel (int): Logging level (e.g., logging.INFO, logging.DEBUG) to control the verbosity.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
None
|
|
39
|
+
"""
|
|
40
|
+
global logger
|
|
41
|
+
logger.setLevel(loglevel)
|
|
42
|
+
|
|
43
|
+
from ._validator import Input
|
|
44
|
+
from .solver import Solver
|
|
45
|
+
|
|
46
|
+
_solver = Solver()
|
|
47
|
+
|
|
48
|
+
def load(*args, **kwargs):
|
|
49
|
+
"""
|
|
50
|
+
Load the Rubik's Cube solver model.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
*args, **kwargs: Arguments to configure model loading.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
None
|
|
57
|
+
"""
|
|
58
|
+
_solver.load(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
def solve(*args, **kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Solve a Rubik's Cube puzzle using the loaded solver model.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
*args, **kwargs: Arguments to configure puzzle solving; passed down to ``alphacube.solver.Solver.__call__()`` and ``alphacube.search.beam_search``.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict | None: A dictionary containing solutions and performance metrics. None if failed.
|
|
69
|
+
"""
|
|
70
|
+
if _solver.model is None:
|
|
71
|
+
raise ValueError('Model not loaded. Call `load` with appropriate arguments first.')
|
|
72
|
+
|
|
73
|
+
return _solver(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
# CLI Option
|
|
76
|
+
def cli():
|
|
77
|
+
"""
|
|
78
|
+
Command-line utility for solving a Rubik's Cube using AlphaCube.
|
|
79
|
+
|
|
80
|
+
Usage::
|
|
81
|
+
|
|
82
|
+
alphacube [--model_id MODEL_ID] [--format FORMAT] [--scramble SCRAMBLE]
|
|
83
|
+
[--beam_width BEAM_WIDTH] [--extra_depths EXTRA_DEPTHS]
|
|
84
|
+
|
|
85
|
+
Command-line Arguments:
|
|
86
|
+
|
|
87
|
+
* ``--model_id``, ``-m`` (str): Choose a specific model for solving (default: 'small').
|
|
88
|
+
* ``--format``, ``-f`` (str): Specify the input format ('moves' or 'stickers').
|
|
89
|
+
* ``--scramble``, ``-s`` (str): Define the initial cube state using either a sequence of moves or a stringify JSON dictionary.
|
|
90
|
+
* ``--beam_width``, ``-bw`` (int): Set the beam width for search (default: 1024).
|
|
91
|
+
* ``--extra_depths``, ``-ex`` (int): Specify additional depths for exploration (default: 0).
|
|
92
|
+
* ``--verbose``, ``-v``: Enable verbose output for debugging and tracking progress.
|
|
93
|
+
|
|
94
|
+
Example Usages:
|
|
95
|
+
|
|
96
|
+
1. Solve a cube using default settings::
|
|
97
|
+
|
|
98
|
+
alphacube --scramble "R U R' U'"
|
|
99
|
+
|
|
100
|
+
2. Solve a cube with custom settings::
|
|
101
|
+
|
|
102
|
+
alphacube --model_id large --beam_width 128 --extra_depths 2 \\
|
|
103
|
+
--scramble "R U2 F' R2 B R' U' L B2 D' U2 R F L"
|
|
104
|
+
|
|
105
|
+
3. Solve a cube using a sticker representation::
|
|
106
|
+
|
|
107
|
+
alphacube --format stickers \\
|
|
108
|
+
--scramble '{ \\
|
|
109
|
+
"U": [0, 0, 5, 5, 0, 5, 5, 4, 0], \\
|
|
110
|
+
"D": [1, 3, 3, 4, 1, 1, 4, 1, 3], \\
|
|
111
|
+
"L": [4, 5, 1, 0, 2, 2, 0, 1, 4], \\
|
|
112
|
+
"R": [5, 2, 0, 2, 3, 3, 2, 0, 2], \\
|
|
113
|
+
"F": [4, 3, 2, 4, 5, 1, 1, 4, 3], \\
|
|
114
|
+
"B": [1, 5, 5, 0, 4, 3, 3, 2, 2] \\
|
|
115
|
+
}' \\
|
|
116
|
+
--beam_width 64
|
|
117
|
+
|
|
118
|
+
:return: None
|
|
119
|
+
"""
|
|
120
|
+
from rich import print
|
|
121
|
+
|
|
122
|
+
import argparse
|
|
123
|
+
parser = argparse.ArgumentParser(description="AlphaCube -- State-of-the-Art Rubik's Cube Solver")
|
|
124
|
+
parser.add_argument("--model_id", "-m", type=str, default="small", help="ID of the model to solve a given scramble with (`small`, `base`, or `large`)")
|
|
125
|
+
parser.add_argument("--format", "-f", type=str, default="moves", help="Format of the input scramble (`moves` or `stickers`)")
|
|
126
|
+
parser.add_argument("--scramble", "-s", type=str, help="Sequence of scramble in HTM (including wide moves) or dictionary of sticker indices by face.")
|
|
127
|
+
parser.add_argument("--beam_width", "-bw", type=int, default=1024, help="Beam width, the parameter strongly correlated with solution optimality and computation time")
|
|
128
|
+
parser.add_argument("--extra_depths", "-ex", type=int, default=0, help="Number of additional depths to explore during the search")
|
|
129
|
+
parser.add_argument('--verbose', '-v', dest="loglevel", action="store_const", const=logging.INFO, help="Enable verbose output for tracking progress")
|
|
130
|
+
args = parser.parse_args()
|
|
131
|
+
|
|
132
|
+
if args.loglevel:
|
|
133
|
+
set_verbose(args.loglevel)
|
|
134
|
+
|
|
135
|
+
# Manual validation
|
|
136
|
+
model_id = args.model_id
|
|
137
|
+
assert model_id in ["small", "base","large"], "Model ID must be either 'base' or 'large'"
|
|
138
|
+
|
|
139
|
+
logger.info(args)
|
|
140
|
+
|
|
141
|
+
# Pydantic validation
|
|
142
|
+
args = Input(
|
|
143
|
+
format=args.format,
|
|
144
|
+
scramble=args.scramble,
|
|
145
|
+
beam_width=args.beam_width,
|
|
146
|
+
extra_depths=args.extra_depths,
|
|
147
|
+
ergonomic_bias=None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Load only once validated
|
|
151
|
+
_solver.load(model_id=model_id)
|
|
152
|
+
# Solve
|
|
153
|
+
solutions = _solver(
|
|
154
|
+
format=args.format,
|
|
155
|
+
scramble=args.scramble,
|
|
156
|
+
beam_width=args.beam_width,
|
|
157
|
+
extra_depths=args.extra_depths
|
|
158
|
+
)
|
|
159
|
+
print(solutions)
|
|
160
|
+
|
|
161
|
+
__all__ = ['load', 'solve', "cli"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input Validator
|
|
3
|
+
|
|
4
|
+
This module defines the input validator for the AlphaCube application. It includes a Pydantic `Input` class that validates input data for the application's tasks. The input data must adhere to specific format and validation rules.
|
|
5
|
+
|
|
6
|
+
Validation Rules:
|
|
7
|
+
|
|
8
|
+
- ``format`` attribute must be either 'moves' or 'stickers'.
|
|
9
|
+
- For 'moves' format, the `scramble` attribute must consist of valid moves.
|
|
10
|
+
- For 'stickers' format (not implemented), additional validation may be performed.
|
|
11
|
+
- ``beam_width`` attribute must be a positive integer.
|
|
12
|
+
- ``extra_depths`` attribute must be a non-negative integer.
|
|
13
|
+
|
|
14
|
+
Usage Example::
|
|
15
|
+
|
|
16
|
+
input_data = {
|
|
17
|
+
'format': 'moves',
|
|
18
|
+
'scramble': "R U R' U'",
|
|
19
|
+
'beam_width': 256,
|
|
20
|
+
'extra_depths': 2,
|
|
21
|
+
'ergonomic_bias': None
|
|
22
|
+
}
|
|
23
|
+
validated_input = Input(**input_data)
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
|
|
27
|
+
The ergonomic bias information is optional and not strictly validated.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from pydantic import BaseModel, validator, model_validator
|
|
32
|
+
from typing import Optional
|
|
33
|
+
import json
|
|
34
|
+
import re
|
|
35
|
+
|
|
36
|
+
class Input(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Input validator (& preprocessor) for AlphaCube.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
format: str # The format of the input data. Must be either 'moves' or 'stickers'.
|
|
42
|
+
scramble: list # The scramble data in the specified format.
|
|
43
|
+
beam_width: int # The beam width for the task. Must be a positive integer.
|
|
44
|
+
extra_depths: int # The number of extra depths for the task. Must be a non-negative integer.
|
|
45
|
+
ergonomic_bias: Optional[dict] # Optional ergonomic bias information (not strictly validated).
|
|
46
|
+
|
|
47
|
+
@validator('format')
|
|
48
|
+
@classmethod
|
|
49
|
+
def validate_format(cls, value: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Validate the input format.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value (str): The format value to be validated.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
str: The validated format value.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If the format is not 'moves' or 'stickers'.
|
|
61
|
+
"""
|
|
62
|
+
if value not in ["moves", "stickers"]:
|
|
63
|
+
raise ValueError("Invalid input format. Must be either 'moves' or 'stickers'.")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
@validator("beam_width")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_beam_width(cls, value):
|
|
69
|
+
"""
|
|
70
|
+
Validate the beam width.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
value: The beam width value to be validated.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
int: The validated beam width value.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ValueError: If the beam width is not a positive integer.
|
|
80
|
+
"""
|
|
81
|
+
if value <= 0:
|
|
82
|
+
raise ValueError("Beam width must be a positive integer.")
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
@validator("extra_depths")
|
|
86
|
+
@classmethod
|
|
87
|
+
def validate_extra_depths(cls, value):
|
|
88
|
+
"""
|
|
89
|
+
Validate the extra depths.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
value: The extra depths value to be validated.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
int: The validated extra depths value.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If the extra depths value is negative.
|
|
99
|
+
"""
|
|
100
|
+
if value < 0:
|
|
101
|
+
raise ValueError("Extra depths must be a non-negative integer.")
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
@model_validator(mode='before')
|
|
105
|
+
@classmethod
|
|
106
|
+
def validate_scramble(cls, values):
|
|
107
|
+
"""
|
|
108
|
+
Validate & preprocess the scramble data based on the chosen format.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
values (dict): The input values.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
dict: The input values with validated scramble data.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If there are invalid moves in 'scramble' for 'moves' format.
|
|
118
|
+
ValueError: If unexpected center-piece configuration in 'scramble' for 'stickers' format (not implemented).
|
|
119
|
+
|
|
120
|
+
Todo:
|
|
121
|
+
Also check for potential orientation, permutation, parity
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
format = values["format"]
|
|
125
|
+
scramble = values["scramble"]
|
|
126
|
+
|
|
127
|
+
if format == "moves":
|
|
128
|
+
if isinstance(scramble, str):
|
|
129
|
+
scramble = scramble.split()
|
|
130
|
+
scramble = [m.replace("2'", "2") for m in scramble]
|
|
131
|
+
invalid_moves = [m for m in scramble if not re.match(r"^[UDLRFBudlrfb'2]{1,2}$", m)] # no wide moves -- yet
|
|
132
|
+
if invalid_moves:
|
|
133
|
+
raise ValueError(f"Invalid move{'s' if len(invalid_moves) > 1 else ''} in `scramble`: {invalid_moves}")
|
|
134
|
+
elif format == "stickers":
|
|
135
|
+
if isinstance(scramble, str):
|
|
136
|
+
scramble = json.loads(scramble.replace("\\\n", ""))
|
|
137
|
+
if isinstance(scramble, dict):
|
|
138
|
+
sticker_colors = [scramble[face] for face in "UDLRBF"]
|
|
139
|
+
# Reset axes if centers are modified
|
|
140
|
+
center_indices = [stickers[4] for stickers in sticker_colors]
|
|
141
|
+
if sorted(center_indices) != list(range(6)):
|
|
142
|
+
raise ValueError('Unexpected center-piece configuration.')
|
|
143
|
+
# Deconstruct a dict of lists to a flat list
|
|
144
|
+
scramble = sum(sticker_colors, [])
|
|
145
|
+
|
|
146
|
+
# Update
|
|
147
|
+
values["scramble"] = scramble
|
|
148
|
+
|
|
149
|
+
return values
|