Superheroes Factory#

This example demostrates how to use the low-sugar in combination with mlflow to solve a simple supply chain problem.

https://mybinder.org/badge_logo.svg
# sphinx_gallery_thumbnail_path = '_static/superheroes.png'

Problem Description#

Populate description here later.

Let’s start by doing some useful imports.

from itertools import product
from typing import Dict
import gurobipy as gp

Tracking an Optimization Experiment#

Add description here.

def build_indices(data: Dict) -> Dict:
    customers = list(data["demand"].keys())
    accessories = list(data["initial_inventory"].keys())
    products = list(data["recipe"].keys())
    max_day = max(demand_details["date"] for demand_details in data["demand"].values())
    days = range(1, max_day + 1)
    customer_dates = {
        (customer, day)
        for customer in customers
        for day in range(
            data["demand"][customer]["date"],
            min(
                data["demand"][customer]["date"] + data["max_delay"],
                max_day + 1,
            ),
        )
    }

    indices = {
        "customers": customers,
        "customer_dates": customer_dates,
        "max_day": max_day,
        "days": days,
        "accessories": accessories,
        "products": products,
    }
    return indices


def build_variables(model: gp.Model, indices: Dict):
    dispatch = model.addVars(indices["customer_dates"], vtype="B", name="dispatch")
    inventory = model.addVars(
        product(indices["accessories"], indices["days"]),
        vtype="C",
        name="inventory",
    )
    from_inventory = model.addVars(
        product(
            indices["customers"],
            indices["products"],
            indices["accessories"],
            indices["days"],
        ),
        vtype="I",
        name="from_inventory",
    )
    from_factory = model.addVars(
        product(
            indices["customers"],
            indices["products"],
            indices["accessories"],
            indices["days"],
        ),
        vtype="I",
        name="from_factory",
    )
    to_inventory = model.addVars(
        product(indices["accessories"], indices["days"]),
        vtype="C",
        name="to_inventory",
    )

    extra_production = model.addVars(
        product(
            indices["customers"],
            indices["products"],
            indices["accessories"],
            indices["days"],
        ),
        vtype="C",
        name="extra_production",
    )

    variables = {
        "dispatch": dispatch,
        "inventory": inventory,
        "from_inventory": from_inventory,
        "from_factory": from_factory,
        "to_inventory": to_inventory,
        "extra_production": extra_production,
    }
    return variables


def build_constraints(model: gp.Model, indices: Dict, variables: Dict) -> gp.Model:
    to_inventory = variables["to_inventory"]
    from_factory = variables["from_factory"]
    from_inventory = variables["from_inventory"]
    inventory = variables["inventory"]
    dispatch = variables["dispatch"]
    extra_production = variables["extra_production"]

    production = data["production"]
    initial_inventory = data["initial_inventory"]
    inventory_capacity = data["inventory_capacity"]
    demand = data["demand"]
    recipe = data["recipe"]

    for accessory, day in product(indices["accessories"], indices["days"]):
        if str(day) in production[accessory]:
            model.addConstr(
                to_inventory[accessory, day]
                + from_factory.sum("*", "*", accessory, day)
                == production[accessory][str(day)],
                name=f"production_allocation_{accessory}_{day}",
            )
        else:
            model.addConstr(
                to_inventory[accessory, day]
                + from_factory.sum("*", "*", accessory, day)
                == 0,
                name=f"production_allocation_{accessory}_{day}",
            )

    for accessory, day in product(indices["accessories"], indices["days"]):
        if day == 1:
            model.addConstr(
                inventory[accessory, day]
                == initial_inventory[accessory]
                + to_inventory[accessory, day]
                - from_inventory.sum("*", "*", accessory, day),
                name=f"keeping_track_of_inventories_{accessory}_{day}",
            )
        else:
            model.addConstr(
                inventory[accessory, day]
                == inventory[accessory, day - 1]
                + to_inventory[accessory, day]
                - from_inventory.sum("*", "*", accessory, day),
                name=f"keeping_track_of_inventories_{accessory}_{day}",
            )

    for accessory, day in product(indices["accessories"], indices["days"]):
        model.addConstr(
            inventory[accessory, day] <= inventory_capacity[accessory],
            name=f"inventory_capacity_{accessory}_{day}",
        )

    for customer in indices["customers"]:
        model.addConstr(
            dispatch.sum(customer, "*") == 1, name=f"customer_served_{customer}"
        )

    for prod, accessory in product(indices["products"], indices["accessories"]):
        for customer, day in indices["customer_dates"]:
            model.addConstr(
                from_inventory[customer, prod, accessory, day]
                + from_factory[customer, prod, accessory, day]
                + extra_production[customer, prod, accessory, day]
                == dispatch[customer, day]
                * demand[customer][prod]
                * recipe[prod][accessory],
                name=f"demand_satisfaction_{customer}_{prod}_{accessory}_{day}",
            )

    return model


def build_objective(
    model: gp.Model, indices: Dict, variables: Dict, demand: Dict
) -> gp.Model:
    dispatch = variables["dispatch"]
    extra_production = variables["extra_production"]

    delay_penalty_costs = gp.quicksum(
        gp.quicksum(
            day * dispatch[customer, day]
            for customer, day in dispatch
            if customer == customer_
        )
        - demand[customer_]["date"]
        for customer_ in indices["customers"]
    )

    cost_per_extra_prod = 2
    extra_production_costs = cost_per_extra_prod * extra_production.sum()

    total_cost = delay_penalty_costs + extra_production_costs

    model.setObjective(total_cost, gp.GRB.MINIMIZE)

    return model


def build(data: Dict) -> gp.Model:
    model = gp.Model(name="supply_chain_blended")
    indices = build_indices(data)
    variables = build_variables(model, indices)
    model = build_constraints(model, indices, variables)
    model = build_objective(model, indices, variables, demand=data["demand"])
    model.update()
    return model

Tracking an Optimization Experiment#

Add description here.

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

# Setting the experiment
import datetime
import logging

logging.getLogger("mlflow").setLevel(logging.CRITICAL)  # Can be set DEBUG

import mlflow
from mlflow import MlflowException

experiment_name = f"superheros_blended_{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

Tracking an Optimization Experiment#

Add description here.

import requests
import matplotlib.pyplot as plt

with mlflow.start_run(experiment_id=experiment_id):
    examples_path = (
        "https://raw.githubusercontent.com/juandados/opt-sugar/main/examples/data"
    )
    data = requests.get(f"{examples_path}/supply_chain_blended_mini_toy.json").json()

    opt_model = low_sugar.Model(build)
    result = opt_model.optimize(data=data)
    from utils.supply_chain import plot_supply_chain
    plot_supply_chain(data=data, solution=result['vars'])
    plt.show()
    model_info = mlflow.sklearn.log_model(opt_model, "supply_chain_blended")
  • plot supply chain blended
  • plot supply chain blended
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 30 rows, 58 columns and 108 nonzeros
Model fingerprint: 0x67d2fc4e
Variable types: 24 continuous, 34 integer (2 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+00]
  Objective range  [1e+00, 2e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 2e+01]
Found heuristic solution: objective 12.0000000
Presolve removed 30 rows and 58 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 2: 0 12

Optimal solution found (tolerance 1.00e-04)
Best objective 0.000000000000e+00, best bound 0.000000000000e+00, gap 0.0000%

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

Gallery generated by Sphinx-Gallery