Skip to content

Solver Manager

noqx.manager

The unified manager for solvers.

Classes:

  • Solver

    Base class to create solvers.

Functions:

  • load_solver

    Load a solver from a valid directory and record the solver module to a dictionary.

  • list_solver_metadata

    Generate A dictionary containing the attributes for each solver.

  • prepare_puzzle

    Convert the raw puzzle from Penpa+ format to a Puzzle object.

  • generate_program

    Generate the solver program in the Answer Set Programming language.

  • store_solution

    Convert the solution from Clingo to a Puzzle object and refine it.

Solver

Base class to create solvers.

  • New solvers should inherit this class and set appropriate attribute values.

Attributes:

  • name (str) –

    The name of the solver.

  • category (str) –

    The category of the solver, should be shade (Shading), loop (Loop / Path), region (Area Division), num (Number), var (Variety), draw (Drawing), unk (Unknown).

  • aliases (List[str] = []) –

    A list of alternative names for the solver.

  • examples (List[Dict[str, Any]] = []) –

    A list of examples of the solver, each example can be created in two conflicting ways, data and url:

    • data: directly draw the board in noqx and get the data URL by using Share → Editing URL → Copy. The URL are suggested to be generated with the following conditions:
      • contains all the required modes in this puzzle.
      • contains the required sub-types in this puzzle.
      • contains necessary initial conditions to pass the coverage tests.
      • set edit mode to solution mode instead of problem mode.
    • url: draw the board in puzz.link and use File → Export URL to get the board URL.
    • config (Optional): the configuration of the solver, which will be passed to the solver when it is created, and the keys of config are the same as parameters keys.
    • test (Optional): whether the example is used as test case, the default value is True, and cannot be used together with url way.
    • Lots of examples can be found at pzplus.
  • parameters (Dict[str, Any]) –

    A dictionary of parameters of the solver, which will be passed to the solver when it is created.

Warning

When you directly draw the board in noqx, make sure to set the puzzle type first. Currently, the puzzle type selection is locked if the user starts drawing the board.

Methods:

  • __init__

    Initialize an internal program.

  • add_program_line

    Add a line to the internal program.

  • reset

    Clear the internal program.

  • solve

    Generate the solver program in the Answer Set Programming language.

  • refine

    Refine the solution.

Source code in noqx/manager.py
class Solver:
    """Base class to create solvers.

    * New solvers should inherit this class and set appropriate attribute values.

    Attributes:
        name: The name of the solver.
        category: The category of the solver, should be `shade` (Shading), `loop` (Loop / Path), `region` (Area Division), `num` (Number), `var` (Variety), `draw` (Drawing), `unk` (Unknown).
        aliases (List[str] = []): A list of alternative names for the solver.
        examples (List[Dict[str, Any]] = []): A list of examples of the solver, each example can be created in two conflicting ways, `data` and `url`:

            * `data`: directly draw the board in noqx and get the data URL by using `Share → Editing URL → Copy`. The URL are suggested to be generated with the following conditions:
                * contains **all** the required `modes` in this puzzle.
                * contains the required `sub-types` in this puzzle.
                * contains necessary initial conditions to pass the coverage tests.
                * set `edit mode` to `solution mode` instead of `problem mode`.
            * `url`: draw the board in [puzz.link](https://puzz.link/list.html) and use `File → Export URL` to get the board URL.
            * `config` (Optional): the configuration of the solver, which will be passed to the solver when it is created, and the keys of `config` are the same as `parameters` keys.
            * `test` (Optional): whether the example is used as test case, the default value is `True`, and cannot be used together with `url` way.
            * **Lots of** examples can be found at [pzplus](https://pzplus.tck.mn/db).

        parameters: A dictionary of parameters of the solver, which will be passed to the solver when it is created.

    Warning:
        When you directly draw the board in noqx, make sure to set the puzzle type first. Currently, the puzzle type selection is **locked** if the user starts drawing the board.
    """

    def __init__(self):
        """Initialize an internal program."""
        self._program: List[str] = []

    def add_program_line(self, line: str):
        """Add a line to the internal program.

        Args:
            line: A line in the Answer Set Programming language.
        """
        if line != "":
            self._program.append(line.strip())

    @property
    def program(self) -> str:
        """Convert the internal program in the Answer Set Programming language."""
        return "\n".join(self._program)

    def reset(self):
        """Clear the internal program.

        * Make sure to reset the program before generating a new one.
        """
        self._program.clear()

    def solve(self, _: Puzzle) -> str:
        """Generate the solver program in the Answer Set Programming language.

        * New solvers should at least implement this method.

        Args:
            _: A `Puzzle` object for the program.

        Raises:
            NotImplementedError: If this method is not implemented.
        """
        raise NotImplementedError("Solver program not implemented.")

    def refine(self, solution: Puzzle) -> Puzzle:
        """Refine the solution.

        * The default operation is passing the original `solution` object to the output.

        Args:
            solution: A `Puzzle` object to be refined.
        """
        return solution

    name: str = "Unknown"
    category: str = "unk"
    aliases: List[str] = []
    examples: List[Dict[str, Any]] = []
    parameters: Dict[str, Any] = {}

