User guide · 2.0
Notebook 1 - Getting Started¶
In this notebook, you will learn the fundamentals of SHOTpy. By the end, you will be comfortable defining and solving an optimization problem.
Table of Contents¶
1. Installation¶
First, install SHOTpy, then import the module and create a solver.
%pip install SHOTpy
import SHOTpy
# Create a SHOT solver
solver = SHOTpy.Solver()
print("SHOT solver created successfully!")
SHOT solver created successfully!
2. Creating Variables¶
Variables are the fundamental building blocks of optimization problems. SHOTpy supports:
Real (continuous) variables
Integer variables
Binary variables
The most convenient way to create a variable is to create it and add it to a problem in one step:
var_name = problem.addVariable("name", SHOTpy.VariableType.<Type>, lower_bound, upper_bound)
name(str): Human-readable identifier shown by the solver and in prints.<Type>: Variable kind —Real,Integer, orBinary.lower_bound(float, optional): Minimum allowed value.upper_bound(float, optional): Maximum allowed value.
Without bounds, a binary variable has the bounds 0 and 1, and other variables are unbounded. A bound of float("inf") or -float("inf") also means that there is no bound.
A variable can also be created on its own with SHOTpy.Variable("name", SHOTpy.VariableType.<Type>, lower_bound, upper_bound) and added later with problem.addVariable(var). Important: A variable must be added to the problem before it is used in the objective or the constraints.
The problem numbers its variables in the order they are added, starting from 0. The number is the variable’s index, readable as var.index, and is the position of the variable’s value in a solution. It is assigned by the problem, so you never pass one in.
Example:¶
# Create a continuous variable: x ∈ [0, 10]
x = SHOTpy.Variable("x", SHOTpy.VariableType.Real, 0.0, 10.0)
# Create an integer variable: y ∈ {0, 1, 2, ..., 5}
y = SHOTpy.Variable("y", SHOTpy.VariableType.Integer, 0.0, 5.0)
# Create a binary variable: z ∈ {0, 1}
z = SHOTpy.Variable("z", SHOTpy.VariableType.Binary, 0.0, 1.0)
print(f"Created variables: {x.name}, {y.name}, {z.name}")
# Create a problem (more on this in §3) and add the variables to it
problem = SHOTpy.Problem(solver)
problem.addVariable(x)
problem.addVariable(y)
problem.addVariable(z)
# Or create a variable and add it to the problem in one step; addVariable returns the variable
u = problem.addVariable("u", SHOTpy.VariableType.Real, 0.0, 10.0)
v = problem.addVariable("v", SHOTpy.VariableType.Binary) # bounds 0 and 1
w = problem.addVariable("w") # real, without bounds
print(f"Variables in the problem (name, index): {[(var.name, var.index) for var in problem.allVariables]}")
Created variables: x, y, z
Variables in the problem (name, index): [('x', 0), ('y', 1), ('z', 2), ('u', 3), ('v', 4), ('w', 5)]
3. Your First Optimization Problem¶
Before exploring all the ways you can build objectives and constraints, let’s solve something small and simple. Every SHOTpy problem follows the same structure:
Item |
Syntax |
Purpose |
|---|---|---|
Create Solver |
|
Initialize SHOT solver instance |
Create Problem |
|
Create optimization problem object, which uses the settings of the solver |
Add Variables |
|
Create variables and add them to the problem |
Set Objective |
|
Set the objective function and its direction |
Add Constraint(s) |
|
Add a constraint with an optional name |
Finalize Problem |
|
Lock problem structure before solving |
Set Problem |
|
Assign problem to solver; returns |
Solve |
|
Execute solver; returns |
Check Status |
|
Check whether the solution is proven optimal |
Get Results |
|
Access primal solution |
Let’s see this in action with a two-variable linear program: $$
$$
# Create solver
solver = SHOTpy.Solver()
# Create problem
problem = SHOTpy.Problem(solver)
# Add variables
x = problem.addVariable("x", SHOTpy.VariableType.Real, 0.0, 100.0)
y = problem.addVariable("y", SHOTpy.VariableType.Real, 0.0, 100.0)
# Set objective
problem.setObjective(2*x + 3*y, SHOTpy.ObjectiveDirection.Minimize)
# Add constraint
problem.addConstraint(x + y >= 5, "c1")
# Finalize problem
problem.finalize()
# Set problem
assert solver.setProblem(problem)
# Solve
assert solver.solveProblem()
# Check status
print(f"Status: {solver.getModelReturnStatus().name}")
# Get solution
sol = solver.getPrimalSolution()
print(f"Optimal objective value: {sol.objValue:.4f}")
print(f"Optimal solution: x = {sol.point[0]:.4f}, y = {sol.point[1]:.4f}")
- Main iteration step ────────────────────────────────────────────────────────────────────────────────────────────────╴
Iteration │ Time │ Dual cuts │ Objective value │ Objective gap │ Current solution
#: type │ tot. │ + | tot. │ dual | primal │ abs. | rel. │ obj.fn. | max.err.
╶─────────────────┴────────┴─────────────┴─────────────────────────┴───────────────────┴──────────────────────────────╴
1: LP-O 0.01 10 | 10 0.0e+00 | 0.0e+00 10 | 0
Status: OptimalGlobal
Optimal objective value: 10.0000
Optimal solution: x = 5.0000, y = 0.0000
This short example already uses every ingredient you’ll need. The next sections slow down and look at each piece in more detail (specifically more about objectives and constraints, what kind of terms you can use, and finally, retrieving solution details), before we put it all together in a larger and harder example.
4. Objectives¶
Objectives specify what to minimize or maximize. The simplest way to set the objective is to write it as an expression:
problem.setObjective(expression, SHOTpy.ObjectiveDirection.<Direction>)
expression: The function to optimize, written with variables, numbers, Python operators (+,-,*,/,**) and SHOTpy functions (e.g.,SHOTpy.sqrt(expr),SHOTpy.exp(expr),SHOTpy.log(expr),SHOTpy.sin(expr), …).<Direction>: EitherMinimize(the default) orMaximize.
You don’t have to say whether the objective is linear, quadratic or nonlinear: problem.finalize() works it out, and creates a LinearObjectiveFunction, QuadraticObjectiveFunction or NonlinearObjectiveFunction.
Alternatively, the objective can be created from its class and built up term by term:
objective = SHOTpy.<Type>ObjectiveFunction(SHOTpy.ObjectiveDirection.<Direction>)
<Type>:Linear,Quadratic, orNonlinear.Add terms using:
objective.add(). The linear and quadratic classes only take terms, i.e., linear (SHOTpy.LinearTerm(coeff, var)) and quadratic (SHOTpy.QuadraticTerm(coeff, var1, var2)) terms.NonlinearObjectiveFunctionalso takes expressions and signomial terms (SHOTpy.SignomialTerm(coeff, [(var, exp), ...])).Then call
problem.setObjective(objective).
The examples in this section and the next use a small problem with the variables \(x, y \in [0.1, 10]\). It is only used to try out objectives and constraints, and is not solved:
# A problem to try out objectives and constraints on; it is not solved
problem = SHOTpy.Problem(solver)
x = problem.addVariable("x", SHOTpy.VariableType.Real, 0.1, 10.0)
y = problem.addVariable("y", SHOTpy.VariableType.Real, 0.1, 10.0)
Linear Example:¶
# Minimize: 2*x + 3*y
problem.setObjective(2*x + 3*y, SHOTpy.ObjectiveDirection.Minimize)
# The same objective created from its class:
# objective = SHOTpy.LinearObjectiveFunction(SHOTpy.ObjectiveDirection.Minimize)
# objective.add(SHOTpy.LinearTerm(2.0, x))
# objective.add(SHOTpy.LinearTerm(3.0, y))
# problem.setObjective(objective)
Quadratic Example:¶
# Minimize: x^2 + y^2 + 2*x*y + x
problem.setObjective(x**2 + y**2 + 2*x*y + x, SHOTpy.ObjectiveDirection.Minimize)
# The same objective created from its class:
# objective = SHOTpy.QuadraticObjectiveFunction(SHOTpy.ObjectiveDirection.Minimize)
# objective.add(SHOTpy.QuadraticTerm(1.0, x, x)) # x^2
# objective.add(SHOTpy.QuadraticTerm(1.0, y, y)) # y^2
# objective.add(SHOTpy.QuadraticTerm(2.0, x, y)) # 2*x*y
# objective.add(SHOTpy.LinearTerm(1.0, x)) # x
# problem.setObjective(objective)
Nonlinear Example:¶
# Minimize: sqrt(x) + 2*exp(x) + log(y) + sin(x) + 1.0*x^2.5*y^-1.0 + 1.0*x^2 - 1.0
problem.setObjective(SHOTpy.sqrt(x) + 2*SHOTpy.exp(x) + SHOTpy.log(y) + SHOTpy.sin(x) + 1.0*x**2.5*y**-1.0 + 1.0*x**2 - 1.0, SHOTpy.ObjectiveDirection.Minimize)
# The same objective created from its class, adding one part at a time:
# objective = SHOTpy.NonlinearObjectiveFunction(SHOTpy.ObjectiveDirection.Minimize)
# objective.add(SHOTpy.sqrt(x))
# objective.add(2*SHOTpy.exp(x))
# objective.add(SHOTpy.log(y))
# objective.add(SHOTpy.sin(x))
# objective.add(1.0*x**2 - 1.0)
# objective.add(SHOTpy.SignomialTerm(1.0, [(x, 2.5), (y, -1.0)]))
# problem.setObjective(objective)
Note: Each call to problem.setObjective() replaces the objective, so the problem now has the nonlinear objective. At the end of the next section, we’ll see what finalize() makes of it.
5. Constraints¶
Constraints are the rules, limits, and mathematical conditions that define the boundaries of your model. The simplest way to add a constraint is to write it as a comparison:
problem.addConstraint(expression <= upper_bound, "name")
The comparison can use
<=,>=or==, with an expression on either side, e.g.,x*y >= x + 1."name"(str, optional): Human-readable name (e.g., “c1”, “demand”, “capacity”). Without a name, the constraint is namedconstraint_<index>. Give each constraint its own name, so that you can look it up withproblem.getConstraint("name").A constraint with both a lower and an upper bound is written as
SHOTpy.inequality(lower_bound, expression, upper_bound). A chained comparison such as1 <= x + y <= 5raises aTypeError, since Python would evaluate it as(1 <= x + y) and (x + y <= 5)and lose one of the bounds.
As for the objective, problem.finalize() decides whether a constraint is a LinearConstraint, QuadraticConstraint or NonlinearConstraint. It may replace the constraint with a new object, so read the constraints back from the problem after finalize(), e.g., with problem.getConstraint("name").
Alternatively, a constraint can be created from its class and built up term by term:
constraint = SHOTpy.<Type>Constraint("name", lower_bound, upper_bound)
<Type>:Linear,Quadratic, orNonlinear.lower_bound(float): Minimum value for the left-hand side; useSHOTpy.SHOT_DBL_MINif unbounded below.upper_bound(float): Maximum value for the left-hand side; useSHOTpy.SHOT_DBL_MAXif unbounded above.Add terms using:
constraint.add(), which takes the same terms as the objective classes.Then call
problem.addConstraint(constraint).
Linear Example:¶
# 2x + y <= 10
problem.addConstraint(2*x + y <= 10, "linear_con")
# The same constraint created from its class:
# constraint = SHOTpy.LinearConstraint("linear_con", SHOTpy.SHOT_DBL_MIN, 10.0)
# constraint.add(SHOTpy.LinearTerm(2.0, x))
# constraint.add(SHOTpy.LinearTerm(1.0, y))
# problem.addConstraint(constraint)
Quadratic Example:¶
# x^2 + y^2 <= 4
problem.addConstraint(x**2 + y**2 <= 4, "quadratic_con")
# The same constraint created from its class:
# constraint = SHOTpy.QuadraticConstraint("quadratic_con", SHOTpy.SHOT_DBL_MIN, 4.0)
# constraint.add(SHOTpy.QuadraticTerm(1.0, x, x))
# constraint.add(SHOTpy.QuadraticTerm(1.0, y, y))
# problem.addConstraint(constraint)
Nonlinear Example:¶
# sqrt(x) + 2*exp(x) + log(y) + sin(x) + 1.0*x^2.5*y^-1.0 + 1.0*x^2 <= 10.0
expr = SHOTpy.sqrt(x) + 2*SHOTpy.exp(x) + SHOTpy.log(y) + SHOTpy.sin(x) + 1.0*x**2.5*y**-1.0 + 1.0*x**2
problem.addConstraint(expr <= 10.0, "nonlinear_con")
# The same constraint created from its class:
# constraint = SHOTpy.NonlinearConstraint("nonlinear_con", expr, SHOTpy.SHOT_DBL_MIN, 10.0)
# problem.addConstraint(constraint)
Two-Sided and Equality Example:¶
# 1 <= x + y <= 8
problem.addConstraint(SHOTpy.inequality(1, x + y, 8), "ranged_con")
# Alternatively, you can add two constraints instead of one ranged constraint:
# problem.addConstraint(x + y >= 1, "lower_bound_con")
# problem.addConstraint(x + y <= 8, "upper_bound_con")
# x = 2y
problem.addConstraint(x == 2*y, "equality_con")
What Does finalize() Make of It?¶
Let’s finalize the problem and read the objective and the constraints back from it, to see which class finalize() gave each of them:
problem.finalize()
print(f"{'objective':14s} -> {type(problem.objectiveFunction).__name__}")
for constraint in problem.numericConstraints:
print(f"{constraint.name:14s} -> {type(constraint).__name__}")
# A constraint can also be looked up by its name
print(problem.getConstraint("ranged_con"))
objective -> NonlinearObjectiveFunction
linear_con -> LinearConstraint
quadratic_con -> QuadraticConstraint
nonlinear_con -> NonlinearConstraint
ranged_con -> LinearConstraint
equality_con -> LinearConstraint
<LinearConstraint 'ranged_con': [ 3,L-convex ] [L ] ranged_con:1 <= +x +y <= 8>
What finalize() Extracts¶
You don’t have to write the terms yourself: problem.finalize() extracts them from the expressions.
Written as |
After |
|---|---|
|
quadratic term |
|
linear terms and a constant |
|
monomial term |
|
signomial term |
|
stays a nonlinear expression |
If nothing nonlinear is left, the constraint or objective becomes a QuadraticConstraint or LinearConstraint (or the corresponding objective class). The extraction is controlled by the settings Model.Reformulation.Quadratics.ExtractStrategy (0 = do not extract), Model.Reformulation.Monomials.Extract and Model.Reformulation.Signomials.Extract, which have to be set before finalize().
finalize() also rewrites the model into the form SHOT works with: a constraint with only a lower bound, f(x) >= L, becomes -f(x) <= -L, while a constraint with both bounds, L <= f(x) <= U (e.g., an equality), is kept as it is.
A signomial term can also be written explicitly with SHOTpy.SignomialTerm(coefficient, [(variable, power), ...]) and added to a NonlinearConstraint or NonlinearObjectiveFunction; e.g., SHOTpy.SignomialTerm(3.0, [(x1, 2.0), (x2, -1.0)]) is \(3x_1^2x_2^{-1}\).
# Create problem
problem = SHOTpy.Problem(solver)
# Add variables
x1 = problem.addVariable("x1", SHOTpy.VariableType.Real, 0.1, 10.0)
x2 = problem.addVariable("x2", SHOTpy.VariableType.Real, 0.1, 10.0)
x3 = problem.addVariable("x3", SHOTpy.VariableType.Real, 0.1, 10.0)
# Set objective: x1*x2*x3 + x1^0.5, a monomial and a signomial term
problem.setObjective(x1*x2*x3 + x1**0.5, SHOTpy.ObjectiveDirection.Minimize)
# Add constraints
# 1 <= 2*x1*x2 <= 5, a quadratic term
problem.addConstraint(SHOTpy.inequality(1, 2*x1*x2, 5), "q")
# 2*x1 >= 1, a linear term
problem.addConstraint(2*x1 >= 1, "lin")
# 3*x1^2*x2^-1 <= 100, with the signomial term written explicitly
constraint = SHOTpy.NonlinearConstraint("sig", SHOTpy.SHOT_DBL_MIN, 100.0)
constraint.add(SHOTpy.SignomialTerm(3.0, [(x1, 2.0), (x2, -1.0)]))
problem.addConstraint(constraint)
# Finalize problem
problem.finalize()
# Read the constraints and the objective back from the problem
for constraint in problem.numericConstraints:
print(f"{constraint.name:9s} -> {type(constraint).__name__}")
properties = problem.objectiveFunction.properties
print(f"objective -> {type(problem.objectiveFunction).__name__}, "
f"monomial terms: {properties.hasMonomialTerms}, signomial terms: {properties.hasSignomialTerms}")
# 2*x1 >= 1 has been rewritten as -2*x1 <= -1
print(problem.getConstraint("lin"))
q -> QuadraticConstraint
lin -> LinearConstraint
sig -> NonlinearConstraint
objective -> NonlinearObjectiveFunction, monomial terms: True, signomial terms: True
<LinearConstraint 'lin': [ 1,L-convex ] [L ] lin: -2*x1 <= -1>
6. Building Large Problems Efficiently¶
Above we added variables, constraints and terms one at a time, which reads well and is fine for small models. However, for large models it does not scale. To build a large problem efficiently, define a Python list and use the following commands:
Instead of |
Use |
|---|---|
|
|
|
|
|
|
|
|
|
|
The bulk add merges the terms through a hash map in a single pass, so the result is exactly the same as adding them one at a time — only faster.
The example below builds a linear and a quadratic constraint with \(N = 20\,000\) terms each, first one term at a time and then all terms at once:
import time
N = 20000
def build(bulk):
"""Build one linear row and one quadratic row of N terms, returning the time it took."""
solver = SHOTpy.Solver()
problem = SHOTpy.Problem(solver)
variables = [SHOTpy.Variable(f"x{i}", SHOTpy.VariableType.Real, 0.0, 10.0) for i in range(N)]
coefficients = [1.0 + i for i in range(N)]
# A list is accepted wherever a Variables container is
problem.addVariables(variables)
start = time.perf_counter()
row = SHOTpy.LinearConstraint("row", 0.0, 1.0)
quadratic_row = SHOTpy.QuadraticConstraint("qrow", SHOTpy.SHOT_DBL_MIN, 1.0)
if bulk:
row.add(SHOTpy.LinearTerms(coefficients, variables))
quadratic_row.add(SHOTpy.QuadraticTerms(coefficients, variables, variables))
else:
for c, v in zip(coefficients, variables):
row.add(SHOTpy.LinearTerm(c, v))
for c, v in zip(coefficients, variables):
quadratic_row.add(SHOTpy.QuadraticTerm(c, v, v))
elapsed = time.perf_counter() - start
problem.addConstraints([row, quadratic_row])
return elapsed, problem
single_time, single_problem = build(bulk=False)
bulk_time, bulk_problem = build(bulk=True)
print(f"one term at a time : {single_time:7.3f} s")
print(f"all terms at once : {bulk_time:7.3f} s ({single_time / bulk_time:.0f} times faster)")
one term at a time : 0.221 s
all terms at once : 0.006 s (36 times faster)
Other Ways of Building a Container¶
A container can also be built from a list of terms, built up with append (which does not merge terms of the same variable, and reserve takes the number of terms that will be added), or handed a list directly: constraint.add([...]) converts the list and adds all of it at once. Constraints written as comparisons are added all at once with problem.addConstraints(comparisons, names).
# Create solver
solver = SHOTpy.Solver()
# Create problem
problem = SHOTpy.Problem(solver)
# Add variables, all at once
x = SHOTpy.Variable("x", SHOTpy.VariableType.Real, 0.0, 10.0)
y = SHOTpy.Variable("y", SHOTpy.VariableType.Real, 0.0, 10.0)
problem.addVariables([x, y])
# Set objective: 2x + 3y, from a list of terms
objective = SHOTpy.LinearObjectiveFunction(SHOTpy.ObjectiveDirection.Minimize)
objective.add(SHOTpy.LinearTerms([SHOTpy.LinearTerm(2.0, x), SHOTpy.LinearTerm(3.0, y)]))
problem.setObjective(objective)
# Add constraints
# x + y >= 1, from terms built up with append
terms = SHOTpy.LinearTerms()
terms.reserve(2)
terms.append(SHOTpy.LinearTerm(1.0, x))
terms.append(SHOTpy.LinearTerm(1.0, y))
c1 = SHOTpy.LinearConstraint("c1", 1.0, SHOTpy.SHOT_DBL_MAX)
c1.add(terms)
# x - y <= 3, from a list passed straight to add()
c2 = SHOTpy.LinearConstraint("c2", SHOTpy.SHOT_DBL_MIN, 3.0)
c2.add([SHOTpy.LinearTerm(1.0, x), SHOTpy.LinearTerm(-1.0, y)])
problem.addConstraints([c1, c2])
# Comparisons and their names, all at once
problem.addConstraints([x + 2*y <= 15, 2*x + y <= 15], ["c3", "c4"])
# Finalize problem
problem.finalize()
7. Retrieving Solution Details¶
After solving, first check the outcome with solver.getModelReturnStatus(): a solution is proven optimal only if the status is SHOTpy.ModelReturnStatus.OptimalGlobal. Other values are, e.g., FeasibleSolution (a solution was found, but it is not proven optimal), InfeasibleGlobal and Unbounded. The solver also provides the bounds and gaps of the solve:
Method |
Returns |
|---|---|
|
The status of the solution, e.g., |
|
Why SHOT stopped, e.g., |
|
The objective value of the best solution found |
|
The best proven bound on the optimal objective value |
|
The gap between the primal and the dual bound |
|
All the solutions found, the best first |
To access the optimal solution itself, use solver.getPrimalSolution(), which returns a solution object with detailed information about the solve. Below you can find a table containing all available attributes:
Attribute |
Type |
Description |
|---|---|---|
|
List of floats |
Vector containing the optimal values for all variables, in the order of their indexes |
|
Float |
The objective function value at the optimal solution |
|
Integer |
The iteration number at which this solution was found |
|
Enum |
The type/source of the solution (e.g., |
|
String |
Human-readable description of how the solution was obtained |
|
Float |
Maximum deviation of integer variables from their rounded values (before rounding was applied) |
|
Pair (int, float) |
Index and value of the linear constraint with largest violation (index -1 if none) |
|
Pair (int, float) |
Index and value of the quadratic constraint with largest violation (index -1 if none) |
|
Pair (int, float) |
Index and value of the nonlinear constraint with largest violation (index -1 if none) |
|
Boolean |
Whether variable bounds were corrected (projected) to upper or lower bounds |
|
Boolean |
Whether integer variables were rounded from their relaxed values |
|
Boolean |
Whether this solution has already been printed to console |
So far we’ve only used sol.objValue and sol.point, but as can be noted, the solution object carries a lot more than that. For example, the attributes let you inspect exactly how a solution was reached, which iteration found it, whether any rounding or bound projection was needed, and where the largest constraint violations (if any) occurred.
8. Putting It All Together: A Full MINLP Example¶
Let’s combine everything to model and solve a complete mixed-integer nonlinear optimization problem.
Consider this mixed-integer nonlinear problem:
# Create solver
solver = SHOTpy.Solver()
# Create problem
problem = SHOTpy.Problem(solver)
# Add variables
x1 = problem.addVariable("x1", SHOTpy.VariableType.Real, 1.0, 20.0)
x2 = problem.addVariable("x2", SHOTpy.VariableType.Integer, 1.0, 20.0)
# Set objective
problem.setObjective(-x1 - x2, SHOTpy.ObjectiveDirection.Minimize)
# Add constraints
# 0.15(x1-8)^2 + 0.1(x2-6)^2 + 0.025*exp(x1)*x2^-2 - 5 <= 0
problem.addConstraint(0.15*(x1 - 8)**2 + 0.1*(x2 - 6)**2 + 0.025*SHOTpy.exp(x1)*x2**(-2) - 5.0 <= 0, "nonlin_con1")
# 1/x1 + 1/x2 - x1^0.5*x2^0.5 + 4 <= 0
problem.addConstraint(1.0/x1 + 1.0/x2 - x1**0.5 * x2**0.5 + 4.0 <= 0, "nonlin_con2")
# 2*x1 - 3*x2 <= 2
problem.addConstraint(2*x1 - 3*x2 <= 2, "lin_con")
# Finalize problem
problem.finalize()
# Set problem
assert solver.setProblem(problem)
# Solve
assert solver.solveProblem()
Performing bound tightening on original problem.
- Bounds for 2 variables tightened in 0.00 s and 5 passes.
- Objective bounds are: [-23.4282, -4.22729]
Performing bound tightening on reformulated problem.
- Bounds for 2 variables tightened in 0.00 s and 2 passes.
- Objective bounds are: [-23.4282, -4.22729]
- Interior point search ──────────────────────────────────────────────────────────────────────────────────────────────╴
Strategy selected: cutting plane minimax
Iteration │ Time │ Cuts │ Objective value │ Objective diff.
#: type │ tot. │ + | tot. │ problem | line srch │ abs. | rel.
╶─────────────────┴────────┴─────────────┴─────────────────────────┴──────────────────╴
1: LP 0.00 -1e+12 | -1e+12 inf. | inf.
2: LP 0.00 2 | 2 -7.6275 | 0.328035 8.0e+00 | 1.0e+00
3: LP 0.00 2 | 4 -3.29179 | 0.299795 3.6e+00 | 1.1e+00
4: LP 0.00 2 | 6 -1.72401 | 0.0590611 1.8e+00 | 1.0e+00
5: LP 0.00 2 | 8 -0.382227 | 0.0339402 4.2e-01 | 1.1e+00
6: LP 0.00 2 | 10 -0.374105 | 0.00370268 3.8e-01 | 1.0e+00
7: LP 0.01 2 | 12 -0.374105 | -0.0165915 3.6e-01 | 9.6e-01
Valid interior point with constraint deviation -0.017 found.
- Main iteration step ────────────────────────────────────────────────────────────────────────────────────────────────╴
Iteration │ Time │ Dual cuts │ Objective value │ Objective gap │ Current solution
#: type │ tot. │ + | tot. │ dual | primal │ abs. | rel. │ obj.fn. | max.err.
╶─────────────────┴────────┴─────────────┴─────────────────────────┴───────────────────┴──────────────────────────────╴
1: LP-O 0.01 -23.4282 | -16.8377 6.6e+00 | 3.9e-01 -23.4282 | 2: 5.00e+00
2: LP-O 0.01 3 | 3 -22.5374 | -20.0386 2.5e+00 | 1.2e-01 -22.5374 | 1: 1.65e+00
3: LP-O 0.01 2 | 5 -22.1259 | -20.0386 2.1e+00 | 1.0e-01 -22.1259 | 1: 1.28e+00
4: LP-O 0.01 2 | 7 -21.7557 | -20.0386 1.7e+00 | 8.6e-02 -21.7557 | 1: 8.78e-01
5: LP-O 0.01 1 | 8 -21.3837 | -20.0386 1.3e+00 | 6.7e-02 -21.3837 | 1: 4.32e-01
6: MILP-SL1 0.01 2 | 10 -21.3837 | -20.2459 1.1e+00 | 5.6e-02 -20.2459 | 0: 0.00e+00
1: NLPSOLPT-R 0.02 2 | 10 -21.3837 | -20.9036 4.8e-01 | 2.3e-02 -20.9036 | 0: 9.98e-09
7: MILP-O 0.02 -20.9373 | -20.9036 3.4e-02 | 1.6e-03 -20.9373 | 2: 5.14e-02
8: MILP-O 0.03 6 | 16 -20.9036 | -20.9036 0.0e+00 | 0.0e+00 -20.9036 | 6: 1.31e+00
# Check the outcome of the solve
print(f"Status: {solver.getModelReturnStatus().name}")
print(f"Termination Reason: {solver.getTerminationReason().name}")
print(f"Primal Bound: {solver.getPrimalBound()}")
print(f"Dual Bound: {solver.getGlobalDualBound()}\n")
# Get detailed solution information
sol = solver.getPrimalSolution()
# Solution details
print(f"Objective Value: {sol.objValue}")
print(f"Iteration Found: {sol.iterFound}")
print(f"Source Description: {sol.sourceDescription}\n")
# Variable values
print("Variable Values:")
print(f" x1 = {sol.point[0]}")
print(f" x2 = {sol.point[1]}")
# The optimum is x1 ≈ 8.903615, x2 = 12
assert abs(sol.objValue + 20.903615) < 1e-3
Status: OptimalGlobal
Termination Reason: AbsoluteGap
Primal Bound: -20.903615008052974
Dual Bound: -20.903615033838058
Objective Value: -20.903615008052974
Iteration Found: 8
Source Description: MIP sol. pool
Variable Values:
x1 = 8.903615008052975
x2 = 12.0
9. Key Takeaways¶
1. SHOTpy Uses a Consistent, Repeatable Workflow¶
Every optimization problem follows the same eight-step pattern:
Create solver.
Create problem.
Add variables.
Set objective.
Add constraints.
Finalize problem.
Set the problem
Solve and retrieve the results.
Once you master this pattern, you can model any LP/QP/NLP/MILP/MI(QC)QP/MINLP problem with SHOTpy.
2. Modeling in SHOTpy Is Straightforward and Intuitive¶
You write mathematics almost exactly as you think it. Build expressions using natural Python operators and SHOTpy functions.
3. Solutions Contain Rich Information¶
The model return status tells you whether a solution is proven optimal. Beyond the optimal variable values, solutions reveal detailed information about the solve. This diagnostic information helps you understand solution quality and solver behavior.
4. Tips for Best Results¶
Follow the order of the steps: nothing can be added to a problem after
finalize(), and most settings have to be changed beforesolver.setProblem().Build anything but a toy model with the bulk forms of Section 6, not a term at a time.
Give the variables bounds that are as tight as you know them; this helps the solver.
Check the return values of
setProblem()andsolveProblem(), andgetModelReturnStatus()and the bounds after solving.
Next Steps¶
In Notebook 2, you’ll learn more about the solver features. To name a few, you’ll learn how to:
Read problems from a file.
Configure the solver for your specific needs.
Use callbacks to customize SHOT’s behavior.