1#  ___________________________________________________________________________
2#
3#  Pyomo: Python Optimization Modeling Objects
4#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
5#  Under the terms of Contract DE-NA0003525 with National Technology and
6#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
7#  rights in this software.
8#  This software is distributed under the 3-clause BSD License.
9#  ___________________________________________________________________________
10
11"""Utility functions and classes for the MindtPy solver."""
12from __future__ import division
13import logging
14from pyomo.common.collections import ComponentMap, Bunch
15from pyomo.core import (Block, Constraint,
16                        Objective, Reals, Suffix, Var, minimize, RangeSet, ConstraintList, TransformationFactory)
17from pyomo.core.expr import differentiate
18from pyomo.core.expr import current as EXPR
19from pyomo.opt import SolverFactory, SolverResults
20from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver
21from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code
22from pyomo.core.expr.calculus.derivatives import differentiate
23from pyomo.common.dependencies import attempt_import
24from pyomo.contrib.fbbt.fbbt import fbbt
25from pyomo.solvers.plugins.solvers.gurobi_persistent import GurobiPersistent
26
27pyomo_nlp = attempt_import('pyomo.contrib.pynumero.interfaces.pyomo_nlp')[0]
28numpy = attempt_import('numpy')[0]
29logger = logging.getLogger('pyomo.contrib')
30
31
32class MindtPySolveData(object):
33    """Data container to hold solve-instance data.
34    Key attributes:
35        - original_model: the original model that the user gave us to solve
36        - working_model: the original model after preprocessing
37    """
38    pass
39
40
41def model_is_valid(solve_data, config):
42    """
43    Determines whether the model is solveable by MindtPy.
44
45    This function returns True if the given model is solveable by MindtPy (and performs some preprocessing such
46    as moving the objective to the constraints).
47
48    Parameters
49    ----------
50    solve_data: MindtPy Data Container
51        data container that holds solve-instance data
52    config: MindtPy configurations
53        contains the specific configurations for the algorithm
54
55    Returns
56    -------
57    Boolean value (True if model is solveable in MindtPy else False)
58    """
59    m = solve_data.working_model
60    MindtPy = m.MindtPy_utils
61
62    # Handle LP/NLP being passed to the solver
63    prob = solve_data.results.problem
64    if len(MindtPy.discrete_variable_list) == 0:
65        config.logger.info('Problem has no discrete decisions.')
66        obj = next(m.component_data_objects(ctype=Objective, active=True))
67        if (any(c.body.polynomial_degree() not in {1, 0} for c in MindtPy.constraint_list) or
68                obj.expr.polynomial_degree() not in {1, 0}):
69            config.logger.info(
70                'Your model is a NLP (nonlinear program). '
71                'Using NLP solver %s to solve.' % config.nlp_solver)
72            nlpopt = SolverFactory(config.nlp_solver)
73            set_solver_options(nlpopt, solve_data, config, solver_type='nlp')
74            nlpopt.solve(solve_data.original_model,
75                         tee=config.nlp_solver_tee, **config.nlp_solver_args)
76            return False
77        else:
78            config.logger.info(
79                'Your model is an LP (linear program). '
80                'Using LP solver %s to solve.' % config.mip_solver)
81            mainopt = SolverFactory(config.mip_solver)
82            if isinstance(mainopt, PersistentSolver):
83                mainopt.set_instance(solve_data.original_model)
84            set_solver_options(mainopt, solve_data,
85                               config, solver_type='mip')
86            mainopt.solve(solve_data.original_model,
87                          tee=config.mip_solver_tee, **config.mip_solver_args)
88            return False
89
90    if not hasattr(m, 'dual') and config.calculate_dual:  # Set up dual value reporting
91        m.dual = Suffix(direction=Suffix.IMPORT)
92
93    # TODO if any continuous variables are multiplied with binary ones,
94    #  need to do some kind of transformation (Glover?) or throw an error message
95    return True
96
97
98def calc_jacobians(solve_data, config):
99    """
100    Generates a map of jacobians for the variables in the model
101
102    This function generates a map of jacobians corresponding to the variables in the model and adds this
103    ComponentMap to solve_data
104
105    Parameters
106    ----------
107    solve_data: MindtPy Data Container
108        data container that holds solve-instance data
109    config: MindtPy configurations
110        contains the specific configurations for the algorithm
111    """
112    # Map nonlinear_constraint --> Map(
113    #     variable --> jacobian of constraint wrt. variable)
114    solve_data.jacobians = ComponentMap()
115    if config.differentiate_mode == 'reverse_symbolic':
116        mode = differentiate.Modes.reverse_symbolic
117    elif config.differentiate_mode == 'sympy':
118        mode = differentiate.Modes.sympy
119    for c in solve_data.mip.MindtPy_utils.nonlinear_constraint_list:
120        vars_in_constr = list(EXPR.identify_variables(c.body))
121        jac_list = differentiate(
122            c.body, wrt_list=vars_in_constr, mode=mode)
123        solve_data.jacobians[c] = ComponentMap(
124            (var, jac_wrt_var)
125            for var, jac_wrt_var in zip(vars_in_constr, jac_list))
126
127
128def add_feas_slacks(m, config):
129    """
130    Adds feasibility slack variables according to config.feasibility_norm (given an infeasible problem)
131
132    Parameters
133    ----------
134    m: model
135        Pyomo model
136    config: ConfigBlock
137        contains the specific configurations for the algorithm
138    """
139    MindtPy = m.MindtPy_utils
140    # generate new constraints
141    for i, constr in enumerate(MindtPy.nonlinear_constraint_list, 1):
142        if constr.has_ub():
143            if config.feasibility_norm in {'L1', 'L2'}:
144                MindtPy.feas_opt.feas_constraints.add(
145                    constr.body - constr.upper
146                    <= MindtPy.feas_opt.slack_var[i])
147            else:
148                MindtPy.feas_opt.feas_constraints.add(
149                    constr.body - constr.upper
150                    <= MindtPy.feas_opt.slack_var)
151        if constr.has_lb():
152            if config.feasibility_norm in {'L1', 'L2'}:
153                MindtPy.feas_opt.feas_constraints.add(
154                    constr.body - constr.lower
155                    >= -MindtPy.feas_opt.slack_var[i])
156            else:
157                MindtPy.feas_opt.feas_constraints.add(
158                    constr.body - constr.lower
159                    >= -MindtPy.feas_opt.slack_var)
160
161
162def var_bound_add(solve_data, config):
163    """
164    This function will add bounds for variables in nonlinear constraints if they are not bounded. (This is to avoid
165    an unbounded main problem in the LP/NLP algorithm.) Thus, the model will be updated to include bounds for the
166    unbounded variables in nonlinear constraints.
167
168    Parameters
169    ----------
170    solve_data: MindtPy Data Container
171        data container that holds solve-instance data
172    config: ConfigBlock
173        contains the specific configurations for the algorithm
174
175    """
176    m = solve_data.working_model
177    MindtPy = m.MindtPy_utils
178    for c in MindtPy.nonlinear_constraint_list:
179        for var in EXPR.identify_variables(c.body):
180            if var.has_lb() and var.has_ub():
181                continue
182            elif not var.has_lb():
183                if var.is_integer():
184                    var.setlb(-config.integer_var_bound - 1)
185                else:
186                    var.setlb(-config.continuous_var_bound - 1)
187            elif not var.has_ub():
188                if var.is_integer():
189                    var.setub(config.integer_var_bound)
190                else:
191                    var.setub(config.continuous_var_bound)
192
193
194def generate_norm2sq_objective_function(model, setpoint_model, discrete_only=False):
195    """
196    This function generates objective (FP-NLP subproblem) for minimum euclidean distance to setpoint_model
197    L2 distance of (x,y) = \sqrt{\sum_i (x_i - y_i)^2}
198
199    Parameters
200    ----------
201    model: Pyomo model
202        the model that needs new objective function
203    setpoint_model: Pyomo model
204        the model that provides the base point for us to calculate the distance
205    discrete_only: Bool
206        only optimize on distance between the discrete variables
207    """
208    # skip objective_value variable and slack_var variables
209    var_filter = (lambda v: v[1].is_integer()) if discrete_only \
210        else (lambda v: v[1].name != 'MindtPy_utils.objective_value' and
211              'MindtPy_utils.feas_opt.slack_var' not in v[1].name)
212
213    model_vars, setpoint_vars = zip(*filter(var_filter,
214                                            zip(model.component_data_objects(Var),
215                                                setpoint_model.component_data_objects(Var))))
216    assert len(model_vars) == len(
217        setpoint_vars), 'Trying to generate Squared Norm2 objective function for models with different number of variables'
218
219    return Objective(expr=(
220        sum([(model_var - setpoint_var.value)**2
221             for (model_var, setpoint_var) in
222             zip(model_vars, setpoint_vars)])))
223
224
225def generate_norm1_objective_function(model, setpoint_model, discrete_only=False):
226    """
227    This function generates objective (PF-OA main problem) for minimum Norm1 distance to setpoint_model
228    Norm1 distance of (x,y) = \sum_i |x_i - y_i|
229
230    Parameters
231    ----------
232    model: Pyomo model
233        the model that needs new objective function
234    setpoint_model: Pyomo model
235        the model that provides the base point for us to calculate the distance
236    discrete_only: Bool
237        only optimize on distance between the discrete variables
238    """
239    # skip objective_value variable and slack_var variables
240    var_filter = (lambda v: v.is_integer()) if discrete_only \
241        else (lambda v: v.name != 'MindtPy_utils.objective_value' and
242              'MindtPy_utils.feas_opt.slack_var' not in v.name)
243    model_vars = list(filter(var_filter, model.component_data_objects(Var)))
244    setpoint_vars = list(
245        filter(var_filter, setpoint_model.component_data_objects(Var)))
246    assert len(model_vars) == len(
247        setpoint_vars), 'Trying to generate Norm1 objective function for models with different number of variables'
248    model.MindtPy_utils.del_component('L1_obj')
249    obj_blk = model.MindtPy_utils.L1_obj = Block()
250    obj_blk.L1_obj_idx = RangeSet(len(model_vars))
251    obj_blk.L1_obj_var = Var(
252        obj_blk.L1_obj_idx, domain=Reals, bounds=(0, None))
253    obj_blk.abs_reform = ConstraintList()
254    for idx, v_model, v_setpoint in zip(obj_blk.L1_obj_idx, model_vars,
255                                        setpoint_vars):
256        obj_blk.abs_reform.add(
257            expr=v_model - v_setpoint.value >= -obj_blk.L1_obj_var[idx])
258        obj_blk.abs_reform.add(
259            expr=v_model - v_setpoint.value <= obj_blk.L1_obj_var[idx])
260
261    return Objective(expr=sum(obj_blk.L1_obj_var[idx] for idx in obj_blk.L1_obj_idx))
262
263
264def generate_norm_inf_objective_function(model, setpoint_model, discrete_only=False):
265    """
266    This function generates objective (PF-OA main problem) for minimum Norm Infinity distance to setpoint_model
267    Norm-Infinity distance of (x,y) = \max_i |x_i - y_i|
268
269    Parameters
270    ----------
271    model: Pyomo model
272        the model that needs new objective function
273    setpoint_model: Pyomo model
274        the model that provides the base point for us to calculate the distance
275    discrete_only: Bool
276        only optimize on distance between the discrete variables
277    """
278    # skip objective_value variable and slack_var variables
279    var_filter = (lambda v: v.is_integer()) if discrete_only \
280        else (lambda v: v.name != 'MindtPy_utils.objective_value' and
281              'MindtPy_utils.feas_opt.slack_var' not in v.name)
282    model_vars = list(filter(var_filter, model.component_data_objects(Var)))
283    setpoint_vars = list(
284        filter(var_filter, setpoint_model.component_data_objects(Var)))
285    assert len(model_vars) == len(
286        setpoint_vars), 'Trying to generate Norm Infinity objective function for models with different number of variables'
287    model.MindtPy_utils.del_component('L_infinity_obj')
288    obj_blk = model.MindtPy_utils.L_infinity_obj = Block()
289    obj_blk.L_infinity_obj_var = Var(domain=Reals, bounds=(0, None))
290    obj_blk.abs_reform = ConstraintList()
291    for v_model, v_setpoint in zip(model_vars,
292                                   setpoint_vars):
293        obj_blk.abs_reform.add(
294            expr=v_model - v_setpoint.value >= -obj_blk.L_infinity_obj_var)
295        obj_blk.abs_reform.add(
296            expr=v_model - v_setpoint.value <= obj_blk.L_infinity_obj_var)
297
298    return Objective(expr=obj_blk.L_infinity_obj_var)
299
300
301def generate_lag_objective_function(model, setpoint_model, config, solve_data, discrete_only=False):
302    """The function generate taylor extension of the Lagrangean function.
303
304    Args:
305        model ([type]): [description]
306        setpoint_model ([type]): [description]
307        discrete_only (bool, optional): [description]. Defaults to False.
308    """
309    temp_model = setpoint_model.clone()
310    for var in temp_model.MindtPy_utils.variable_list:
311        if var.is_integer():
312            var.unfix()
313    # objective_list[0] is the original objective function, not in MindtPy_utils block
314    temp_model.MindtPy_utils.objective_list[0].activate()
315    temp_model.MindtPy_utils.deactivate()
316    TransformationFactory('core.relax_integer_vars').apply_to(temp_model)
317    # Note: PyNumero does not support discrete variables
318    # So PyomoNLP should operate on setpoint_model
319
320    # Implementation 1
321    # First calculate Jacobian and Hessian without assigning variable and constraint sequence, then use get_primal_indices to get the indices.
322    with time_code(solve_data.timing, 'PyomoNLP'):
323        nlp = pyomo_nlp.PyomoNLP(temp_model)
324        lam = [-temp_model.dual[constr] if abs(temp_model.dual[constr]) > config.zero_tolerance else 0
325               for constr in nlp.get_pyomo_constraints()]
326        nlp.set_duals(lam)
327        obj_grad = nlp.evaluate_grad_objective().reshape(-1, 1)
328        jac = nlp.evaluate_jacobian().toarray()
329        jac_lag = obj_grad + jac.transpose().dot(numpy.array(lam).reshape(-1, 1))
330        jac_lag[abs(jac_lag) < config.zero_tolerance] = 0
331        # jac_lag of continuous variables should be zero
332        for var in temp_model.MindtPy_utils.continuous_variable_list[:-1]:
333            jac_lag[nlp.get_primal_indices([var])[0]] = 0
334        nlp_var = set([i.name for i in nlp.get_pyomo_variables()])
335        first_order_term = sum(float(jac_lag[nlp.get_primal_indices([temp_var])[0]]) * (var - temp_var.value) for var,
336                               temp_var in zip(model.MindtPy_utils.variable_list[:-1], temp_model.MindtPy_utils.variable_list[:-1]) if temp_var.name in nlp_var)
337
338        if config.add_regularization == 'grad_lag':
339            return Objective(expr=first_order_term, sense=minimize)
340        elif config.add_regularization in {'hess_lag', 'hess_only_lag'}:
341            # Implementation 1
342            hess_lag = nlp.evaluate_hessian_lag().toarray()
343            hess_lag[abs(hess_lag) < config.zero_tolerance] = 0
344            second_order_term = 0.5 * sum((var_i - temp_var_i.value) * float(hess_lag[nlp.get_primal_indices([temp_var_i])[0]][nlp.get_primal_indices([temp_var_j])[0]]) * (var_j - temp_var_j.value)
345                                          for var_i, temp_var_i in zip(model.MindtPy_utils.variable_list[:-1], temp_model.MindtPy_utils.variable_list[:-1])
346                                          for var_j, temp_var_j in zip(model.MindtPy_utils.variable_list[:-1], temp_model.MindtPy_utils.variable_list[:-1])
347                                          if (temp_var_i.name in nlp_var and temp_var_j.name in nlp_var))
348            if config.add_regularization == 'hess_lag':
349                return Objective(expr=first_order_term + second_order_term, sense=minimize)
350            elif config.add_regularization == 'hess_only_lag':
351                return Objective(expr=second_order_term, sense=minimize)
352        elif config.add_regularization == 'sqp_lag':
353            var_filter = (lambda v: v[1].is_integer()) if discrete_only \
354                else (lambda v: v[1].name != 'MindtPy_utils.objective_value' and
355                      'MindtPy_utils.feas_opt.slack_var' not in v[1].name)
356
357            model_vars, setpoint_vars = zip(*filter(var_filter,
358                                                    zip(model.component_data_objects(Var),
359                                                        setpoint_model.component_data_objects(Var))))
360            assert len(model_vars) == len(
361                setpoint_vars), 'Trying to generate Squared Norm2 objective function for models with different number of variables'
362            if config.sqp_lag_scaling_coef is None:
363                rho = 1
364            elif config.sqp_lag_scaling_coef == 'fixed':
365                r = 1
366                rho = numpy.linalg.norm(jac_lag/(2*r))
367            elif config.sqp_lag_scaling_coef == 'variable_dependent':
368                r = numpy.sqrt(
369                    len(temp_model.MindtPy_utils.discrete_variable_list))
370                rho = numpy.linalg.norm(jac_lag/(2*r))
371
372            return Objective(expr=first_order_term + rho*sum([(model_var - setpoint_var.value)**2 for (model_var, setpoint_var) in zip(model_vars, setpoint_vars)]))
373
374
375def generate_norm1_norm_constraint(model, setpoint_model, config, discrete_only=True):
376    """
377    This function generates constraint (PF-OA main problem) for minimum Norm1 distance to setpoint_model
378    Norm constraint is used to guarantees the monotonicity of the norm objective value sequence of all iterations
379    Norm1 distance of (x,y) = \sum_i |x_i - y_i|
380    Ref: Paper 'A storm of feasibility pumps for nonconvex MINLP' Eq. (16)
381
382    Parameters
383    ----------
384    model: Pyomo model
385        the model that needs new objective function
386    setpoint_model: Pyomo model
387        the model that provides the base point for us to calculate the distance
388    discrete_only: Bool
389        only optimize on distance between the discrete variables
390    """
391
392    var_filter = (lambda v: v.is_integer()) if discrete_only \
393        else (lambda v: True)
394    model_vars = list(filter(var_filter, model.component_data_objects(Var)))
395    setpoint_vars = list(
396        filter(var_filter, setpoint_model.component_data_objects(Var)))
397    assert len(model_vars) == len(
398        setpoint_vars), 'Trying to generate Norm1 norm constraint for models with different number of variables'
399    norm_constraint_blk = model.MindtPy_utils.L1_norm_constraint = Block()
400    norm_constraint_blk.L1_slack_idx = RangeSet(len(model_vars))
401    norm_constraint_blk.L1_slack_var = Var(
402        norm_constraint_blk.L1_slack_idx, domain=Reals, bounds=(0, None))
403    norm_constraint_blk.abs_reform = ConstraintList()
404    for idx, v_model, v_setpoint in zip(norm_constraint_blk.L1_slack_idx, model_vars,
405                                        setpoint_vars):
406        norm_constraint_blk.abs_reform.add(
407            expr=v_model - v_setpoint.value >= -norm_constraint_blk.L1_slack_var[idx])
408        norm_constraint_blk.abs_reform.add(
409            expr=v_model - v_setpoint.value <= norm_constraint_blk.L1_slack_var[idx])
410    rhs = config.fp_norm_constraint_coef * \
411        sum(abs(v_model.value-v_setpoint.value)
412            for v_model, v_setpoint in zip(model_vars, setpoint_vars))
413    norm_constraint_blk.sum_slack = Constraint(
414        expr=sum(norm_constraint_blk.L1_slack_var[idx] for idx in norm_constraint_blk.L1_slack_idx) <= rhs)
415
416
417def set_solver_options(opt, solve_data, config, solver_type, regularization=False):
418    """ set options for MIP/NLP solvers
419
420    Args:
421        opt : SolverFactory
422            the solver
423        solve_data: MindtPy Data Container
424            data container that holds solve-instance data
425        config: ConfigBlock
426            contains the specific configurations for the algorithm
427        solver_type: String
428            The type of the solver, i.e. mip or nlp
429        regularization (bool, optional): Boolean.
430            Defaults to False.
431    """
432    # TODO: integrate nlp_args here
433    # nlp_args = dict(config.nlp_solver_args)
434    elapsed = get_main_elapsed_time(solve_data.timing)
435    remaining = int(max(config.time_limit - elapsed, 1))
436    if solver_type == 'mip':
437        if regularization:
438            solver_name = config.mip_regularization_solver
439            if config.regularization_mip_threads > 0:
440                opt.options['threads'] = config.regularization_mip_threads
441        else:
442            solver_name = config.mip_solver
443            if config.threads > 0:
444                opt.options['threads'] = config.threads
445    elif solver_type == 'nlp':
446        solver_name = config.nlp_solver
447    # TODO: opt.name doesn't work for GAMS
448    if solver_name in {'cplex', 'gurobi', 'gurobi_persistent'}:
449        opt.options['timelimit'] = remaining
450        opt.options['mipgap'] = config.mip_solver_mipgap
451        if solver_name == 'gurobi_persistent' and config.single_tree:
452            # PreCrush: Controls presolve reductions that affect user cuts
453            # You should consider setting this parameter to 1 if you are using callbacks to add your own cuts.
454            opt.set_gurobi_param('PreCrush', 1)
455            opt.set_gurobi_param('LazyConstraints', 1)
456        if regularization == True:
457            if solver_name == 'cplex':
458                if config.solution_limit is not None:
459                    opt.options['mip limits solutions'] = config.solution_limit
460                opt.options['mip strategy presolvenode'] = 3
461                # TODO: need to discuss if this option should be added.
462                if config.add_regularization in {'hess_lag', 'hess_only_lag'}:
463                    opt.options['optimalitytarget'] = 3
464            elif solver_name == 'gurobi':
465                if config.solution_limit is not None:
466                    opt.options['SolutionLimit'] = config.solution_limit
467                opt.options['Presolve'] = 2
468    elif solver_name == 'cplex_persistent':
469        opt.options['timelimit'] = remaining
470        opt._solver_model.parameters.mip.tolerances.mipgap.set(
471            config.mip_solver_mipgap)
472        if regularization is True:
473            if config.solution_limit is not None:
474                opt._solver_model.parameters.mip.limits.solutions.set(
475                    config.solution_limit)
476            opt._solver_model.parameters.mip.strategy.presolvenode.set(3)
477            if config.add_regularization in {'hess_lag', 'hess_only_lag'}:
478                opt._solver_model.parameters.optimalitytarget.set(3)
479    elif solver_name == 'glpk':
480        opt.options['tmlim'] = remaining
481        # TODO: mipgap does not work for glpk yet
482        # opt.options['mipgap'] = config.mip_solver_mipgap
483    elif solver_name == 'baron':
484        opt.options['MaxTime'] = remaining
485        opt.options['AbsConFeasTol'] = config.zero_tolerance
486    elif solver_name == 'ipopt':
487        opt.options['max_cpu_time'] = remaining
488        opt.options['constr_viol_tol'] = config.zero_tolerance
489    elif solver_name == 'gams':
490        if solver_type == 'mip':
491            opt.options['add_options'] = ['option optcr=%s;' % config.mip_solver_mipgap,
492                                          'option reslim=%s;' % remaining]
493        elif solver_type == 'nlp':
494            opt.options['add_options'] = ['option reslim=%s;' % remaining]
495            if config.nlp_solver_args.__contains__('solver'):
496                if config.nlp_solver_args['solver'] in {'ipopt', 'ipopth', 'msnlp', 'conopt', 'baron'}:
497                    if config.nlp_solver_args['solver'] == 'ipopt':
498                        opt.options['add_options'].append(
499                            '$onecho > ipopt.opt')
500                        opt.options['add_options'].append(
501                            'constr_viol_tol ' + str(config.zero_tolerance))
502                    elif config.nlp_solver_args['solver'] == 'ipopth':
503                        opt.options['add_options'].append(
504                            '$onecho > ipopth.opt')
505                        opt.options['add_options'].append(
506                            'constr_viol_tol ' + str(config.zero_tolerance))
507                        # TODO: Ipopt warmstart option
508                        # opt.options['add_options'].append('warm_start_init_point       yes\n'
509                        #                                   'warm_start_bound_push       1e-9\n'
510                        #                                   'warm_start_bound_frac       1e-9\n'
511                        #                                   'warm_start_slack_bound_frac 1e-9\n'
512                        #                                   'warm_start_slack_bound_push 1e-9\n'
513                        #                                   'warm_start_mult_bound_push  1e-9\n')
514                    elif config.nlp_solver_args['solver'] == 'conopt':
515                        opt.options['add_options'].append(
516                            '$onecho > conopt.opt')
517                        opt.options['add_options'].append(
518                            'RTNWMA ' + str(config.zero_tolerance))
519                    elif config.nlp_solver_args['solver'] == 'msnlp':
520                        opt.options['add_options'].append(
521                            '$onecho > msnlp.opt')
522                        opt.options['add_options'].append(
523                            'feasibility_tolerance ' + str(config.zero_tolerance))
524                    elif config.nlp_solver_args['solver'] == 'baron':
525                        opt.options['add_options'].append(
526                            '$onecho > baron.opt')
527                        opt.options['add_options'].append(
528                            'AbsConFeasTol ' + str(config.zero_tolerance))
529                    opt.options['add_options'].append('$offecho')
530                    opt.options['add_options'].append('GAMS_MODEL.optfile=1')
531
532
533def get_integer_solution(model, string_zero=False):
534    """ obtain the value of integer variables from the provided model.
535
536    Args:
537        model: Pyomo model
538            the model to extract value of integer variables
539        string_zero: Boolean
540            whether to store zero as string
541    """
542    temp = []
543    for var in model.component_data_objects(ctype=Var):
544        if var.is_integer():
545            if string_zero:
546                if var.value == 0:
547                    # In cplex, negative zero is different from zero, so we use string to denote this(Only in singletree)
548                    temp.append(str(var.value))
549                else:
550                    temp.append(int(round(var.value)))
551            else:
552                temp.append(int(round(var.value)))
553    return tuple(temp)
554
555
556def setup_solve_data(model, config):
557    solve_data = MindtPySolveData()
558    solve_data.results = SolverResults()
559    solve_data.timing = Bunch()
560    solve_data.curr_int_sol = []
561    solve_data.should_terminate = False
562    solve_data.integer_list = []
563
564    # if the objective function is a constant, dual bound constraint is not added.
565    obj = next(model.component_data_objects(ctype=Objective, active=True))
566    if obj.expr.polynomial_degree() == 0:
567        config.use_dual_bound = False
568
569    if config.use_fbbt:
570        fbbt(model)
571        # TODO: logging_level is not logging.INFO here
572        config.logger.info(
573            'Use the fbbt to tighten the bounds of variables')
574
575    solve_data.original_model = model
576    solve_data.working_model = model.clone()
577
578    # Set up iteration counters
579    solve_data.nlp_iter = 0
580    solve_data.mip_iter = 0
581    solve_data.mip_subiter = 0
582    solve_data.nlp_infeasible_counter = 0
583    if config.init_strategy == 'FP':
584        solve_data.fp_iter = 1
585
586    # set up bounds
587    solve_data.LB = float('-inf')
588    solve_data.UB = float('inf')
589    solve_data.LB_progress = [solve_data.LB]
590    solve_data.UB_progress = [solve_data.UB]
591    if config.single_tree and (config.add_no_good_cuts or config.use_tabu_list):
592        solve_data.stored_bound = {}
593    if config.strategy == 'GOA' and (config.add_no_good_cuts or config.use_tabu_list):
594        solve_data.num_no_good_cuts_added = {}
595
596    # Flag indicating whether the solution improved in the past
597    # iteration or not
598    solve_data.solution_improved = False
599    solve_data.bound_improved = False
600
601    if config.nlp_solver == 'ipopt':
602        if not hasattr(solve_data.working_model, 'ipopt_zL_out'):
603            solve_data.working_model.ipopt_zL_out = Suffix(
604                direction=Suffix.IMPORT)
605        if not hasattr(solve_data.working_model, 'ipopt_zU_out'):
606            solve_data.working_model.ipopt_zU_out = Suffix(
607                direction=Suffix.IMPORT)
608
609    return solve_data
610
611
612class GurobiPersistent4MindtPy(GurobiPersistent):
613
614    def _intermediate_callback(self):
615        def f(gurobi_model, where):
616            self._callback_func(self._pyomo_model, self,
617                                where, self.solve_data, self.config)
618        return f
619