h48

A prototype for an optimal Rubik's cube solver, work in progress.
git clone https://git.tronto.net/h48
Download | Log | Files | Refs | README | LICENSE

commit a3a477870cdbdb4b9c4c8b403c09b13502291234
parent b80b1b78901154240bc5d2ab6f5ea4465bf7fa67
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Sat, 19 Oct 2024 19:17:13 +0200

Finished Python module

Diffstat:
MREADME.md | 10++++++++--
Apython/example.py | 37+++++++++++++++++++++++++++++++++++++
Mpython/nissy_module.c | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/nissy.c | 13+++++++++----
Msrc/solvers/tables.h | 3++-
5 files changed, 304 insertions(+), 17 deletions(-)

diff --git a/README.md b/README.md @@ -182,8 +182,14 @@ From here you can call the library functions directly, for example: 'ASTUGFBH=DACXEZGBLIKF' ``` -Please note: as this is work in progress, not all functions are currently -available from the Python module. +Some methods have a different signature in the Pythn module than in +libnissy: for example `solve()` returns the solutions as a list of +strings instead of writing them to a return parameter buffer. +You can access the documentation for the Python module from within +a Python interpreter with `help(nissy)`. Cross-check this documentation +with the comments in nissy.h for more details. + +Please note: the support for the Python module is still rudimentary. ## Cube formats diff --git a/python/example.py b/python/example.py @@ -0,0 +1,37 @@ +# Small example of nissy_python_module usage + +# Compile the python module with "make python", then run this from the main +# folder containing nissy_python_module.so + +# Append the main folder to the python path so we can load the module +from sys import path +path.append("./") + +# Import with a nicer name +import nissy_python_module as nissy + +# Choose the solver you prefer +solver = "h48h0k4" + +# Load the pruning table from file +data = bytearray(open("tables/" + solver, "rb").read()) + +# If you have not generated the table yet, you can do so: +# data = nissy.gendata("h48h0k4") + +# Get a scrambled cube +cube = nissy.applymoves(nissy.solved_cube, "U F R2"); + +# Solve! +solutions = nissy.solve(cube, solver, nissy.nissflag_normal, 0, 9, 3, -1, data) + +# Print the solutions, one per line +print("Found ", len(solutions), " solutions:") +for s in solutions: + print(s) + +# You can use help(nissy) for more info about the available methods: +# help(nissy) + +# Or help(nissy.methodname) if you want to know more about a specific method: +# help(nissy.solve) diff --git a/python/nissy_module.c b/python/nissy_module.c @@ -5,12 +5,17 @@ #include "../src/nissy.h" #define MAX_CUBE_STR_LEN 1024 /* Update when adding formats */ +#define MAX_SOLUTIONS_SIZE 250000 static bool check_error(long long err) { char err_string[255]; + /* A positive value always denotes a success */ + if (err > 0) + return true; + switch (err) { case NISSY_OK: /* Fallthrough */ case NISSY_WARNING_UNSOLVABLE: @@ -58,6 +63,18 @@ long_result(long long result) return PyLong_FromLong(result); } + +PyDoc_STRVAR(compose_doc, +"compose(cube, permutation)\n" +"--\n\n" +"Apply 'permutation' on 'cube'.\n" +"\n" +"Parameters:\n" +" - cube: a cube in B32 format\n" +" - permutation: another cube in B32 format\n" +"\n" +"Returns: the resulting cube string in B32 format\n" +); static PyObject * compose(PyObject *self, PyObject *args) { @@ -72,6 +89,16 @@ compose(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(inverse_doc, +"inverse(cube)\n" +"--\n\n" +"Invert 'cube'.\n" +"\n" +"Parameters:\n" +" - cube: a cube in B32 format\n" +"\n" +"Returns: the inverse cube in B32 format\n" +); static PyObject * inverse(PyObject *self, PyObject *args) { @@ -86,6 +113,17 @@ inverse(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(applymoves_doc, +"applymoves(cube, moves)\n" +"--\n\n" +"Apply 'moves' to 'cube'.\n" +"\n" +"Parameters:\n" +" - cube: a cube in B32 format\n" +" - moves: the moves to apply on the cube\n" +"\n" +"Returns: the resulting cube in B32 format\n" +); static PyObject * applymoves(PyObject *self, PyObject *args) { @@ -100,6 +138,19 @@ applymoves(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(applytrans_doc, +"applytrans(cube, transformation)\n" +"--\n\n" +"Apply 'transformation' to 'cube'.\n" +"\n" +"Parameters:\n" +" - cube: a cube in B32 format\n" +" - transformation: the transformation to apply on the cube, formatted as\n" +" (rotation|mirrored) (2 letters)\n" +" for example 'mirrored ur' or 'rotation lf'\n" +"\n" +"Returns: the resulting cube in B32 format\n" +); static PyObject * applytrans(PyObject *self, PyObject *args) { @@ -114,6 +165,18 @@ applytrans(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(convert_doc, +"convert(fin, fout, cube)\n" +"--\n\n" +"Convert 'cube' from format 'fin' to format 'fout'.\n" +"\n" +"Parameters:\n" +" - fin: the format in which 'cube' is given\n" +" - fout: the format to which 'cube' has to be converted\n" +" - cube: a cube in B32 format\n" +"\n" +"Returns: 'cube' in 'fout' format\n" +); static PyObject * convert(PyObject *self, PyObject *args) { @@ -128,6 +191,20 @@ convert(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(getcube_doc, +"getcube(ep, eo, cp, co, options)\n" +"--\n\n" +"Constructs the cube from the given coordinates and options\n" +"\n" +"Parameters:\n" +" - ep: the edge permutation coordinate\n" +" - eo: the edge orientation coordinate\n" +" - cp: the corner permutation coordinate\n" +" - co: the corner orientation coordinate\n" +" - options: a string, for example \"fix\"\n" +"\n" +"Returns: the cube constructed from the given coordinates\n" +); static PyObject * getcube(PyObject *self, PyObject *args) { @@ -142,6 +219,16 @@ getcube(PyObject *self, PyObject *args) return string_result(err, result); } +PyDoc_STRVAR(datasize_doc, +"datasize(solver)\n" +"--\n\n" +"Returns the size of the data (pruning table) for the given solver\n" +"\n" +"Parameters:\n" +" - solver: the name of the solver\n" +"\n" +"Returns: the size of the data for the solver, in bytes\n" +); static PyObject * datasize(PyObject *self, PyObject *args) { @@ -155,23 +242,168 @@ datasize(PyObject *self, PyObject *args) return long_result(result); } -/* TODO: gendata, checkdata and solve and countmoves */ +PyDoc_STRVAR(gendata_doc, +"gendata(solver)\n" +"--\n\n" +"Generates the data (pruning table) for the given solver\n" +"\n" +"Parameters:\n" +" - solver: the name of the solver\n" +"\n" +"Returns: a bytearray containing the data for the solver\n" +); +static PyObject * +gendata(PyObject *self, PyObject *args) +{ + long long size, err; + const char *solver; + char *buf; + + if (!PyArg_ParseTuple(args, "s", &solver)) + return NULL; + + size = nissy_datasize(solver); + if (!check_error(size)) + return NULL; + + buf = PyMem_Malloc(size); + + Py_BEGIN_ALLOW_THREADS + err = nissy_gendata(solver, size, buf); + Py_END_ALLOW_THREADS + + if (check_error(err)) + return PyByteArray_FromStringAndSize(buf, size); + else + return NULL; +} + +PyDoc_STRVAR(checkdata_doc, +"checkdata(data)\n" +"--\n\n" +"Checks if the data (pruning table) given is valid or not\n" +"\n" +"Parameters:\n" +" - data: a bytearray containing the data for a solver" +"\n" +"Returns: true if the data is valid, false otherwise\n" +); +PyObject * +checkdata(PyObject *self, PyObject *args) +{ + long long result; + PyByteArrayObject *data; + + if (!PyArg_ParseTuple(args, "Y", &data)) + return NULL; + + result = nissy_checkdata(data->ob_alloc, data->ob_bytes); + + if (check_error(result)) + return Py_True; + else + return Py_False; +} + +PyDoc_STRVAR(solve_doc, +"solve(cube, solver, nissflag, minmoves, maxmoves, maxsolutions, optimal, data)\n" +"--\n\n" +"Solves the given 'cube' with the given 'solver' and other parameters." +"See the documentation for libnissy (in nissy.h) for details.\n" +"\n" +"Parameters:\n" +" - cube: a cube in B32 format\n" +" - solver: the solver to use\n" +" - minmoves: the minimum number of moves to use\n" +" - maxmoves: the maximum number of moves to use\n" +" - maxsolution: the maximum number of solutions to return\n" +" - optimal: the largest number of moves from the shortest solution" +"(set to -1 to ignore)\n" +" - data: a bytearray containing the data for the solver\n" +"\n" +"Returns: a list with the solutions found\n" +); +PyObject * +solve(PyObject *self, PyObject *args) +{ + long long result; + unsigned nissflag, minmoves, maxmoves, maxsolutions; + int optimal, i, j, k; + const char *cube, *solver; + char solutions[MAX_SOLUTIONS_SIZE]; + long long stats[NISSY_SIZE_SOLVE_STATS]; + PyByteArrayObject *data; + PyObject *list, *item; + + if (!PyArg_ParseTuple(args, "ssIIIIiY", &cube, &solver, &nissflag, + &minmoves, &maxmoves, &maxsolutions, &optimal, &data)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + result = nissy_solve(cube, solver, nissflag, minmoves, maxmoves, + maxsolutions, optimal, data->ob_alloc, data->ob_bytes, + MAX_SOLUTIONS_SIZE, solutions, stats); + Py_END_ALLOW_THREADS + + if(!check_error(result)) { + return NULL; + } else { + list = PyList_New(result); + for (i = 0, j = 0, k = 0; solutions[i] != 0; i++) { + if (solutions[i] != '\n') + continue; + solutions[i] = 0; + item = PyUnicode_FromString(&solutions[k]); + PyList_SetItem(list, j, item); + j++; + k = i+1; + } + return list; + } +} + +PyDoc_STRVAR(countmoves_doc, +"countmoves(moves)\n" +"--\n\n" +"Count the moves\n" +"\n" +"Parameters:\n" +" - moves: the moves to be counted\n" +"\n" +"Returns: the number of moves in HTM metric\n" +); +PyObject * +countmoves(PyObject *self, PyObject *args) +{ + long long count; + const char *moves; + + if (!PyArg_ParseTuple(args, "s", &moves)) + return NULL; + + count = nissy_countmoves(moves); + return long_result(count); +} static PyMethodDef nissy_methods[] = { - { "compose", compose, METH_VARARGS, NULL }, - { "inverse", inverse, METH_VARARGS, NULL }, - { "applymoves", applymoves, METH_VARARGS, NULL }, - { "applytrans", applytrans, METH_VARARGS, NULL }, - { "convert", convert, METH_VARARGS, NULL }, - { "getcube", getcube, METH_VARARGS, NULL }, - { "datasize", datasize, METH_VARARGS, NULL }, + { "compose", compose, METH_VARARGS, compose_doc }, + { "inverse", inverse, METH_VARARGS, inverse_doc }, + { "applymoves", applymoves, METH_VARARGS, applymoves_doc }, + { "applytrans", applytrans, METH_VARARGS, applytrans_doc }, + { "convert", convert, METH_VARARGS, convert_doc }, + { "getcube", getcube, METH_VARARGS, getcube_doc }, + { "datasize", datasize, METH_VARARGS, datasize_doc }, + { "gendata", gendata, METH_VARARGS, gendata_doc }, + { "checkdata", checkdata, METH_VARARGS, checkdata_doc }, + { "solve", solve, METH_VARARGS, solve_doc }, + { "countmoves", countmoves, METH_VARARGS, countmoves_doc }, { NULL, NULL, 0, NULL } }; static struct PyModuleDef nissy_python_module = { .m_base = PyModuleDef_HEAD_INIT, .m_name = "nissy", - .m_doc = NULL, + .m_doc = "python module for libnissy", .m_size = -1, .m_methods = nissy_methods, .m_slots = NULL, @@ -195,7 +427,13 @@ PyMODINIT_FUNC PyInit_nissy_python_module(void) { nissy_setlogger(log_stdout); module = PyModule_Create(&nissy_python_module); - PyModule_AddStringConstant(module, "solvedcube", NISSY_SOLVED_CUBE); + + PyModule_AddStringConstant(module, "solved_cube", NISSY_SOLVED_CUBE); + PyModule_AddIntConstant(module, "nissflag_normal", NISSY_NISSFLAG_NORMAL); + PyModule_AddIntConstant(module, "nissflag_inverse", NISSY_NISSFLAG_INVERSE); + PyModule_AddIntConstant(module, "nissflag_mixed", NISSY_NISSFLAG_MIXED); + PyModule_AddIntConstant(module, "nissflag_linear", NISSY_NISSFLAG_LINEAR); + PyModule_AddIntConstant(module, "nissflag_all", NISSY_NISSFLAG_ALL); return module; } diff --git a/src/nissy.c b/src/nissy.c @@ -71,7 +71,10 @@ checkdata(const char *buf, const tableinfo_t *info) { uint64_t distr[INFO_DISTRIBUTION_LEN]; - if (!strncmp(info->solver, "cocsep", 6)) { + if (info == NULL) { + LOG("checkdata: error reading table info\n"); + return false; + } else if (!strncmp(info->solver, "cocsep", 6)) { getdistribution_cocsep( (uint32_t *)((char *)buf + INFOSIZE), distr); } else if (!strncmp(info->solver, "h48", 3)) { @@ -442,21 +445,23 @@ nissy_checkdata( { char *buf; tableinfo_t info; + int64_t err; for (buf = (char *)data; - readtableinfo(data_size, buf, &info); + (err = readtableinfo(data_size, buf, &info)) == NISSY_OK; buf += info.next, data_size -= info.next) { if (!checkdata(buf, &info)) { - LOG("Error: data for %s is inconsistent with info!\n", + LOG("Error: data for solver '%s' is corrupted!\n", info.solver); return NISSY_ERROR_DATA; } + if (info.next == 0) break; } - return NISSY_OK; + return err; } long long diff --git a/src/solvers/tables.h b/src/solvers/tables.h @@ -35,7 +35,8 @@ readtableinfo(uint64_t buf_size, const char *buf, tableinfo_t *info) if (buf_size < INFOSIZE) { LOG("Error reading table: buffer size is too small " - "(smaller than INFOSIZE = %" PRId64 ")\n", INFOSIZE); + "(given size %" PRIu64 " is smaller than INFOSIZE = %" + PRId64 ")\n", buf_size, INFOSIZE); return NISSY_ERROR_BUFFER_SIZE; }