Mixed Integer Linear Programming with Datamodel#

Consider the following example,

Given system constraints:

2x + 4y >= 230
3x + 2y <= 190
x >= 0, x - Integer
y >= 0, y - Continuous/Floating/non-Integer

Maximize objective function:

f(x) = 5x + 3y

You need to find x and y such a way that it satisfies constraints and maximizes the objective function.

import numpy as np
import cuopt_mps_parser
import solver_settings

from data_model import DataModel
from solver_settings import SolverSettings

problem_data = {}

dm = DataModel()
ss = SolverSettings()

Set Constraint Matrix#

If the constraints are:

2x + 4y >= 230
3x + 2y <= 190

Constraints are depicted in CSR format. The constraints can be transformed to the CSR matrix as follows:

offsets = np.array([0, 2, 4], dtype=np.int32)
indices = np.array([0, 1, 0, 1], dtype=np.int32)
coefficients = np.array([2.0, 4.0, 3.0, 2.0], dtype=np.float64)

dm.set_csr_constraint_matrix(coefficients, indices, offsets)

The offsets indicate the length of the constraint and indices indicate variables.

Set Constraint Bounds#

If the constraints are as follows:

2x + 4y >= 230
3x + 2y <= 190

You need to define upper_bounds and lower_bounds of all the constraints, each value signifies the upper or lower bound of each constraint respective to its index.

upper_bounds = np.array([np.inf, 190], dtype=np.float64)
lower_bounds = np.array([230, -np.inf], dtype=np.float64)

dm.set_constraint_lower_bounds(lower_bounds)
dm.set_constraint_upper_bounds(upper_bounds)

inf - infinity and -inf - negative infinity are used when there is no explict upper or lower bound.

Set Variable Bounds#

Variables:

x >= 0
y >= 0

Define the variable bounds similar to constraint bounds.

var_upper_bounds = np.array([np.inf, np.inf], dtype=np.float64)
var_lower_bounds = np.array([0, 0], dtype=np.float64)

dm.set_variable_lower_bounds(var_lower_bounds)
dm.set_variable_upper_bounds(var_upper_bounds)

Set Objective Data#

Objective:

f(x) = 5x + 3y

Pass coefficents for objective data and also set whether it needs to be maximized or minimized.

objective_coefficients = np.array([5, 3], dtype=np.float64)

dm.set_objective_coefficients(objective_coefficients)
dm.set_maximize(True)

Set Variable Names#

This is optional, but it helps users to navigate the result.

dm.set_variable_names(np.array(["x", "y"]))

Set Variable Types#

Set variable types, “I” - Integer “C” - Continuous

dm.set_variable_types(np.array(["I", "C"]))

Set Solver Configuration#

The solver configuration can be fine-tuned for optimization and runtimes.

ss.set_time_limit(1)

Solve the Problem#

For managed service, cuOpt endpoints can be triggered as shown in the thin client example for managed service.

For self-hosted, cuOpt endpoints can be triggered as shown in the thin client example for self-hosted.

Use this data and invoke the cuOpt endpoint, which would return values for x and y.

The following example is using a locally hosted server:

data = cuopt_mps_parser.toDict(dm)
data["solver_config"] = solver_settings.toDict(ss)
import json
import time
from cuopt_sh_client import CuOptServiceSelfHostClient

# If cuOpt is not running on localhost:5000, edit ip and port parameters
cuopt_service_client = CuOptServiceSelfHostClient(
    ip="localhost",
    port=5000,
    timeout_exception=False
)

data['variable_bounds']['upper_bounds'] = ["inf", "inf"]

repoll_tries = 50

def repoll(solution, repoll_tries):
    # If solver is still busy solving, the job will be assigned a request id and response is sent back in the
    # following format {"reqId": <REQUEST-ID>}. Solver needs to be rei-polled for response using this <REQUEST-ID>.

    if "reqId" in solution and "response" not in solution:
        req_id = solution["reqId"]
        for i in range(repoll_tries):
            solution = cuopt_service_client.repoll(req_id, response_type="dict")
            if "reqId" in solution and "response" in solution:
                break;

            # Sleep for a second before requesting
            time.sleep(1)

    return solution

solution = cuopt_service_client.get_LP_solve(data, response_type="dict")

solution = repoll(solution, repoll_tries)

print(json.dumps(solution, indent=4))

Status - 2 corresponds to Feasible solution is available.

{
    "response": {
        "solver_response": {
            "status": 2,
            "solution": {
                "problem_category": 1,
                "primal_solution": [
                    37.0,
                    39.49999999148369
                ],
                "dual_solution": null,
                "primal_objective": 303.49999997445104,
                "dual_objective": null,
                "solver_time": 1.005017378,
                "vars": {
                    "x": 37.0,
                    "y": 39.49999999148369
                },
                "lp_statistics": {
                    "reduced_cost": null
                },
                "milp_statistics": {
                    "mip_gap": -303.49999997445104,
                    "solution_bound": -2.0,
                    "presolve_time": 0.042006453,
                    "max_constraint_violation": 0.0,
                    "max_int_violation": 0.0,
                    "max_variable_bound_violation": 0.0
                }
            }
        },
        "total_solve_time": 1.0515711307525635
    },
    "reqId": "fc2871c8-d3e8-410a-8a32-b540d572054c"
}