Solving Sudokus Like a Master#

This example demostrates how to use the low-sugar in combination with mlflow to solve sudoku puzzles.

https://mybinder.org/badge_logo.svg
# sphinx_gallery_thumbnail_path = '_static/sudoku.png'
from itertools import product
import datetime
import numpy as np

import mlflow
from mlflow import MlflowException
import pandas as pd

import gurobipy as gp

# import sys; sys.path.append("/Users/Juan.ChaconLeon/opt/opt-sugar/src")  # when running locally
from opt_sugar import low_sugar
from opt_sugar import opt_flow
from utils.sudoku import show_sudoku


experiment_name = f"sudoku_{datetime.datetime.now().strftime('%Y_%m_%d')}"
try:
    experiment_id = mlflow.create_experiment(name=experiment_name)
except MlflowException:
    experiment_id = mlflow.get_experiment_by_name(name=experiment_name).experiment_id

with mlflow.start_run(experiment_id=experiment_id):

    def build(data):
        # Create a new model
        m = gp.Model("sudoku")

        # Create Indices
        pos_ys = list(range(3))
        pos_xs = list(range(3))
        square_ys = list(range(3))
        square_xs = list(range(3))
        positions = list(product(pos_ys, pos_xs, square_ys, square_xs))

        digits = list(range(1, 10))
        indices = list((*pos, digit) for pos, digit in product(positions, digits))

        # Create variables
        digit_pick = m.addVars(indices, vtype="B", name="digit_pick")

        # Set objective: This is a feasibility problem rather than on optimization one.
        m.setObjective(0)

        # Add constraints:
        for pos_y, square_y, digit in product(pos_ys, square_ys, digits):
            m.addConstr(
                digit_pick.sum(pos_y, "*", square_y, "*", digit) == 1,
                name=f"row_{pos_y}_{square_y}_{digit}",
            )

        for pos_x, square_x, digit in product(pos_xs, square_xs, digits):
            m.addConstr(
                digit_pick.sum("*", pos_x, "*", square_x, digit) == 1,
                name=f"col_{pos_x}_{square_x}_{digit}",
            )

        for square_y, square_x, digit in product(square_ys, square_xs, digits):
            m.addConstr(
                digit_pick.sum("*", "*", square_y, square_x, digit) == 1,
                name=f"square_{square_y}_{square_x}_{digit}",
            )

        for pos_y, pos_x, square_y, square_x in positions:
            y = square_y * 3 + pos_y
            x = square_x * 3 + pos_x
            chosen_digit = data[y][x]
            if chosen_digit:
                chosen_digit = int(chosen_digit)
                m.addConstr(
                    digit_pick[pos_y, pos_x, square_y, square_x, chosen_digit] == 1,
                    name=f"chosen_{pos_y}_{pos_x}_{square_y}_{square_x}_{chosen_digit}",
                )

        for pos_y, pos_x, square_y, square_x in positions:
            m.addConstr(
                digit_pick.sum(pos_y, pos_x, square_y, square_x, "*") == 1,
                name=f"position_{pos_y}_{pos_x}_{square_y}_{square_x}_chosen",
            )

        return m

    examples_path = (
        "https://raw.githubusercontent.com/juandados/opt-sugar/main/examples/data"
    )
    data = (
        pd.read_csv(f"{examples_path}/sudoku_1.csv", header=None)
        .replace({np.NaN: None})
        .values
    )

    # Building
    opt_model = low_sugar.Model(build)
    result = opt_model.optimize(data=data)
    show_sudoku(vars=result["vars"]["digit_pick"])
    model_info = mlflow.sklearn.log_model(opt_model, "opt_model")
Gurobi Optimizer version 9.5.2 build v9.5.2rc0 (linux64)
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads
Optimize a model with 360 rows, 729 columns and 2952 nonzeros
Model fingerprint: 0x2d3b6756
Variable types: 0 continuous, 729 integer (729 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 1e+00]
Presolve removed 360 rows and 729 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 1 (of 2 available processors)

Solution count 1: 0

Optimal solution found (tolerance 1.00e-04)
Best objective 0.000000000000e+00, best bound 0.000000000000e+00, gap 0.0000%
[[1, 6, 3, 5, 8, 4, 2, 9, 7],
 [2, 9, 8, 1, 7, 6, 3, 5, 4],
 [5, 7, 4, 2, 3, 9, 8, 6, 1],
 [6, 3, 2, 7, 9, 5, 1, 4, 8],
 [8, 4, 7, 6, 1, 2, 5, 3, 9],
 [9, 5, 1, 8, 4, 3, 6, 7, 2],
 [7, 2, 5, 4, 6, 1, 9, 8, 3],
 [3, 8, 6, 9, 2, 7, 4, 1, 5],
 [4, 1, 9, 3, 5, 8, 7, 2, 6]]
2022/12/12 20:58:36 WARNING mlflow.utils.requirements_utils: The following packages were not found in the public PyPI package index as of 2022-09-16; if these packages are not present in the public PyPI index, you must install them manually before loading your model: {'opt-sugar'}

Load the Registered Model and Optimize with new Data#

Add description here.

logged_model_uri = model_info.model_uri
print(f"logged_model_uri: {logged_model_uri}")

# Load model as a PyFuncModel.
loaded_model = opt_flow.pyfunc.load_model(logged_model_uri)

examples_path = (
    "https://raw.githubusercontent.com/juandados/opt-sugar/main/examples/data"
)
data = (
    pd.read_csv(f"{examples_path}/sudoku_2.csv", header=None)
    .replace({np.NaN: None})
    .values
)
result = loaded_model.optimize(data=data)

# Using a util function (Check imports)
show_sudoku(vars=result["vars"]["digit_pick"])
print(f"solution from the registered model {result['objective_value']}")
logged_model_uri: runs:/0f3a1115ace644968d41954e158f65d8/opt_model
Gurobi Optimizer version 9.5.2 build v9.5.2rc0 (linux64)
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads
Optimize a model with 348 rows, 729 columns and 2940 nonzeros
Model fingerprint: 0x5f5d2d9a
Variable types: 0 continuous, 729 integer (729 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 1e+00]
Presolve removed 348 rows and 729 columns
Presolve time: 0.00s
Presolve: All rows and columns removed

Explored 0 nodes (0 simplex iterations) in 0.00 seconds (0.00 work units)
Thread count was 1 (of 2 available processors)

Solution count 1: 0

Optimal solution found (tolerance 1.00e-04)
Best objective 0.000000000000e+00, best bound 0.000000000000e+00, gap 0.0000%
[[2, 8, 9, 5, 6, 7, 3, 1, 4],
 [7, 5, 3, 4, 1, 8, 6, 2, 9],
 [1, 4, 6, 9, 3, 2, 5, 7, 8],
 [8, 9, 5, 1, 4, 3, 2, 6, 7],
 [3, 7, 2, 6, 8, 5, 4, 9, 1],
 [4, 6, 1, 7, 2, 9, 8, 5, 3],
 [9, 1, 8, 3, 5, 6, 7, 4, 2],
 [5, 3, 4, 2, 7, 1, 9, 8, 6],
 [6, 2, 7, 8, 9, 4, 1, 3, 5]]
solution from the registered model 0.0
https://mybinder.org/badge_logo.svg

Total running time of the script: ( 0 minutes 1.695 seconds)

Gallery generated by Sphinx-Gallery