Example: callbacks_first#
Demonstrates the use of generic callbacks
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from amplpy import AMPL
from pprint import pprint
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# Demonstrates the use of generic callbacks
MUTE = True # Set to false to enable more chatter
PRINT_CANDO = False
class Callback(ampls.GenericCallback):
'''Callback class that implements user defined functionalities'''
def __init__(self):
self.calls = {w: 0 for w in ampls.Where}
self.not_mapped = {}
self.last_bound = 10e10
def handle_not_mapped(self):
'''Function to record names and number of occurences of callback
calls not mapped by ampls' GenericCallback'''
where_name = self.get_where_string()
if where_name in self.not_mapped:
self.not_mapped[where_name]+=1
else:
self.not_mapped[where_name]=1
def run(self):
'''Function executed at each callback call'''
# Get the generic stage where the callback has been called
t = self.get_ampl_where()
# Record it
self.calls[t] += 1
if t == ampls.Where.MSG and not MUTE:
print(self.get_message())
return 0
if t == ampls.Where.LPSOLVE and not MUTE:
print(f"LP solve, {self.get_value(ampls.Value.ITERATIONS)} iterations")
return 0
if t == ampls.Where.PRESOLVE and not MUTE:
print("Presolve, eliminated {} rows and {} columns.".format(
self.get_value(ampls.Value.PRE_DELROWS),
self.get_value(ampls.Value.PRE_DELCOLS)))
return 0
if t in [ampls.Where.MIPNODE, ampls.Where.MIPSOL]:
obj_bound = self.get_value(ampls.Value.MIP_OBJBOUND)
if self.last_bound == obj_bound: return 0 # Don't do anything if bound hasn't improved
self.last_bound = obj_bound
nonzeroes = sum(x != 0 for x in self.get_solution_vector())
gap = self.get_value(ampls.Value.MIP_RELATIVEGAP)
print(f"{t.name}: nonzeroes={nonzeroes} obj={self.get_obj()} bound={obj_bound} gap={gap}")
if t == ampls.Where.MIP:
if not PRINT_CANDO:
return 0
# Certain solvers have stop at points during execution of a MIP
# solve that are neither a new node or a new solution. Check for functionalities
if self.can_do(ampls.CanDo.GET_MIP_SOLUTION):
print("In MIP where I can get MIP solution")
if self.can_do(ampls.CanDo.GET_LP_SOLUTION):
print("In MIP where I can get LP solution")
if self.can_do(ampls.CanDo.IMPORT_SOLUTION):
print("In MIP where I can import an heuristic solution")
if t == ampls.Where.NOTMAPPED:
# In case of not mapped, we record the solver-specific stage
self.handle_not_mapped()
return 0
# Load a model with amplpy
ampl = AMPL()
ampl.read("models/queens.mod")
ampl.param["size"]=10
# Export to ampls
model = ampl.to_ampls(SOLVER)
# In case of CPLEX, multithreading when using callbacks must
# be handled by the user. See cplex-genericbranch
if SOLVER == "cplex": model.set_option("threads", 1)
# Create a callback and set it in ampls
cb = Callback()
model.set_callback(cb)
# Start the optimization
model.optimize()
# Import the solution back to amplpy
ampl.import_solution(model)
ampls_obj = model.get_obj()
ampl_obj = ampl.get_current_objective().value()
assert(ampls_obj == ampl_obj)
# Show the number of calls to the callack
pprint(cb.calls)
print(cb.not_mapped)
Example: callbacks_stopping#
Stopping the solution process given a certain criteria using callbacks
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from math import e
from amplpy import AMPL
import time
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# This example shows how to stop the solution process of a MIP
# problem with different criteria: one based on MIP gap,
# the other based on the time since an improvement in the solution
# has been achieved.
# Set to true to activate the MIP gap criteria
STOP_AT_MIPGAP = False
# Set to true to stop when no solution improvement has been detected
# for a certain time
STOP_AT_NOSOLUTIONIMPROVEMENT = True
## Desired MIP gap where to stop execution
DESIREDGAP = 0.01
# Define the maximum time without improvement in
# the solution (in seconds)
MAX_TIME = 10
MUTE = True
class StoppingCallback(ampls.GenericCallback):
'''User defined class implementing the stopping functionality.
Returning -1 from a callback makes the solver interrupt the
solution process'''
def __init__(self):
self.last_bound = 10e10
self.last_obj = None
self.last_obj_time = None
def _evaluate_mipgap(self, mipsol : bool):
# At each solution and each node, we check if the desired gap
# abs(bestobjective-objbound)/bestobjective
# has been achieved. If so, terminate the solution process.
obj_bound = self.get_value(ampls.Value.MIP_OBJBOUND)
gap = self.get_value(ampls.Value.MIP_RELATIVEGAP)
if not mipsol: # print only if bound has improved
if self.last_bound == obj_bound: return 0
self.last_bound = obj_bound;
nnz = sum(x != 0 for x in self.get_solution_vector())
print(f"Current objective: {self.getObj()} - bound: {obj_bound} - relgap: {gap} - nonzeroes: {nnz}")
if gap < DESIREDGAP:
print("Desired gap reached, terminating")
return -1
return 0
def _evaluate_solutionprogress(self):
# Each time we see a new solultion, check its value;
# If no improvement in the solution value has been detected
# for a specified amount of time, interrupt the solution process.
obj = self.get_obj()
print(f"Objective value {obj}")
if obj != self.last_obj:
if self.last_obj_time is not None:
print(f"Improvement in the solution after {time.time() - self.last_obj_time} seconds.")
self.last_obj_time=time.time()
self.last_obj = obj
else:
if time.time() - self.last_obj_time > MAX_TIME:
print(f"No improvement in the solution for {time.time() - self.last_obj_time} seconds, terminating.")
return -1
return 0
def run(self):
returnvalue=0
t = self.get_ampl_where()
if t == ampls.Where.MIPNODE:
if STOP_AT_MIPGAP:
if not MUTE: print(f"New MIP node, count {self.get_value(ampls.Value.MIP_NODES)}")
return self._evaluate_mipgap(False)
if t == ampls.Where.MIPSOL:
print(f"New MIP solution.")
if STOP_AT_MIPGAP:
if self._evaluate_mipgap(True)==-1:
return -1
if STOP_AT_NOSOLUTIONIMPROVEMENT:
return self._evaluate_solutionprogress()
return 0
# Load a model with amplpy
ampl = AMPL()
ampl.read("models/queens.mod")
# Export to ampls
model = ampl.to_ampls(SOLVER);
# Specify option to return the mip gap to AMPL after solving,
# see https://mp.ampl.com/features-guide.html, useful if
# the MIP gap value is then needed in amplpy
model.setOption("mip:return_gap", 7)
# Declare a callback
cb = StoppingCallback()
# In case of CPLEX, multithreading when using callbacks must
# be handled by the user. See cplex-genericbranch
if SOLVER == "cplex": model.set_option("threads", 1)
# Set the callback and start the optimization
model.set_callback(cb)
model.optimize()
# When the optimization finishes, show the results
obj = model.get_obj()
if abs(158 - obj) >= DESIREDGAP:
print("Error")
# Import the solution back to amplpy
ampl.import_solution(model)
ampl_obj = ampl.get_current_objective().value()
if STOP_AT_MIPGAP:
if abs(158 - ampl_obj) >= DESIREDGAP:
print("Error")
# Print MIP gap
ampl_rel_gap = ampl.get_value("Initial.relmipgap")
print(f"From AMPL we see a relative gap of {ampl_rel_gap}")
Example: load_model#
Shows how to export a model from amplpy to ampls, how to solve it and how to get the solution as a vector or as a dictionary
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from amplpy import AMPL
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# Shows how to export a model from amplpy to ampls,
# how to solve it and how to get the solution as a vector or
# as a dictionary
def doStuff(a):
'''Generic function doing the optimization and reading the results'''
# Optimize with default settings
a.optimize()
print("Model status:", a.get_status())
# Print the objective function
print("Objective:", a.get_obj())
# Get the solution as vector and count the nonzeroes
sol = a.get_solution_vector()
countnz = sum(x != 0 for x in sol)
# Get the solution as dictionary (name : value)
sol = a.get_solution_dict()
nonzeroes = {name : value for name, value in sol.items() if value != 0}
for (name, value) in nonzeroes.items():
print("{} = {}".format(name, value))
print(f"Non zeroes: vector = {countnz}, dict = {len(nonzeroes)}")
# Using amplpy
ampl = AMPL()
ampl.read("models/queens.mod")
ampl.param["size"]=10
# Export to specified solver
ampls_model = ampl.to_ampls(SOLVER)
# Call generic function
doStuff(ampls_model)
Example: multiple_models#
Shows how to create multiple models with AMPL, export an AMPLs instance for a specific solver, set some solver options and get status/solution, and get the results back in AMPL. It also shows how to use the native C API of various solvers
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import amplpy
import pandas as pd
from typing import Any, Dict
import amplpy_cplex as ampls
SOLVER = "cplex"
# Example description
# Shows how to create multiple models with AMPL, export an AMPLs instance
# for a specific solver, set some solver options and get status/solution, and get the results
# back in AMPL. It also shows how to use the native C API of various solvers
def makeAmplModel(num_vars :int=10, flip_objective:bool = False, infeasible:bool = False,
presolve_level : bool = 10):
""" Create an instance of AMPL and a model"""
ampl = amplpy.AMPL()
# Note that if a model is detected as infeasible by the AMPL presolver,
# it will not be exported to the solver, and an exception will be thrown.
ampl.set_option("presolve", presolve_level)
ampl.eval("""
set varIndexes within Integers;
set constraintIndexes within Integers;
param varCosts { varIndexes };
param rightHandSides { constraintIndexes };
var x {i in varIndexes} >=0;
maximize objectiveFunction:
sum { i in varIndexes } varCosts[i] * x[i];
subject to mainConstraints { j in constraintIndexes } :
x[j] + x[j+1] <= rightHandSides[j];"""
)
varIndexDf = pd.DataFrame(
data = [(i,i if not flip_objective else -i) for i in range(num_vars)],
columns = ["varIndexes","varCosts"]
).set_index("varIndexes")
ampl.setData(varIndexDf,set_name = "varIndexes")
constraintIndexesDf = pd.DataFrame(
data = [(i,1 if not infeasible else -1) for i in range(num_vars-1)],
columns = ["constraintIndexes","rightHandSides"]
).set_index("constraintIndexes")
ampl.setData(constraintIndexesDf,set_name = "constraintIndexes")
return ampl
def solveModel(ampl:amplpy.AMPL, options:Dict[str,Any] = {}, useNativeCall: bool=True):
"""Solves a model by exporting it to ampls and using either the generic optimize()
function or the solver native C API"""
model= ampl.to_ampls(SOLVER)
for o, v in options.items():
model.set_option(o,v) # Set options from
# ampl driver (see for example https://dev.ampl.com/solvers/gurobi/options.html,
# https://dev.ampl.com/solvers/xpress/options.html# )
# Most of them will work the same way across solvers
# For specific solvers there are also wrappers for the
# gurobi parameter accessors. See https://www.gurobi.com/documentation/10.0/refman/c_parameter_examples.html
if SOLVER == "gurobi":
model.set_param("Threads", 4)
# A third option is to use the solver C API directy, by getting access to the underlying
# gurobi model/environment pointer. This is suitabe to use functionalities provided
# by the specific solver C API
# The two statmeents below are therefore equivalent, the first uses the Gurobi C API directly,
# the latter uses the (generic) function provided by ampls
if useNativeCall:
if SOLVER == "gurobi":
grbnative = model.get_grb_model()
ampls.GRBoptimize(grbnative)
if SOLVER == "cplex":
cplexnativeenv = model.get_cplex_env()
cplexnativemodel = model.get_cplex_lp()
ampls.CPXlpopt(cplexnativeenv, cplexnativemodel)
if SOLVER == "xpress":
xpressnative = model.getXPRSprob()
ampls.XPRSlpoptimize(xpressnative, None)
else:
model.optimize()
ampl.import_solution(model)
return model
def createAndSolveSimpleModel(useNativeCall=True):
num_vars = 10
ampl = makeAmplModel(num_vars=num_vars)
model = solveModel(ampl, useNativeCall=useNativeCall)
print(f"Status is {model.get_status().name}")
assert model.get_status() == ampls.Status.OPTIMAL
# Num Vars is even -> last var idx is odd -> expect odd idx vars to be 1
# Num Vars is odd -> last var idx is even -> expect even idx vars to be 1
expectedSolution = [abs(i%2-num_vars %2) for i in range(num_vars )]
expectedObjective = sum( [i*expectedSolution[i] for i in range(num_vars )])
solverObjective = model.getObj()
assert solverObjective == expectedObjective
# Check that AMPL has received the correct objective
assert solverObjective == ampl.getCurrentObjective().value()
print("Completed Simple Model Test.")
def createAndSolveInfeasibleModel(presolve_level : bool = 10, useNativeCall=True):
ampl = makeAmplModel(infeasible=True, presolve_level=presolve_level)
# Set some solver options
# Setting iisfind to 1 to find the Irreducible Infeasibility Subset
model = solveModel(ampl, {"outlev" : 1, "iisfind" : 1}, useNativeCall=useNativeCall)
print(f"Status is {model.get_status().name}")
assert model.get_status() == ampls.Status.INFEASIBLE
# Display IIS for infeasible model by converting it to a pandas dataframe
print(ampl.get_data("_varname, _var.iis").to_pandas().set_index('_varname'))
print(ampl.get_data("_conname, _con.iis").to_pandas().set_index('_conname'))
print("Completed Infeasible Model Test.")
if __name__ == "__main__":
try:
# With presolve set to a high level, AMPL detects the infeasibilitty
# and does not export the model. Throws an explainatory runtime error
createAndSolveInfeasibleModel(10)
except ampls.AMPLSolverException as e:
print(e)
# Turning off presolve makes ampl actually export the model
# In the fucntion we'll set some options to find the source of the infeasibility
createAndSolveInfeasibleModel(0)
# Repeated solves below for feasible model:
createAndSolveSimpleModel()
createAndSolveSimpleModel()
Example: options#
How to set options in the solver driver using: 1) solver driver options 2) solver specific parameters
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from amplpy import AMPL
import amplpy_cplex as ampls
SOLVER = "cplex"
# Example description
# How to set options in the solver driver using:
# 1) solver driver options
# 2) solver specific parameters
def define_model():
ampl = AMPL()
ampl.eval('var x binary; var y binary; var z binary;'
'minimize TotalSum: z + 1;'
'subj to C1: x+y >= 1;')
return ampl
def solve(ampl: AMPL):
mod = ampl.to_ampls(SOLVER, ["sol:stub=test_multi"])
# Use AMPL driver options
# See https://mp.ampl.com/features-guide.html
# and https://dev.ampl.com/solvers/index.html
mod.set_option("outlev", 1)
# Use solver specific parameters
if SOLVER=="gurobi":
mod.set_param(ampls.GRB_INT_PAR_SOLUTIONLIMIT, 5)
if SOLVER=="cplex":
mod.set_param(ampls.CPXPARAM_MIP_Pool_Capacity, 5)
if SOLVER=="xpress":
mod.set_param(ampls.XPRS_MSP_SOLUTIONS, 5)
# Optimize
mod.optimize()
# Import into AMPL
ampl.import_solution(mod)
print(f"Found {ampl.get_value('Initial.nsol')} solutions")
print(f"Objective value: {mod.get_obj()}")
if __name__ == "__main__":
a = define_model()
solve(a)
Example: pattern_generation#
Solves a cutting stock problem using two AMPL problems for pattern generation and shows how to terminate the solution of the final MIP with a callback by increasing the allowed MIP gap depending on how many solutions have been found
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from math import floor
from amplpy import AMPL
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# Solves a cutting stock problem using two AMPL problems for pattern generation
# and shows how to terminate the solution of the final MIP with a callback by
# increasing the allowed MIP gap depending on how many solutions have been found
def create_models() -> tuple:
master="""param nPatterns integer > 0;
set PATTERNS = 1..nPatterns; # patterns
set WIDTHS; # finished widths
param order {WIDTHS} >= 0; # rolls of width j ordered
param overrun; # permitted overrun on any width
param rawWidth; # width of raw rolls to be cut
param rolls {WIDTHS,PATTERNS} >= 0, default 0;
# rolls of width i in pattern j
var Cut {PATTERNS} integer >= 0; # raw rolls to cut in each pattern
minimize TotalRawRolls: sum {p in PATTERNS} Cut[p];
subject to OrderLimits {w in WIDTHS}:
order[w] <= sum {p in PATTERNS} rolls[w,p] * Cut[p] <= order[w] + overrun;"""
subproblem = """set SIZES; # ordered widths
param cap >= 0; # width of raw rolls
param val {SIZES}; # shadow prices of ordered widths (from Master)
var Qty {SIZES} integer >= 0; # number of each width in generated pattern
maximize TotVal: sum {s in SIZES} val[s] * Qty[s];
subject to Cap: sum {s in SIZES} s * Qty[s] <= cap;"""
# Data
roll_width = 6000
overrun = 30
orders = {
1630: 172, 1625: 714, 1620: 110, 1617: 262, 1540: 32, 1529: 100, 1528: 76,
1505: 110, 1504: 20, 1484: 58, 1466: 15, 1450: 10, 1283: 40, 1017: 50,
970: 70, 930: 8, 916: 210, 898: 395, 894: 49, 881: 17, 855: 20, 844: 10,
805: 718, 787: 17, 786: 710, 780: 150, 754: 34, 746: 15, 707: 122, 698: 7,
651: 10, 644: 15, 638: 10, 605: 10, 477: 4, 473: 34, 471: 25, 468: 10,
460: 908, 458: 161, 453: 765, 447: 21, 441: 20, 422: 318, 421: 22,
419: 382, 396: 22, 309: 123,266: 35
}
widths = list(sorted(orders.keys(), reverse=True))
# Create master
Master = AMPL()
Master.eval(master)
# Send scalar values
Master.param["nPatterns"] = len(widths)
Master.param["overrun"] = overrun
Master.param["rawWidth"] = roll_width
# Send order vector
Master.set["WIDTHS"] = widths
Master.param["order"] = orders
# Generate and send initial pattern matrix
Master.param["rolls"] = {
(widths[i], 1+i): int(floor(roll_width/widths[i]))
for i in range(len(widths))
}
# Define a param for sending new patterns to master problem
Master.eval("param newPat {WIDTHS} integer >= 0;")
# Set solve options
Master.option["solver"] = SOLVER
Master.option["relax_integrality"] = 1
# Create subproblem
Sub = AMPL()
Sub.eval(subproblem)
Sub.set["SIZES"] = widths
Sub.param["cap"] = roll_width
Sub.option["solver"] = SOLVER
return Master,Sub
Master,Sub = create_models()
# Main cycle, solves the relaxed master and use the duals
# to populate the subproblem.
# Solve the subproblem to generate new patterns and add them
# to the master
while True:
Master.solve()
Sub.param["val"].setValues(Master.con["OrderLimits"].getValues())
Sub.solve()
if Sub.obj["TotVal"].value() <= 1.00001:
break
Master.param["newPat"].setValues(Sub.var["Qty"].getValues())
Master.eval("let nPatterns := nPatterns + 1;")
Master.eval("let {w in WIDTHS} rolls[w, nPatterns] := newPat[w];")
# At this point, we export the non-relaxed master problem
# and solve with ampls
Master.option["relax_integrality"] = 0
# Export model to ampls
# If we plan to import the results back to AMPL, we have to explicitly set what additional
# suffixes we want returned at this stage
# In this case, we want to return the mip gap as a suffix
ampls_model = Master.to_ampls(SOLVER, ["return_mipgap=5"])
class MyCallback(ampls.GenericCallback):
"""Callback implementing the stopping rule"""
def __init__(self, stoprule):
super(MyCallback, self).__init__()
self._stoprule = stoprule
self._current = 0
self._continueOpt = True
def setCurrentGap(self):
print("Increasing gap tolerance to %.2f%%" % \
(100*self._stoprule["gaptol"][self._current]))
ampls_model.setAMPLParameter(ampls.SolverParams.DBL_MIPGap,
self._stoprule["gaptol"][self._current])
self._current += 1
def run(self):
where = self.getAMPLWhere()
if where == ampls.Where.MIPNODE:
runtime = self.getValue(ampls.Value.RUNTIME)
if runtime >= self._stoprule["time"][self._current]:
print(f"Current is: {self._stoprule['time'][self._current]}")
print(f"Stopping optimization at {runtime} seconds")
self._continueOpt = True
return -1
return 0
# Callback"s stopping rule is created here...
stopdict = { "time" : ( 1, 2, 3, 60 ),
"gaptol" : ( .0002, .002, .02, .1 )
}
# ...and initialized in the constructor
callback = MyCallback(stopdict)
ampls_model.setCallback(callback)
# Invoke solver
# Most solvers (e.g. Gurobi https://support.gurobi.com/hc/en-us/articles/360039233191-How-do-I-change-parameters-in-a-callback-)
# do not support changing optimization parameters from a callback
# Instead we have to stop the optimization, change the desired parameters
# and start it again
callback._continueOpt = True
while callback._continueOpt:
callback._continueOpt = False
ampls_model.optimize()
if callback._continueOpt:
callback.setCurrentGap()
# Import solution from the solver
Master.importSolution(ampls_model)
print(f"From AMPL MIPGap={100*Master.getValue('TotalRawRolls.relmipgap'):.3f}%")
print(f"Objective value: {Master.getValue('TotalRawRolls')}")
Example: tsp_callback#
This example uses generic callbacks to solve a travelling salesperson problem with MTZ cuts or with sub-tour elimination constraints.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from tsp_helpers import tsp_model, ford_fulkerson, UnionFind
import amplpy_gurobi as ampls
SOLVER = "gurobi"
# Example description
# This example uses generic callbacks to solve a travelling salesperson problem
# with MTZ cuts or with sub-tour elimination constraints.
VERBOSE = True
ENABLE_MTZ = False
if ENABLE_MTZ:
ENABLE_CB_MIPSOL = False # not necessary if MTZ cuts are in place
else:
ENABLE_CB_MIPSOL = True # must add sub-tour elimination constraints
ENABLE_CB_MIPNODE = not ENABLE_CB_MIPSOL
class MyCallback(ampls.GenericCallback):
CALL_COUNT_MIPSOL = 0
CALL_COUNT_MIPNODE = 0
def mipsol(self):
self.CALL_COUNT_MIPSOL += 1
sol = self.get_solution_vector()
nv = sum(abs(x) > 1e-5 for x in sol)
if VERBOSE:
print("MIPSOL #{}, nnz={}".format(self.CALL_COUNT_MIPSOL, nv))
values = {
xvars[i]: sol[i]
for i in xvars
if abs(sol[i]) > 1e-5
}
uf = UnionFind()
for (u, v) in values:
uf.link(u, v)
groups = uf.groups()
if len(groups) == 1:
print('Valid solution!')
else:
for grp in groups:
print('> sub-tour: ', grp)
cutvarnames = [ampls.tuple2var('x', i, j)
for i in grp for j in grp if i != j]
coeffs = [1 for i in range(len(cutvarnames))]
print(f"Adding lazy: {cutvarnames}")
self.add_lazy(cutvarnames, coeffs,
ampls.CutDirection.LE, len(grp)-1)
return 0
def mipnode(self):
self.CALL_COUNT_MIPNODE += 1
if VERBOSE:
print("MIPNODE #{}!".format(self.CALL_COUNT_MIPNODE))
if self.CALL_COUNT_MIPNODE >= 1000:
return 1
sol = self.getSolutionVector()
capacities = {
xvars[i]: sol[i]
for i in xvars
if abs(sol[i]) > 1e-5
}
for i in range(1, len(vertices)):
max_flow, partition = ford_fulkerson(
capacities, vertices[0], vertices[i]
)
if max_flow <= 1-1e-5:
p1 = set(partition)
p2 = set(vertices) - p1
min_cut = sum(
capacities.get((i, j), 0)
for i in p1 for j in p2
)
cutvarnames = [ampls.tuple2var('x', i, j) for i in p1 for j in p2]
coeffs = [1 for i in range(len(cutvarnames))]
self.add_cut(cutvarnames, coeffs, ampls.CutDirection.GE, 1)
print('> max-flow: {}, min-cut: {}, must be == 1'.format(
max_flow, min_cut))
return 0
return 0
def run(self):
try:
if ENABLE_CB_MIPSOL and self.getAMPLWhere() == ampls.Where.MIPSOL:
return self.mipsol()
elif ENABLE_CB_MIPNODE and self.getAMPLWhere() == ampls.Where.MIPNODE:
return self.mipnode()
else:
return 0
except Exception as e:
print('Error:', e)
return 1
# Create the ampls model instance
ampl = tsp_model('tsp/tsp_51_1.txt', ENABLE_MTZ)
m = ampl.to_ampls(SOLVER)
print("Model loaded, nvars=", m.getNumVars())
if ENABLE_CB_MIPSOL: # needs lazy constraints
m.enableLazyConstraints()
# Initialize global structures
var_map = dict(m.getVarMapFiltered('x'))
xvars = {
index: ampls.var2tuple(var)[1:]
for var, index in var_map.items()}
vertices = list(sorted(set(
[x[0] for x in xvars.values()] + [x[1] for x in xvars.values()])))
# Set the callback
cb = MyCallback()
m.setCallback(cb)
# In case of CPLEX, multithreading when using callbacks must
# be handled by the user. See cplex-genericbranch
if SOLVER == "cplex": m.set_option("threads", 1)
# Start the optimzation
m.optimize()
# Get the results
obj = m.getObj()
nvars = m.getNumVars()
print("Solved for {} variables, objective {}".format(nvars, obj))
if m.getStatus() == ampls.Status.OPTIMAL:
ampl.importSolution(m)
ampl.display('total')