Frozen Solve#

In rare cases, the GAMSPy model generation time dominates the solver solution time and GAMSPy itself becomes the bottleneck in an optimization application. For a model instance which is a single mathematical model generated by a GAMSPy solve statement, frozen solve provides a controlled way of modifying a model instance and solving the resulting problem repeatedly in the most efficient way, by communicating only the changes of the model to the solver and doing a hot start (in case of a continuous model like LP) without the use of disk IO.

Frozen solves are defined in two steps:

1. The freeze call will enable the frozen solve mode and query the symbol information of the modifiable symbols. The modifiables can be consisted of Parameter and Variable or Equation attributes such as Variable.lo, Variable.up and Variable.fx.

2. The solve method uses this data to update the model instance. After the model instance has been updated, the model is passed to the selected solver. After the completion of the solve method, the container will contain the primal and dual solution of the model just solved. Moreover, the parameters that are modifiable are also accessible in database with the name of the Parameter plus “_var”. The marginal of this variable can provide sensitivity information about the parameter setting. The status of the solve is accessible through the Model.model_status, Model.solve_status and Model.objective_value attributes.

  1. The unfreeze function unfreezes the model and releases the resources.

from gamspy import (
    Container,
    Set,
    Parameter,
    Variable,
    Equation,
    Sum,
    Model,
    Sense,
    ModelStatus,
)
import numpy as np

m = Container()

i = Set(m, name="i")
j = Set(m, name="j")

a = Parameter(
    m,
    name="a",
    domain=i,
    domain_forwarding=True,
    records=[["seattle", 350], ["san-diego", 600]],
)
b = Parameter(
    m,
    name="b",
    domain=j,
    domain_forwarding=True,
    records=[["new-york", 325], ["chicago", 300], ["topeka", 275]],
)
d = Parameter(
    m, name="d", domain=[i, j], records=np.array([[2.5, 1.7, 1.8], [2.5, 1.8, 1.4]])
)
c = Parameter(m, name="c", domain=[i, j])
c[i, j] = 90 * d[i, j] / 1000

x = Variable(m, name="x", domain=[i, j], type="Positive")
z = Variable(m, name="z")

supply = Equation(m, name="supply", domain=i)
demand = Equation(m, name="demand", domain=j)
bmult = Parameter(m, name="bmult", records=1)

cost = Sum((i, j), c[i, j] * x[i, j])
supply[i] = Sum(j, x[i, j]) <= a[i]
demand[j] = Sum(i, x[i, j]) >= bmult * b[j]

transport = Model(
    m,
    name="transport",
    equations=[supply, demand],
    problem="LP",
    sense=Sense.MIN,
    objective=cost,
)

bmult_list = [0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3]

transport.freeze(modifiables=[bmult])

for b_value in bmult_list:
    bmult.setRecords(b_value)
    transport.solve(solver="conopt")
    print(
        f'obj:{transport.objective_value if transport.status == ModelStatus.OptimalGlobal else "infeasible"}'
    )

transport.unfreeze()

The solver used can be switched in between solves, for example the following script uses conopt for even numbers and cplex for odd numbers:

for index, b_value in enumerate(bmult_list):
    bmult.setRecords(b_value)

    if index % 2 == 0:
        transport.solve(solver="conopt")
    else:
        transport.solve(solver="cplex")

Note

Modifiable parameters cannot be used in .where conditions. Variable and equation attributes used in equation algebra are evaluated once at model generation. Changes in the attibutes will not percolate to the algebra. For example, the algebra x <= b * x.up will not change even if the modifiables include x.up. One needs a parameter bigM and algebra x <= b * bigM in order to modify this algebra in a frozen solve.