program property

program: str

Convert the internal program in the Answer Set Programming language.

__init__

__init__()

Initialize an internal program.

Source code in noqx/manager.py
def __init__(self):
    """Initialize an internal program."""
    self._program: List[str] = []

add_program_line

add_program_line(line: str)

Add a line to the internal program.

Parameters:

  • line
    (str) –

    A line in the Answer Set Programming language.

Source code in noqx/manager.py
def add_program_line(self, line: str):
    """Add a line to the internal program.

    Args:
        line: A line in the Answer Set Programming language.
    """
    if line != "":
        self._program.append(line.strip())

reset

reset()

Clear the internal program.

  • Make sure to reset the program before generating a new one.
Source code in noqx/manager.py
def reset(self):
    """Clear the internal program.

    * Make sure to reset the program before generating a new one.
    """
    self._program.clear()

solve

solve(_: Puzzle) -> str

Generate the solver program in the Answer Set Programming language.

  • New solvers should at least implement this method.

Parameters:

  • _
    (Puzzle) –

    A Puzzle object for the program.

Raises:

  • NotImplementedError

    If this method is not implemented.

Source code in noqx/manager.py
def solve(self, _: Puzzle) -> str:
    """Generate the solver program in the Answer Set Programming language.

    * New solvers should at least implement this method.

    Args:
        _: A `Puzzle` object for the program.

    Raises:
        NotImplementedError: If this method is not implemented.
    """
    raise NotImplementedError("Solver program not implemented.")

refine

refine(solution: Puzzle) -> Puzzle

Refine the solution.

  • The default operation is passing the original solution object to the output.

Parameters:

  • solution
    (Puzzle) –

    A Puzzle object to be refined.

Source code in noqx/manager.py
def refine(self, solution: Puzzle) -> Puzzle:
    """Refine the solution.

    * The default operation is passing the original `solution` object to the output.

    Args:
        solution: A `Puzzle` object to be refined.
    """
    return solution

load_solver

load_solver(solver_dir: str, solver_name: str) -> None

Load a solver from a valid directory and record the solver module to a dictionary.

Parameters:

  • solver_dir

    (str) –

    The directory where the solver is located.

  • solver_name

    (str) –

    The name of the solver to load.

Raises:

  • ValueError

    If the solver already exists.

  • ImportError

    If the solver module cannot be imported.

Source code in noqx/manager.py
def load_solver(solver_dir: str, solver_name: str) -> None:
    """Load a solver from a valid directory and record the solver module to a dictionary.

    Args:
        solver_dir: The directory where the solver is located.
        solver_name: The name of the solver to load.

    Raises:
        ValueError: If the solver already exists.
        ImportError: If the solver module cannot be imported.
    """
    if solver_name in modules:
        raise ValueError(f"Solver for {solver_name} already exists.")

    module = __import__(f"{solver_dir}.{solver_name}")
    module_attr = getattr(module, solver_name)

    for attr_name in dir(module_attr):
        attr = getattr(module_attr, attr_name)

        if isinstance(attr, type) and issubclass(attr, Solver) and attr is not Solver:
            puzzle_name = solver_name.lower()
            modules[puzzle_name] = attr()

list_solver_metadata

list_solver_metadata() -> Dict[str, Any]

Generate A dictionary containing the attributes for each solver.

Source code in noqx/manager.py
def list_solver_metadata() -> Dict[str, Any]:
    """Generate A dictionary containing the attributes for each solver."""
    metadata = {}
    for puzzle_name, module in modules.items():
        metadata[puzzle_name] = {
            "name": module.name,
            "category": module.category,
            "aliases": module.aliases,
            "examples": module.examples,
            "parameters": module.parameters,
        }

    return metadata

prepare_puzzle

prepare_puzzle(puzzle_name: str, puzzle_content: str, param: Dict[str, Any]) -> Puzzle

Convert the raw puzzle from Penpa+ format to a Puzzle object.

Parameters:

  • puzzle_name

    (str) –

    The name of the puzzle.

  • puzzle_content

    (str) –

    The puzzle content exported in Penpa+ format.

  • param

    (Dict[str, Any]) –

    Additional parameters for the puzzle.

