Sorry, I missed the detail about the new variables. This is actually not possible with CPLEX. From a callback you can only add new constraints. During a solve, variables can neither be added nor removed. In some cases it is possible to create the required variables a-priori, protect them from being removed in presolve and then do something with them in a callback. But that smells a lot like a hack.
Original Message:
Sent: Mon May 25, 2020 08:24 AM
From: Fernando Dias
Subject: Docplex Python "Losing Constraint"
Thank you very much. This will give a clear idea of how to start.
One difference that I noticed is that you don't add any new variable when you add the cuts. In my case, every time, I'll add a cut a new binary related to the specific cut? Do you know if callback can handle that? Adding a new constraint and new variables?
Thank you
------------------------------
Fernando Dias
------------------------------
Original Message:
Sent: Mon May 25, 2020 12:46 AM
From: Daniel Junglas
Subject: Docplex Python "Losing Constraint"
Here is a short code example that uses a cut callback with CPLEX. In order to do this, you have to use the (undocumented) mixin class:
import sysfrom cplex.callbacks import UserCutCallbackfrom docplex.mp.callbacks.cb_mixin import *from docplex.mp.model import Modelclass MyCutCallback(ConstraintCallbackMixin, UserCutCallback): def __init__(self, env): UserCutCallback.__init__(self, env) ConstraintCallbackMixin.__init__(self) self.eps = 1e-6 self.nb_cuts = 0 def add_cut_constraint(self, ct): self.register_constraint(ct) @print_called("--> custom cut callback called: def __call__(self): sol = self.make_solution() unsats = self.get_cpx_unsatisfied_cts(self.cts, sol, self.eps) for ct, cut, sense, rhs in unsats: self.add(cut, sense, rhs) self.nb_cuts += 1 print('-- add new cut[{0}]: [{1!s}]'.format(self.nb_cuts, ct))def build_supply_model(fixed_costs, supply_costs, use_cuts=False, **kwargs): m = Model(name='suppy', **kwargs) nb_locations = len(fixed_costs) nb_clients = len(supply_costs) range_locations = range(nb_locations) range_clients = range(nb_clients) used = m.binary_var_list(range_locations, name='used') supply = m.continuous_var_matrix(range_clients, range_locations, lb=0, ub=1, name='supply') m.used = used m.supply = supply m.add_constraints(m.sum(supply[c, l] for l in range_locations) == 1 for c in range_clients) m.add_constraints( m.sum(supply[c, l] for c in range_clients) <= (nb_clients - 1) * used[l] for l in range_locations) total_fixed_cost = m.dot(used, fixed_costs) m.add_kpi(total_fixed_cost, 'Total fixed cost') total_supply_cost = m.sum(supply[c, l] * supply_costs[c][l] for c in range_clients for l in range_locations) m.add_kpi(total_supply_cost, 'Total supply cost') m.minimize(total_fixed_cost + total_supply_cost) if use_cuts: cut_cb = m.register_callback(MyCutCallback) for l in range_locations: location_used = used[l] for c in range_clients: cut_cb.add_cut_constraint(supply[c, l] <= location_used) print('* add cut constraints callback with {0} cuts'.format(len(cut_cb.cts))) m.cut_callback = cut_cb return mdef print_solution(m, tol=1e-6): used = m.used supply = m.supply n_locations = len(used) n_clients = (int)(len(supply) / n_locations) for l in range(n_locations): if used[l].solution_value >= 1 - tol: print('Facility %d is used, it serves clients %s' % (l, ', '.join((str(c) for c in range(n_clients) if supply[c, l].solution_value >= 1 - tol))))DEFAULT_FIXED_COSTS = [480, 200, 320, 340, 300]DEFAULT_SUPPLY_COSTS = [[24, 74, 31, 51, 84], [57, 54, 86, 61, 68], [57, 67, 29, 91, 71], [54, 54, 65, 82, 94], [98, 81, 16, 61, 27], [13, 92, 34, 94, 87], [54, 72, 41, 12, 78], [54, 64, 65, 89, 89]]def build_test_supply_model(use_cuts, **kwargs): return build_supply_model(DEFAULT_FIXED_COSTS, DEFAULT_SUPPLY_COSTS, use_cuts=use_cuts, **kwargs)if __name__ == "__main__": args = sys.argv use_cuts = True for arg in args[1:]: if arg == '-cuts': use_cuts = False elif arg == '-nocuts': use_cuts = False else: print('Unknown argument %s' % arg) random = False if random: import numpy as np nl = 20 nc = 100 fixed = np.random.randint(100, high=500, size=nl) supply = np.random.randint(1, high=100, size=(nc, nl)) else: fixed = DEFAULT_FIXED_COSTS supply = DEFAULT_SUPPLY_COSTS m = build_supply_model(fixed, supply, use_cuts=use_cuts) m.parameters.preprocessing.presolve = 0 m.print_information() s = m.solve(log_output=False) assert s m.report() print_solution(m) if not random: assert abs(m.objective_value - 843) <= 1e-4
------------------------------
Daniel Junglas
Original Message:
Sent: Sun May 24, 2020 11:41 PM
From: Fernando Dias
Subject: Docplex Python "Losing Constraint"
Thank you very much for your help
It was exactly the problem that I had.
Now furthering that, I wanna use that loop that I have that creates the cuts as a separated function and then use that function as part of a callback option in the DOCPLEX library (if they're available). Basically, the idea is to instead of solving the model from start in every iteration, I would start the code as it is, and from time to time, check if the cuts are necessary and add them, without stopping, going another iteration in the loop or rebuilding the model. So the callbacks would verify if cuts are necessary, process them and add them to the original model.
Any idea if that's possible in Python using the docplex library and how to start it?
Thank you
------------------------------
Fernando Dias
Original Message:
Sent: Tue May 12, 2020 09:38 AM
From: Daniel Junglas
Subject: Docplex Python "Losing Constraint"
How do you cuts look like? Are these just linear constraints? From your output you not only add constraints but also add variables. Could you export your model using Model.export_as_lp() for each iteration and then compare the LP files generated for each iteration? What is the difference between them? Can you maybe attach the two files here?
------------------------------
Daniel Junglas