Source code in noqx/manager.py
def prepare_puzzle(puzzle_name: str, puzzle_content: str, param: Dict[str, Any]) -> Puzzle:
    """Convert the raw puzzle from [Penpa+](https://swaroopg92.github.io/penpa-edit/) format to a `Puzzle` object.

    Args:
        puzzle_name: The name of the puzzle.
        puzzle_content: The puzzle content exported in [Penpa+](https://swaroopg92.github.io/penpa-edit/) format.
        param: Additional parameters for the puzzle.
    """
    puzzle = PenpaPuzzle(puzzle_name, puzzle_content, param)
    puzzle.decode()

    return puzzle

generate_program

generate_program(puzzle: Puzzle) -> str

Generate the solver program in the Answer Set Programming language.

  • The program generator is based on the puzzle name and the corresponding solver module.

Parameters:

  • puzzle

    (Puzzle) –

    A Puzzle object for the program.

Source code in noqx/manager.py
def generate_program(puzzle: Puzzle) -> str:
    """Generate the solver program in the Answer Set Programming language.

    * The program generator is based on the puzzle name and the corresponding solver module.

    Args:
        puzzle: A `Puzzle` object for the program.
    """
    module = modules[puzzle.puzzle_name]
    return module.solve(puzzle)

store_solution

store_solution(puzzle: Puzzle, model_str: str) -> Puzzle

Convert the solution from Clingo to a Puzzle object and refine it.

  • The solution refiner is based on the puzzle name and the corresponding solver module. It does nothing by default.

  • Since the solution from Clingo is in a raw string format, this function will parse the string and fill in the corresponding attributes of the Puzzle object. The parsing order will be: edges, lines, texts (numbers/contents), triangle symbols, colors, other symbols and debugging elements.

Parameters:

  • puzzle

    (Puzzle) –

    A Puzzle object without stored solution.

  • model_str

    (str) –

    The raw solution string generated by the Clingo solver.

Source code in noqx/manager.py
def store_solution(puzzle: Puzzle, model_str: str) -> Puzzle:
    """Convert the solution from [Clingo](https://potassco.org/clingo/) to a `Puzzle` object and refine it.

    * The solution refiner is based on the puzzle name and the corresponding solver module. It does nothing by default.

    * Since the solution from [Clingo](https://potassco.org/clingo/) is in a raw string format, this function will parse the string and fill in the corresponding attributes of the `Puzzle` object. The parsing order will be: edges, lines, texts (numbers/contents), triangle symbols, colors, other symbols and debugging elements.

    Args:
        puzzle: A `Puzzle` object without stored solution.
        model_str: The raw solution string generated by the [Clingo](https://potassco.org/clingo/) solver.
    """
    module = modules[puzzle.puzzle_name]

    solution_data = tuple(str(model_str).split())  # raw solution converted from clingo
    solution = PenpaPuzzle(puzzle.puzzle_name, puzzle.content, puzzle.param)
    solution.decode()
    solution.clear()

    for item in solution_data:
        _type, _data = item.replace("(", " ").replace(")", " ").split()
        data = _data.split(",")

        r, c = tuple(map(int, data[:2]))  # ensure the first two elements of data is the row and column

        if _type == "edge":
            d = str(data[2]).replace('"', "")
            solution.edge[Point(r, c, d)] = True

        elif _type.startswith("line_"):
            d = str(data[2]).replace('"', "")
            if puzzle.puzzle_name == "hashi" and str(data[3]) == "2":
                solution.line[Point(r, c, d, "double")] = True
            else:
                solution.line[Point(r, c, d)] = True

        elif _type.startswith("number"):
            solution.text[Point(r, c, Direction.CENTER, "normal")] = int(data[2])

        elif _type == "triangle":
            shaka_dict = {
                f'"{Direction.TOP_LEFT}"': "1",
                f'"{Direction.TOP_RIGHT}"': "4",
                f'"{Direction.BOTTOM_LEFT}"': "2",
                f'"{Direction.BOTTOM_RIGHT}"': "3",
            }
            solution.symbol[Point(r, c, Direction.CENTER)] = f"tri__{shaka_dict[data[2]]}"

        elif _type == "gray":
            solution.surface[Point(r, c)] = Color.GRAY
        elif _type == "black":
            solution.surface[Point(r, c)] = Color.BLACK
        elif _type == "blue":
            solution.surface[Point(r, c)] = Color.BLUE

        elif len(data) == 2:
            solution.symbol[Point(r, c, Direction.CENTER)] = str(_type)

        else:  # pragma: no cover
            solution.text[Point(r, c, Direction.CENTER, "normal")] = int(data[2])  # for debugging

    module.refine(solution)
    return solution